From 486b061558415aa1693b0c255f6cc9c6492d555b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 21 Apr 2026 15:40:17 -0700 Subject: [PATCH] Refactor runtime ownership and clean up warnings --- .gitignore | 2 +- README.md | 332 +- artifacts/captures/README.md | 14 + .../analysis/analysis-context-functions.csv | 0 .../analysis/analysis-context.md | 0 RT2.LOG => artifacts/captures/logs/RT2.LOG | Bin .../captures/logs/rt3_auto_load_winedbg.log | 0 .../captures/logs/rt3_manual_load_winedbg.log | 0 .../win-bin}/BuildingDetail.win.bin | Bin .../win-bin}/CompanyDetail.win.bin | Bin artifacts/exports/rt3-1.06/README.md | 49 + artifacts/exports/rt3-1.06/function-map.csv | 56 +- .../runtime-effect-kind8-startup-subgraph.dot | 4 +- .../runtime-effect-kind8-startup-subgraph.md | 36 +- ...effect-service-depth7-forward-subgraph.dot | 4 +- ...-effect-service-depth7-forward-subgraph.md | 34 +- ...runtime-effect-service-depth7-subgraph.dot | 4 +- .../runtime-effect-service-depth7-subgraph.md | 34 +- ...indow-submodes-depth5-forward-subgraph.dot | 2 +- ...window-submodes-depth5-forward-subgraph.md | 18 +- .../setup-window-submodes-depth5-subgraph.dot | 4 +- .../setup-window-submodes-depth5-subgraph.md | 32 +- .../world-entry-bringup-refresh-subgraph.dot | 2 +- .../world-entry-bringup-refresh-subgraph.md | 16 +- crates/rrt-cli/src/app/command/finance.rs | 20 + crates/rrt-cli/src/app/command/mod.rs | 158 + crates/rrt-cli/src/app/command/model.rs | 194 + .../src/app/command/runtime/compare.rs | 66 + .../src/app/command/runtime/fixture_state.rs | 73 + .../src/app/command/runtime/inspect.rs | 146 + crates/rrt-cli/src/app/command/runtime/mod.rs | 74 + .../rrt-cli/src/app/command/runtime/scan.rs | 44 + crates/rrt-cli/src/app/command/validate.rs | 18 + crates/rrt-cli/src/app/dispatch/finance.rs | 12 + crates/rrt-cli/src/app/dispatch/mod.rs | 81 + .../src/app/dispatch/runtime/compare.rs | 30 + .../src/app/dispatch/runtime/fixture_state.rs | 43 + .../src/app/dispatch/runtime/inspect.rs | 85 + .../rrt-cli/src/app/dispatch/runtime/mod.rs | 15 + .../rrt-cli/src/app/dispatch/runtime/scan.rs | 28 + crates/rrt-cli/src/app/dispatch/validate.rs | 15 + crates/rrt-cli/src/app/finance.rs | 110 + crates/rrt-cli/src/app/helpers/inspect.rs | 446 + crates/rrt-cli/src/app/helpers/mod.rs | 2 + crates/rrt-cli/src/app/helpers/state_io.rs | 102 + crates/rrt-cli/src/app/mod.rs | 16 + crates/rrt-cli/src/app/reports/inspect.rs | 258 + crates/rrt-cli/src/app/reports/mod.rs | 2 + crates/rrt-cli/src/app/reports/state.rs | 72 + .../app/runtime_compare/candidate_table.rs | 472 + .../rrt-cli/src/app/runtime_compare/common.rs | 104 + crates/rrt-cli/src/app/runtime_compare/mod.rs | 43 + .../src/app/runtime_compare/post_special.rs | 94 + .../src/app/runtime_compare/profiles.rs | 195 + .../src/app/runtime_compare/recipe_book.rs | 250 + .../rrt-cli/src/app/runtime_compare/region.rs | 33 + .../src/app/runtime_compare/setup_payload.rs | 345 + .../src/app/runtime_fixture_state/fixtures.rs | 115 + .../src/app/runtime_fixture_state/mod.rs | 9 + .../app/runtime_fixture_state/save_import.rs | 70 + .../app/runtime_fixture_state/save_load.rs | 29 + .../src/app/runtime_fixture_state/state.rs | 81 + .../rrt-cli/src/app/runtime_inspect/assets.rs | 175 + .../rrt-cli/src/app/runtime_inspect/maps.rs | 128 + crates/rrt-cli/src/app/runtime_inspect/mod.rs | 19 + crates/rrt-cli/src/app/runtime_inspect/smp.rs | 117 + .../src/app/runtime_scan/aligned_band.rs | 245 + .../src/app/runtime_scan/candidate_table.rs | 388 + crates/rrt-cli/src/app/runtime_scan/common.rs | 130 + crates/rrt-cli/src/app/runtime_scan/mod.rs | 15 + .../src/app/runtime_scan/post_special.rs | 489 + .../src/app/runtime_scan/recipe_book.rs | 183 + .../app/runtime_scan/special_conditions.rs | 167 + crates/rrt-cli/src/app/tests/compare.rs | 581 + crates/rrt-cli/src/app/tests/mod.rs | 32 + crates/rrt-cli/src/app/tests/state/diff.rs | 410 + .../src/app/tests/state/document_io.rs | 176 + .../src/app/tests/state/fixture_summary.rs | 77 + crates/rrt-cli/src/app/tests/state/mod.rs | 7 + .../src/app/tests/state/save_slice_overlay.rs | 265 + .../src/app/tests/state/snapshot_io.rs | 18 + crates/rrt-cli/src/app/tests/support/json.rs | 6 + crates/rrt-cli/src/app/tests/support/mod.rs | 5 + .../src/app/tests/support/temp_files.rs | 15 + crates/rrt-cli/src/app/validate.rs | 119 + crates/rrt-cli/src/main.rs | 7103 +--- crates/rrt-fixtures/src/lib.rs | 2 + crates/rrt-fixtures/src/load.rs | 196 +- crates/rrt-fixtures/src/normalize.rs | 2 +- crates/rrt-fixtures/src/schema.rs | 1780 - crates/rrt-fixtures/src/schema/document.rs | 68 + crates/rrt-fixtures/src/schema/mod.rs | 17 + .../rrt-fixtures/src/schema/state_fragment.rs | 49 + crates/rrt-fixtures/src/schema/summary.rs | 302 + .../src/schema/summary_compare/collections.rs | 345 + .../src/schema/summary_compare/mod.rs | 22 + .../schema/summary_compare/packed_events.rs | 314 + .../summary_compare/selected_company.rs | 194 + .../src/schema/summary_compare/world.rs | 398 + crates/rrt-fixtures/src/schema/tests.rs | 112 + crates/rrt-fixtures/src/schema/validate.rs | 37 + crates/rrt-fixtures/src/summary.rs | 1 + crates/rrt-fixtures/src/validation.rs | 2 + crates/rrt-hook/src/capture.rs | 156 + crates/rrt-hook/src/lib.rs | 3125 +- crates/rrt-hook/src/windows/auto_load.rs | 377 + crates/rrt-hook/src/windows/capture.rs | 333 + crates/rrt-hook/src/windows/constants.rs | 221 + crates/rrt-hook/src/windows/detours.rs | 292 + crates/rrt-hook/src/windows/ffi.rs | 86 + crates/rrt-hook/src/windows/install.rs | 383 + crates/rrt-hook/src/windows/logging.rs | 1096 + crates/rrt-hook/src/windows/memory.rs | 99 + crates/rrt-hook/src/windows/mod.rs | 71 + crates/rrt-hook/src/windows/state.rs | 45 + crates/rrt-runtime/src/building.rs | 1274 - crates/rrt-runtime/src/derived/company.rs | 320 + .../src/derived/finance/annual_policy.rs | 138 + .../src/derived/finance/bond_policy.rs | 168 + .../src/derived/finance/credit_rating.rs | 151 + .../src/derived/finance/distress.rs | 168 + .../src/derived/finance/dividend_policy.rs | 227 + crates/rrt-runtime/src/derived/finance/mod.rs | 13 + .../src/derived/finance/stock_actions.rs | 271 + crates/rrt-runtime/src/derived/mod.rs | 10 + .../src/derived/tests/bond_metrics.rs | 260 + .../rrt-runtime/src/derived/tests/company.rs | 2519 ++ .../rrt-runtime/src/derived/tests/finance.rs | 1565 + .../src/derived/tests/issue_state.rs | 490 + crates/rrt-runtime/src/derived/tests/mod.rs | 15 + .../src/derived/tests/selected_year.rs | 213 + .../src/derived/tests/share_price.rs | 598 + .../src/derived/world/bond_metrics.rs | 73 + .../rrt-runtime/src/derived/world/calendar.rs | 92 + .../src/derived/world/issue_state.rs | 206 + crates/rrt-runtime/src/derived/world/mod.rs | 13 + .../src/derived/world/selected_year.rs | 120 + .../src/derived/world/share_price.rs | 94 + .../src/derived/world/stat_readers.rs | 356 + crates/rrt-runtime/src/documents/blockers.rs | 404 + crates/rrt-runtime/src/documents/io.rs | 179 + .../lowering/condition_targets/blockers.rs | 118 + .../lowering/condition_targets/conditions.rs | 213 + .../condition_targets/context_targets.rs | 100 + .../lowering/condition_targets/effects.rs | 256 + .../lowering/condition_targets/mod.rs | 16 + .../lowering/condition_targets/predicates.rs | 58 + .../lowering/condition_targets/territory.rs | 28 + .../documents/lowering/contextual_effects.rs | 244 + .../src/documents/lowering/decode/actions.rs | 39 + .../documents/lowering/decode/conditions.rs | 35 + .../src/documents/lowering/decode/effects.rs | 263 + .../src/documents/lowering/decode/mod.rs | 9 + .../src/documents/lowering/decode/record.rs | 81 + .../src/documents/lowering/decode/targets.rs | 56 + .../lowering/import_outcome/conditions.rs | 71 + .../lowering/import_outcome/descriptors.rs | 81 + .../documents/lowering/import_outcome/mod.rs | 10 + .../lowering/import_outcome/outcome.rs | 124 + .../lowering/import_outcome/targets.rs | 194 + .../lowering/import_outcome/variants.rs | 63 + .../rrt-runtime/src/documents/lowering/mod.rs | 10 + .../src/documents/lowering/summary.rs | 266 + crates/rrt-runtime/src/documents/mod.rs | 17 + crates/rrt-runtime/src/documents/model.rs | 248 + .../src/documents/project/actors.rs | 273 + .../src/documents/project/entrypoints.rs | 62 + .../src/documents/project/events.rs | 96 + .../src/documents/project/metadata.rs | 413 + .../rrt-runtime/src/documents/project/mod.rs | 22 + .../src/documents/project/near_city.rs | 306 + .../project/profile_world/availability.rs | 93 + .../project/profile_world/catalogs.rs | 122 + .../documents/project/profile_world/mod.rs | 85 + .../project/profile_world/overrides.rs | 32 + .../project/profile_world/save_profile.rs | 73 + .../project/profile_world/world_restore.rs | 268 + .../src/documents/project/projection.rs | 100 + .../src/documents/project/state_build.rs | 202 + crates/rrt-runtime/src/documents/tests/mod.rs | 26 + .../src/documents/tests/overlay_import.rs | 580 + .../src/documents/tests/packed_events.rs | 8145 ++++ .../src/documents/tests/profile_world.rs | 223 + .../src/documents/tests/projection.rs | 813 + .../src/documents/tests/roundtrip.rs | 129 + .../src/documents/tests/support.rs | 1640 + .../src/documents/tests/validation.rs | 194 + crates/rrt-runtime/src/economy.rs | 875 - crates/rrt-runtime/src/engine/advance.rs | 93 + crates/rrt-runtime/src/engine/command.rs | 112 + .../src/engine/conditions/catalogs.rs | 88 + .../src/engine/conditions/context.rs | 50 + .../src/engine/conditions/entities.rs | 140 + .../src/engine/conditions/entrypoints.rs | 317 + .../rrt-runtime/src/engine/conditions/mod.rs | 9 + .../src/engine/conditions/territories.rs | 40 + .../src/engine/conditions/world_state.rs | 75 + .../src/engine/effects/catalogs.rs | 74 + .../rrt-runtime/src/engine/effects/company.rs | 302 + .../src/engine/effects/entrypoints.rs | 253 + .../src/engine/effects/event_graph.rs | 38 + crates/rrt-runtime/src/engine/effects/mod.rs | 12 + .../rrt-runtime/src/engine/effects/model.rs | 18 + .../rrt-runtime/src/engine/effects/players.rs | 141 + .../src/engine/effects/territories.rs | 24 + .../rrt-runtime/src/engine/effects/trains.rs | 27 + .../rrt-runtime/src/engine/effects/world.rs | 45 + crates/rrt-runtime/src/engine/metrics.rs | 219 + crates/rrt-runtime/src/engine/mod.rs | 15 + crates/rrt-runtime/src/engine/mutations.rs | 57 + .../service/annual_finance/bankruptcy.rs | 6 + .../service/annual_finance/bond_issue.rs | 132 + .../service/annual_finance/constants.rs | 3 + .../engine/service/annual_finance/dividend.rs | 30 + .../service/annual_finance/entrypoints.rs | 124 + .../src/engine/service/annual_finance/mod.rs | 10 + .../src/engine/service/annual_finance/news.rs | 44 + .../service/annual_finance/stock_issue.rs | 71 + .../annual_finance/stock_repurchase.rs | 86 + .../rrt-runtime/src/engine/service/bonds.rs | 105 + .../src/engine/service/company_fields.rs | 242 + .../src/engine/service/company_lifecycle.rs | 113 + crates/rrt-runtime/src/engine/service/mod.rs | 22 + .../engine/service/near_city_acquisition.rs | 170 + .../src/engine/service/periodic.rs | 72 + .../src/engine/service/trigger_dispatch.rs | 120 + .../src/engine/targets/chairmen.rs | 102 + .../rrt-runtime/src/engine/targets/company.rs | 89 + crates/rrt-runtime/src/engine/targets/mod.rs | 10 + .../rrt-runtime/src/engine/targets/players.rs | 86 + .../rrt-runtime/src/engine/targets/shared.rs | 44 + .../src/engine/targets/territories.rs | 30 + .../rrt-runtime/src/engine/tests/advance.rs | 163 + .../src/engine/tests/annual_finance.rs | 1345 + .../rrt-runtime/src/engine/tests/effects.rs | 1439 + .../src/engine/tests/event_graph.rs | 504 + crates/rrt-runtime/src/engine/tests/mod.rs | 21 + .../rrt-runtime/src/engine/tests/periodic.rs | 257 + .../rrt-runtime/src/engine/tests/support.rs | 25 + .../rrt-runtime/src/engine/tests/targets.rs | 1289 + crates/rrt-runtime/src/event/conditions.rs | 130 + crates/rrt-runtime/src/event/effects.rs | 147 + crates/rrt-runtime/src/event/metrics.rs | 133 + crates/rrt-runtime/src/event/mod.rs | 7 + crates/rrt-runtime/src/event/news.rs | 29 + crates/rrt-runtime/src/event/packed.rs | 186 + crates/rrt-runtime/src/event/records.rs | 54 + crates/rrt-runtime/src/event/targets.rs | 66 + crates/rrt-runtime/src/import.rs | 16210 -------- crates/rrt-runtime/src/inspect.rs | 6 + .../src/inspect/building/bindings.rs | 51 + .../rrt-runtime/src/inspect/building/mod.rs | 355 + .../rrt-runtime/src/inspect/building/probe.rs | 63 + .../src/inspect/building/recovered_tables.rs | 475 + .../rrt-runtime/src/inspect/building/scan.rs | 184 + .../rrt-runtime/src/inspect/building/types.rs | 184 + .../{campaign_exe.rs => inspect/campaign.rs} | 0 .../rrt-runtime/src/inspect/cargo/bindings.rs | 30 + .../src/inspect/cargo/cargo_skin.rs | 72 + .../src/inspect/cargo/cargo_types.rs | 81 + .../rrt-runtime/src/inspect/cargo/economy.rs | 159 + crates/rrt-runtime/src/inspect/cargo/mod.rs | 127 + .../rrt-runtime/src/inspect/cargo/registry.rs | 78 + .../rrt-runtime/src/inspect/cargo/selector.rs | 94 + crates/rrt-runtime/src/inspect/cargo/types.rs | 124 + crates/rrt-runtime/src/{ => inspect}/pk4.rs | 0 .../src/inspect/smp/bundle/anchor.rs | 185 + .../src/inspect/smp/bundle/ascii.rs | 88 + .../src/inspect/smp/bundle/entrypoints.rs | 520 + .../rrt-runtime/src/inspect/smp/bundle/mod.rs | 31 + .../src/inspect/smp/bundle/model.rs | 160 + .../src/inspect/smp/bundle/post_span.rs | 158 + .../src/inspect/smp/bundle/preamble.rs | 76 + .../src/inspect/smp/bundle/report.rs | 179 + .../src/inspect/smp/bundle/trailer.rs | 134 + .../src/inspect/smp/bundle/variants.rs | 431 + .../inspect/smp/catalog/conditions/cargo.rs | 142 + .../src/inspect/smp/catalog/conditions/ids.rs | 35 + .../inspect/smp/catalog/conditions/kinds.rs | 43 + .../inspect/smp/catalog/conditions/lookup.rs | 38 + .../src/inspect/smp/catalog/conditions/mod.rs | 13 + .../inspect/smp/catalog/conditions/table.rs | 351 + .../src/inspect/smp/catalog/descriptors.rs | 217 + .../src/inspect/smp/catalog/fixups.rs | 16 + .../src/inspect/smp/catalog/mod.rs | 12 + .../src/inspect/smp/catalog/offsets/bundle.rs | 13 + .../src/inspect/smp/catalog/offsets/events.rs | 26 + .../src/inspect/smp/catalog/offsets/mod.rs | 5 + .../inspect/smp/catalog/offsets/regions.rs | 15 + .../smp/catalog/offsets/special_conditions.rs | 80 + .../src/inspect/smp/catalog/offsets/world.rs | 72 + .../inspect/smp/catalog/special_conditions.rs | 275 + .../src/inspect/smp/catalog/tags.rs | 29 + crates/rrt-runtime/src/inspect/smp/common.rs | 46 + .../src/inspect/smp/common/ascii.rs | 46 + .../src/inspect/smp/common/headers.rs | 273 + .../src/inspect/smp/common/model.rs | 121 + .../src/inspect/smp/common/names.rs | 159 + .../src/inspect/smp/common/read.rs | 102 + .../src/inspect/smp/common/recipe.rs | 95 + .../src/inspect/smp/common/search.rs | 52 + .../src/inspect/smp/common/signatures.rs | 50 + .../src/inspect/smp/events/actions/actors.rs | 195 + .../inspect/smp/events/actions/catalogs.rs | 69 + .../src/inspect/smp/events/actions/decode.rs | 53 + .../src/inspect/smp/events/actions/mod.rs | 18 + .../inspect/smp/events/actions/row_summary.rs | 160 + .../smp/events/actions/runtime_variables.rs | 58 + .../src/inspect/smp/events/actions/targets.rs | 43 + .../src/inspect/smp/events/actions/trains.rs | 35 + .../src/inspect/smp/events/actions/world.rs | 67 + .../smp/events/collection/control_lane.rs | 484 + .../inspect/smp/events/collection/live_ids.rs | 34 + .../src/inspect/smp/events/collection/mod.rs | 4 + .../inspect/smp/events/collection/records.rs | 62 + .../inspect/smp/events/collection/summary.rs | 193 + .../src/inspect/smp/events/compat.rs | 115 + .../inspect/smp/events/conditions/labels.rs | 132 + .../src/inspect/smp/events/conditions/mod.rs | 7 + .../events/conditions/negative_sentinel.rs | 49 + .../smp/events/conditions/row_decode.rs | 370 + .../inspect/smp/events/descriptors/cargo.rs | 211 + .../smp/events/descriptors/classification.rs | 73 + .../smp/events/descriptors/locomotives.rs | 207 + .../smp/events/descriptors/metadata.rs | 52 + .../src/inspect/smp/events/descriptors/mod.rs | 7 + .../smp/events/descriptors/runtime_keys.rs | 72 + .../events/descriptors/special_conditions.rs | 45 + .../inspect/smp/events/descriptors/targets.rs | 108 + .../rrt-runtime/src/inspect/smp/events/mod.rs | 80 + .../src/inspect/smp/events/model.rs | 195 + .../src/inspect/smp/events/nondirect.rs | 401 + .../src/inspect/smp/events/real_records.rs | 278 + .../src/inspect/smp/events/synthetic.rs | 269 + .../rrt-runtime/src/inspect/smp/map_title.rs | 238 + .../src/inspect/smp/map_title/model.rs | 33 + crates/rrt-runtime/src/inspect/smp/mod.rs | 150 + .../src/inspect/smp/profiles/classic.rs | 102 + .../src/inspect/smp/profiles/mod.rs | 28 + .../src/inspect/smp/profiles/model.rs | 145 + .../src/inspect/smp/profiles/name_table.rs | 178 + .../inspect/smp/profiles/named_locomotive.rs | 143 + .../src/inspect/smp/profiles/rt3_105.rs | 320 + .../src/inspect/smp/regions/compare.rs | 139 + .../src/inspect/smp/regions/fixed_rows.rs | 305 + .../src/inspect/smp/regions/headers.rs | 205 + .../src/inspect/smp/regions/mod.rs | 27 + .../src/inspect/smp/regions/model.rs | 225 + .../src/inspect/smp/regions/queued_notice.rs | 77 + .../src/inspect/smp/regions/triplets.rs | 263 + .../src/inspect/smp/save_load/assembly.rs | 257 + .../src/inspect/smp/save_load/entrypoints.rs | 23 + .../inspect/smp/save_load/loaded_regions.rs | 57 + .../src/inspect/smp/save_load/mod.rs | 23 + .../src/inspect/smp/save_load/model.rs | 190 + .../src/inspect/smp/save_load/notes.rs | 315 + .../src/inspect/smp/save_load/summary.rs | 175 + .../inspect/smp/services/company/entries.rs | 148 + .../smp/services/company/entrypoints.rs | 22 + .../src/inspect/smp/services/company/mod.rs | 9 + .../company/near_city_acquisition/gaps.rs | 14 + .../near_city_acquisition/hypotheses.rs | 109 + .../near_city_acquisition/input_fields.rs | 37 + .../company/near_city_acquisition/mod.rs | 73 + .../company/near_city_acquisition/model.rs | 31 + .../projection_status.rs | 97 + .../company/near_city_acquisition/tri_lane.rs | 86 + .../company/notes/acquisition_frontier.rs | 94 + .../company/notes/linked_transit_reference.rs | 192 + .../company/notes/linked_transit_runtime.rs | 13 + .../inspect/smp/services/company/notes/mod.rs | 33 + .../company/notes/save_side_payload.rs | 85 + .../inspect/smp/services/company/peer_site.rs | 140 + .../company/status/linked_transit_fields.rs | 35 + .../smp/services/company/status/mod.rs | 6 + .../company/status/near_city_fields.rs | 105 + .../company/status/peer_site_fields.rs | 108 + .../smp/services/company/status/report.rs | 151 + .../smp/services/infrastructure/atlas.rs | 69 + .../services/infrastructure/entrypoints.rs | 13 + .../services/infrastructure/family_scan.rs | 43 + .../hypotheses/consumers/attach_rebuild.rs | 81 + .../consumers/constructor_families.rs | 24 + .../consumers/live_entry_directory.rs | 8 + .../hypotheses/consumers/mod.rs | 114 + .../hypotheses/consumers/payload_writer.rs | 27 + .../hypotheses/consumers/save_side_corpus.rs | 521 + .../infrastructure/hypotheses/follow_on.rs | 31 + .../infrastructure/hypotheses/footer_flags.rs | 8 + .../services/infrastructure/hypotheses/mod.rs | 19 + .../infrastructure/hypotheses/notes.rs | 14 + .../smp/services/infrastructure/mod.rs | 8 + .../smp/services/infrastructure/status.rs | 48 + .../src/inspect/smp/services/mod.rs | 13 + .../src/inspect/smp/services/model/company.rs | 209 + .../smp/services/model/infrastructure.rs | 40 + .../src/inspect/smp/services/model/mod.rs | 9 + .../src/inspect/smp/services/model/region.rs | 43 + .../src/inspect/smp/services/model/shared.rs | 38 + .../src/inspect/smp/services/region/atlas.rs | 60 + .../inspect/smp/services/region/entries.rs | 72 + .../smp/services/region/entrypoints.rs | 11 + .../region/hypotheses/later_restore.rs | 27 + .../smp/services/region/hypotheses/mod.rs | 22 + .../region/hypotheses/modal_dispatch.rs | 19 + .../region/hypotheses/pending_bonus.rs | 32 + .../region/hypotheses/periodic_producer.rs | 22 + .../region/hypotheses/post_load_generation.rs | 32 + .../services/region/hypotheses/tagged_load.rs | 46 + .../src/inspect/smp/services/region/mod.rs | 16 + .../src/inspect/smp/services/region/notes.rs | 84 + .../src/inspect/smp/services/region/report.rs | 36 + .../src/inspect/smp/services/shared.rs | 419 + .../smp/special_conditions/aligned_band.rs | 174 + .../special_conditions/locomotive_policy.rs | 182 + .../src/inspect/smp/special_conditions/mod.rs | 22 + .../inspect/smp/special_conditions/model.rs | 355 + .../smp/special_conditions/post_scalar.rs | 448 + .../inspect/smp/special_conditions/recipe.rs | 322 + .../inspect/smp/special_conditions/table.rs | 85 + .../src/inspect/smp/structures/alignment.rs | 92 + .../dynamic_side_buffer/embedded_names.rs | 61 + .../dynamic_side_buffer/evidence.rs | 161 + .../dynamic_side_buffer/live_entry_prelude.rs | 191 + .../smp/structures/dynamic_side_buffer/mod.rs | 12 + .../dynamic_side_buffer/mode_family/mod.rs | 21 + .../name_prelude/candidate_rows.rs | 54 + .../name_prelude/candidate_summary.rs | 139 + .../correlations/candidate_pattern.rs | 162 + .../correlations/compact_prefix.rs | 234 + .../name_prelude/correlations/mod.rs | 7 + .../name_prelude/correlations/profile_span.rs | 219 + .../dynamic_side_buffer/name_prelude/mod.rs | 9 + .../dynamic_side_buffer/name_prelude/parse.rs | 65 + .../name_prelude/profile_span.rs | 237 + .../payload_envelope/fixed_policy.rs | 324 + .../payload_envelope/mod.rs | 9 + .../payload_envelope/rows.rs | 97 + .../payload_envelope/short_profile.rs | 114 + .../payload_envelope/span_stats.rs | 80 + .../dynamic_side_buffer/prefix_patterns.rs | 176 + .../structures/dynamic_side_buffer/scan.rs | 330 + .../structures/dynamic_side_buffer/summary.rs | 30 + .../src/inspect/smp/structures/entrypoints.rs | 37 + .../src/inspect/smp/structures/mod.rs | 19 + .../inspect/smp/structures/model/alignment.rs | 21 + .../model/dynamic_side_buffer/core.rs | 93 + .../model/dynamic_side_buffer/mod.rs | 9 + .../model/dynamic_side_buffer/patterns.rs | 55 + .../model/dynamic_side_buffer/payload.rs | 254 + .../model/dynamic_side_buffer/prelude.rs | 269 + .../src/inspect/smp/structures/model/mod.rs | 7 + .../inspect/smp/structures/model/triplets.rs | 81 + .../inspect/smp/structures/queued_notice.rs | 44 + .../src/inspect/smp/structures/triplets.rs | 246 + .../inspect/smp/tests/events/collection.rs | 615 + .../smp/tests/events/conditions_actions.rs | 1037 + .../inspect/smp/tests/events/descriptors.rs | 1159 + .../src/inspect/smp/tests/events/mod.rs | 5 + .../rrt-runtime/src/inspect/smp/tests/mod.rs | 22 + .../src/inspect/smp/tests/profiles.rs | 722 + .../src/inspect/smp/tests/regions.rs | 739 + .../src/inspect/smp/tests/save_load.rs | 1288 + .../smp/tests/services/infrastructure.rs | 1124 + .../src/inspect/smp/tests/services/mod.rs | 8 + .../smp/tests/services/periodic_company.rs | 1838 + .../src/inspect/smp/tests/services/region.rs | 225 + .../src/inspect/smp/tests/services/shared.rs | 59 + .../inspect/smp/tests/special_conditions.rs | 613 + .../src/inspect/smp/tests/structures.rs | 685 + .../src/inspect/smp/tests/support.rs | 238 + .../src/inspect/smp/tests/world.rs | 1401 + .../src/inspect/smp/world/analysis.rs | 137 + .../src/inspect/smp/world/catalogs.rs | 113 + .../src/inspect/smp/world/chairman_records.rs | 353 + .../smp/world/company_records/analysis.rs | 225 + .../smp/world/company_records/bonds.rs | 92 + .../smp/world/company_records/capacity.rs | 17 + .../inspect/smp/world/company_records/mod.rs | 7 + .../smp/world/company_records/offsets.rs | 104 + .../smp/world/company_records/roster.rs | 296 + .../inspect/smp/world/company_records/scan.rs | 115 + .../smp/world/company_records/stat_bands.rs | 87 + .../src/inspect/smp/world/derived_state.rs | 202 + .../src/inspect/smp/world/entrypoints.rs | 19 + .../rrt-runtime/src/inspect/smp/world/mod.rs | 33 + .../src/inspect/smp/world/model/analysis.rs | 150 + .../src/inspect/smp/world/model/mod.rs | 9 + .../src/inspect/smp/world/model/roster.rs | 84 + .../src/inspect/smp/world/model/selection.rs | 44 + .../inspect/smp/world/model/world_probes.rs | 193 + .../src/inspect/smp/world/notes.rs | 198 + .../src/inspect/smp/world/selection.rs | 61 + .../smp/world/world_probes/economic_tuning.rs | 90 + .../world_probes/finance_neighborhood.rs | 180 + .../smp/world/world_probes/issue_37.rs | 122 + .../src/inspect/smp/world/world_probes/mod.rs | 9 + .../world/world_probes/selection_context.rs | 123 + .../src/inspect/win/entrypoints.rs | 97 + crates/rrt-runtime/src/inspect/win/header.rs | 59 + crates/rrt-runtime/src/inspect/win/mod.rs | 44 + crates/rrt-runtime/src/inspect/win/names.rs | 33 + crates/rrt-runtime/src/inspect/win/read.rs | 4 + .../rrt-runtime/src/inspect/win/references.rs | 134 + .../rrt-runtime/src/inspect/win/selectors.rs | 115 + crates/rrt-runtime/src/inspect/win/types.rs | 117 + crates/rrt-runtime/src/lib.rs | 166 +- crates/rrt-runtime/src/persistence.rs | 5 +- crates/rrt-runtime/src/runtime.rs | 10913 ----- crates/rrt-runtime/src/smp.rs | 33997 ---------------- crates/rrt-runtime/src/state/core/mod.rs | 12 + crates/rrt-runtime/src/state/core/profile.rs | 21 + .../src/state/core/refresh_market.rs | 128 + .../src/state/core/refresh_world.rs | 109 + .../src/state/core/runtime_state.rs | 87 + crates/rrt-runtime/src/state/core/service.rs | 70 + .../src/state/core/validate/catalogs.rs | 149 + .../src/state/core/validate/context.rs | 14 + .../src/state/core/validate/entities.rs | 218 + .../src/state/core/validate/events.rs | 272 + .../src/state/core/validate/metadata.rs | 71 + .../src/state/core/validate/mod.rs | 26 + .../state/core/validate/runtime_variables.rs | 109 + .../src/state/core/validate/service_state.rs | 134 + .../src/state/core/world_restore.rs | 171 + .../rrt-runtime/src/state/entities/actors.rs | 43 + .../src/state/entities/companies.rs | 61 + .../src/state/entities/company_finance.rs | 289 + .../src/state/entities/company_market.rs | 123 + crates/rrt-runtime/src/state/entities/mod.rs | 15 + .../src/state/entities/near_city.rs | 54 + .../src/state/entities/territories.rs | 26 + .../rrt-runtime/src/state/entities/trains.rs | 39 + crates/rrt-runtime/src/state/mod.rs | 7 + crates/rrt-runtime/src/step.rs | 7930 ---- crates/rrt-runtime/src/summary.rs | 4167 -- .../rrt-runtime/src/summary/builders/base.rs | 296 + .../src/summary/builders/collections.rs | 276 + .../rrt-runtime/src/summary/builders/mod.rs | 28 + .../summary/builders/packed_events/apply.rs | 119 + .../packed_events/blocked_outcomes.rs | 185 + .../builders/packed_events/collection.rs | 55 + .../src/summary/builders/packed_events/mod.rs | 132 + .../selected_company/annual_finance.rs | 68 + .../builders/selected_company/bond_policy.rs | 68 + .../builders/selected_company/distress.rs | 69 + .../selected_company/dividend_policy.rs | 48 + .../selected_company/equity_actions.rs | 136 + .../builders/selected_company/inputs.rs | 73 + .../builders/selected_company/market.rs | 101 + .../summary/builders/selected_company/mod.rs | 388 + .../builders/selected_company/model.rs | 124 + .../builders/selected_company/periodic.rs | 68 + .../src/summary/builders/world/apply.rs | 220 + .../src/summary/builders/world/economy.rs | 35 + .../src/summary/builders/world/mod.rs | 27 + .../src/summary/builders/world/model.rs | 84 + .../src/summary/builders/world/overview.rs | 19 + .../summary/builders/world/policy_state.rs | 83 + .../summary/builders/world/restore_core.rs | 35 + .../summary/builders/world/selected_year.rs | 53 + crates/rrt-runtime/src/summary/mod.rs | 16 + crates/rrt-runtime/src/summary/model.rs | 308 + crates/rrt-runtime/src/summary/tests.rs | 2495 ++ crates/rrt-runtime/src/test_support/fs.rs | 18 + crates/rrt-runtime/src/test_support/mod.rs | 7 + crates/rrt-runtime/src/test_support/state.rs | 56 + .../rrt-runtime/src/validation/documents.rs | 108 + crates/rrt-runtime/src/validation/mod.rs | 5 + .../src/validation/runtime/conditions.rs | 98 + .../src/validation/runtime/effects.rs | 158 + .../src/validation/runtime/metrics.rs | 16 + .../rrt-runtime/src/validation/runtime/mod.rs | 14 + .../src/validation/runtime/targets.rs | 101 + .../src/validation/runtime/templates.rs | 46 + crates/rrt-runtime/src/win.rs | 551 - docs/README.md | 13 + docs/control-loop-atlas.md | 20 +- ...tion-paintterrain-and-save-load-restore.md | 4 +- ...ntime-roots-camera-and-support-families.md | 12 +- .../station-detail-overlay.md | 6 +- docs/function-map.md | 15 + docs/history/progress-history.md | 311 + docs/re-workflow.md | 15 + docs/rehost-queue.md | 2124 +- docs/rehost-queue/README.md | 6 + docs/rehost-queue/archive-2026-04-19.md | 2120 + docs/runtime-rehost-plan.md | 8 +- docs/{atlas => subsystem-views}/README.md | 16 +- .../company-and-ledger.md | 10 +- .../editor-and-site-service.md | 0 .../{atlas => subsystem-views}/multiplayer.md | 0 .../route-entry-and-trackers.md | 2 +- .../runtime-and-world-tools.md | 0 .../startup-shell-and-content.md | 0 ...te.json => minimal-world-state-input.json} | 4 +- ...d-event-chairman-cash-overlay-fixture.json | 2 +- ...nt-chairman-condition-overlay-fixture.json | 2 +- ...an-missing-context-save-slice-fixture.json | 2 +- ...irman-scope-parity-save-slice-fixture.json | 2 +- ...-governance-condition-overlay-fixture.json | 2 +- ...-event-confiscate-all-overlay-fixture.json | 2 +- ...t-deactivate-chairman-overlay-fixture.json | 2 +- ...nt-deactivate-company-overlay-fixture.json | 2 +- ...ent-deactivate-player-overlay-fixture.json | 2 +- ...event-economic-status-overlay-fixture.json | 2 +- ...comotive-availability-overlay-fixture.json | 2 +- ...event-locomotive-cost-overlay-fixture.json | 2 +- ...ed-company-descriptor-overlay-fixture.json | 2 +- ...egative-company-scope-overlay-fixture.json | 2 +- ...inary-company-finance-overlay-fixture.json | 2 +- ...mpany-territory-track-overlay-fixture.json | 2 +- ...rdinary-company-track-overlay-fixture.json | 2 +- ...med-company-territory-overlay-fixture.json | 2 +- ...-territory-executable-overlay-fixture.json | 2 +- ...inary-named-territory-overlay-fixture.json | 2 +- ...inary-territory-track-overlay-fixture.json | 2 +- ...ked-event-player-cash-overlay-fixture.json | 2 +- ...-retire-train-company-overlay-fixture.json | 2 +- ...etire-train-territory-overlay-fixture.json | 2 +- ...me-variable-condition-overlay-fixture.json | 2 +- ...vent-runtime-variable-overlay-fixture.json | 2 +- ...election-only-context-overlay-fixture.json | 2 +- ...vent-selective-import-overlay-fixture.json | 2 +- ...ymbolic-company-scope-overlay-fixture.json | 2 +- ...vent-territory-access-overlay-fixture.json | 2 +- ...-event-track-capacity-overlay-fixture.json | 2 +- ...-world-flag-condition-overlay-fixture.json | 2 +- 628 files changed, 97954 insertions(+), 90763 deletions(-) create mode 100644 artifacts/captures/README.md rename artifacts/{tmp => captures}/analysis/analysis-context-functions.csv (100%) rename artifacts/{tmp => captures}/analysis/analysis-context.md (100%) rename RT2.LOG => artifacts/captures/logs/RT2.LOG (100%) rename rt3_auto_load_winedbg.log => artifacts/captures/logs/rt3_auto_load_winedbg.log (100%) rename rt3_manual_load_winedbg.log => artifacts/captures/logs/rt3_manual_load_winedbg.log (100%) rename artifacts/{tmp => captures/win-bin}/BuildingDetail.win.bin (100%) rename artifacts/{tmp => captures/win-bin}/CompanyDetail.win.bin (100%) create mode 100644 artifacts/exports/rt3-1.06/README.md create mode 100644 crates/rrt-cli/src/app/command/finance.rs create mode 100644 crates/rrt-cli/src/app/command/mod.rs create mode 100644 crates/rrt-cli/src/app/command/model.rs create mode 100644 crates/rrt-cli/src/app/command/runtime/compare.rs create mode 100644 crates/rrt-cli/src/app/command/runtime/fixture_state.rs create mode 100644 crates/rrt-cli/src/app/command/runtime/inspect.rs create mode 100644 crates/rrt-cli/src/app/command/runtime/mod.rs create mode 100644 crates/rrt-cli/src/app/command/runtime/scan.rs create mode 100644 crates/rrt-cli/src/app/command/validate.rs create mode 100644 crates/rrt-cli/src/app/dispatch/finance.rs create mode 100644 crates/rrt-cli/src/app/dispatch/mod.rs create mode 100644 crates/rrt-cli/src/app/dispatch/runtime/compare.rs create mode 100644 crates/rrt-cli/src/app/dispatch/runtime/fixture_state.rs create mode 100644 crates/rrt-cli/src/app/dispatch/runtime/inspect.rs create mode 100644 crates/rrt-cli/src/app/dispatch/runtime/mod.rs create mode 100644 crates/rrt-cli/src/app/dispatch/runtime/scan.rs create mode 100644 crates/rrt-cli/src/app/dispatch/validate.rs create mode 100644 crates/rrt-cli/src/app/finance.rs create mode 100644 crates/rrt-cli/src/app/helpers/inspect.rs create mode 100644 crates/rrt-cli/src/app/helpers/mod.rs create mode 100644 crates/rrt-cli/src/app/helpers/state_io.rs create mode 100644 crates/rrt-cli/src/app/mod.rs create mode 100644 crates/rrt-cli/src/app/reports/inspect.rs create mode 100644 crates/rrt-cli/src/app/reports/mod.rs create mode 100644 crates/rrt-cli/src/app/reports/state.rs create mode 100644 crates/rrt-cli/src/app/runtime_compare/candidate_table.rs create mode 100644 crates/rrt-cli/src/app/runtime_compare/common.rs create mode 100644 crates/rrt-cli/src/app/runtime_compare/mod.rs create mode 100644 crates/rrt-cli/src/app/runtime_compare/post_special.rs create mode 100644 crates/rrt-cli/src/app/runtime_compare/profiles.rs create mode 100644 crates/rrt-cli/src/app/runtime_compare/recipe_book.rs create mode 100644 crates/rrt-cli/src/app/runtime_compare/region.rs create mode 100644 crates/rrt-cli/src/app/runtime_compare/setup_payload.rs create mode 100644 crates/rrt-cli/src/app/runtime_fixture_state/fixtures.rs create mode 100644 crates/rrt-cli/src/app/runtime_fixture_state/mod.rs create mode 100644 crates/rrt-cli/src/app/runtime_fixture_state/save_import.rs create mode 100644 crates/rrt-cli/src/app/runtime_fixture_state/save_load.rs create mode 100644 crates/rrt-cli/src/app/runtime_fixture_state/state.rs create mode 100644 crates/rrt-cli/src/app/runtime_inspect/assets.rs create mode 100644 crates/rrt-cli/src/app/runtime_inspect/maps.rs create mode 100644 crates/rrt-cli/src/app/runtime_inspect/mod.rs create mode 100644 crates/rrt-cli/src/app/runtime_inspect/smp.rs create mode 100644 crates/rrt-cli/src/app/runtime_scan/aligned_band.rs create mode 100644 crates/rrt-cli/src/app/runtime_scan/candidate_table.rs create mode 100644 crates/rrt-cli/src/app/runtime_scan/common.rs create mode 100644 crates/rrt-cli/src/app/runtime_scan/mod.rs create mode 100644 crates/rrt-cli/src/app/runtime_scan/post_special.rs create mode 100644 crates/rrt-cli/src/app/runtime_scan/recipe_book.rs create mode 100644 crates/rrt-cli/src/app/runtime_scan/special_conditions.rs create mode 100644 crates/rrt-cli/src/app/tests/compare.rs create mode 100644 crates/rrt-cli/src/app/tests/mod.rs create mode 100644 crates/rrt-cli/src/app/tests/state/diff.rs create mode 100644 crates/rrt-cli/src/app/tests/state/document_io.rs create mode 100644 crates/rrt-cli/src/app/tests/state/fixture_summary.rs create mode 100644 crates/rrt-cli/src/app/tests/state/mod.rs create mode 100644 crates/rrt-cli/src/app/tests/state/save_slice_overlay.rs create mode 100644 crates/rrt-cli/src/app/tests/state/snapshot_io.rs create mode 100644 crates/rrt-cli/src/app/tests/support/json.rs create mode 100644 crates/rrt-cli/src/app/tests/support/mod.rs create mode 100644 crates/rrt-cli/src/app/tests/support/temp_files.rs create mode 100644 crates/rrt-cli/src/app/validate.rs delete mode 100644 crates/rrt-fixtures/src/schema.rs create mode 100644 crates/rrt-fixtures/src/schema/document.rs create mode 100644 crates/rrt-fixtures/src/schema/mod.rs create mode 100644 crates/rrt-fixtures/src/schema/state_fragment.rs create mode 100644 crates/rrt-fixtures/src/schema/summary.rs create mode 100644 crates/rrt-fixtures/src/schema/summary_compare/collections.rs create mode 100644 crates/rrt-fixtures/src/schema/summary_compare/mod.rs create mode 100644 crates/rrt-fixtures/src/schema/summary_compare/packed_events.rs create mode 100644 crates/rrt-fixtures/src/schema/summary_compare/selected_company.rs create mode 100644 crates/rrt-fixtures/src/schema/summary_compare/world.rs create mode 100644 crates/rrt-fixtures/src/schema/tests.rs create mode 100644 crates/rrt-fixtures/src/schema/validate.rs create mode 100644 crates/rrt-fixtures/src/summary.rs create mode 100644 crates/rrt-fixtures/src/validation.rs create mode 100644 crates/rrt-hook/src/capture.rs create mode 100644 crates/rrt-hook/src/windows/auto_load.rs create mode 100644 crates/rrt-hook/src/windows/capture.rs create mode 100644 crates/rrt-hook/src/windows/constants.rs create mode 100644 crates/rrt-hook/src/windows/detours.rs create mode 100644 crates/rrt-hook/src/windows/ffi.rs create mode 100644 crates/rrt-hook/src/windows/install.rs create mode 100644 crates/rrt-hook/src/windows/logging.rs create mode 100644 crates/rrt-hook/src/windows/memory.rs create mode 100644 crates/rrt-hook/src/windows/mod.rs create mode 100644 crates/rrt-hook/src/windows/state.rs delete mode 100644 crates/rrt-runtime/src/building.rs create mode 100644 crates/rrt-runtime/src/derived/company.rs create mode 100644 crates/rrt-runtime/src/derived/finance/annual_policy.rs create mode 100644 crates/rrt-runtime/src/derived/finance/bond_policy.rs create mode 100644 crates/rrt-runtime/src/derived/finance/credit_rating.rs create mode 100644 crates/rrt-runtime/src/derived/finance/distress.rs create mode 100644 crates/rrt-runtime/src/derived/finance/dividend_policy.rs create mode 100644 crates/rrt-runtime/src/derived/finance/mod.rs create mode 100644 crates/rrt-runtime/src/derived/finance/stock_actions.rs create mode 100644 crates/rrt-runtime/src/derived/mod.rs create mode 100644 crates/rrt-runtime/src/derived/tests/bond_metrics.rs create mode 100644 crates/rrt-runtime/src/derived/tests/company.rs create mode 100644 crates/rrt-runtime/src/derived/tests/finance.rs create mode 100644 crates/rrt-runtime/src/derived/tests/issue_state.rs create mode 100644 crates/rrt-runtime/src/derived/tests/mod.rs create mode 100644 crates/rrt-runtime/src/derived/tests/selected_year.rs create mode 100644 crates/rrt-runtime/src/derived/tests/share_price.rs create mode 100644 crates/rrt-runtime/src/derived/world/bond_metrics.rs create mode 100644 crates/rrt-runtime/src/derived/world/calendar.rs create mode 100644 crates/rrt-runtime/src/derived/world/issue_state.rs create mode 100644 crates/rrt-runtime/src/derived/world/mod.rs create mode 100644 crates/rrt-runtime/src/derived/world/selected_year.rs create mode 100644 crates/rrt-runtime/src/derived/world/share_price.rs create mode 100644 crates/rrt-runtime/src/derived/world/stat_readers.rs create mode 100644 crates/rrt-runtime/src/documents/blockers.rs create mode 100644 crates/rrt-runtime/src/documents/io.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/condition_targets/blockers.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/condition_targets/conditions.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/condition_targets/context_targets.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/condition_targets/effects.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/condition_targets/mod.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/condition_targets/predicates.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/condition_targets/territory.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/contextual_effects.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/decode/actions.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/decode/conditions.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/decode/effects.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/decode/mod.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/decode/record.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/decode/targets.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/import_outcome/conditions.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/import_outcome/descriptors.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/import_outcome/mod.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/import_outcome/outcome.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/import_outcome/targets.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/import_outcome/variants.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/mod.rs create mode 100644 crates/rrt-runtime/src/documents/lowering/summary.rs create mode 100644 crates/rrt-runtime/src/documents/mod.rs create mode 100644 crates/rrt-runtime/src/documents/model.rs create mode 100644 crates/rrt-runtime/src/documents/project/actors.rs create mode 100644 crates/rrt-runtime/src/documents/project/entrypoints.rs create mode 100644 crates/rrt-runtime/src/documents/project/events.rs create mode 100644 crates/rrt-runtime/src/documents/project/metadata.rs create mode 100644 crates/rrt-runtime/src/documents/project/mod.rs create mode 100644 crates/rrt-runtime/src/documents/project/near_city.rs create mode 100644 crates/rrt-runtime/src/documents/project/profile_world/availability.rs create mode 100644 crates/rrt-runtime/src/documents/project/profile_world/catalogs.rs create mode 100644 crates/rrt-runtime/src/documents/project/profile_world/mod.rs create mode 100644 crates/rrt-runtime/src/documents/project/profile_world/overrides.rs create mode 100644 crates/rrt-runtime/src/documents/project/profile_world/save_profile.rs create mode 100644 crates/rrt-runtime/src/documents/project/profile_world/world_restore.rs create mode 100644 crates/rrt-runtime/src/documents/project/projection.rs create mode 100644 crates/rrt-runtime/src/documents/project/state_build.rs create mode 100644 crates/rrt-runtime/src/documents/tests/mod.rs create mode 100644 crates/rrt-runtime/src/documents/tests/overlay_import.rs create mode 100644 crates/rrt-runtime/src/documents/tests/packed_events.rs create mode 100644 crates/rrt-runtime/src/documents/tests/profile_world.rs create mode 100644 crates/rrt-runtime/src/documents/tests/projection.rs create mode 100644 crates/rrt-runtime/src/documents/tests/roundtrip.rs create mode 100644 crates/rrt-runtime/src/documents/tests/support.rs create mode 100644 crates/rrt-runtime/src/documents/tests/validation.rs delete mode 100644 crates/rrt-runtime/src/economy.rs create mode 100644 crates/rrt-runtime/src/engine/advance.rs create mode 100644 crates/rrt-runtime/src/engine/command.rs create mode 100644 crates/rrt-runtime/src/engine/conditions/catalogs.rs create mode 100644 crates/rrt-runtime/src/engine/conditions/context.rs create mode 100644 crates/rrt-runtime/src/engine/conditions/entities.rs create mode 100644 crates/rrt-runtime/src/engine/conditions/entrypoints.rs create mode 100644 crates/rrt-runtime/src/engine/conditions/mod.rs create mode 100644 crates/rrt-runtime/src/engine/conditions/territories.rs create mode 100644 crates/rrt-runtime/src/engine/conditions/world_state.rs create mode 100644 crates/rrt-runtime/src/engine/effects/catalogs.rs create mode 100644 crates/rrt-runtime/src/engine/effects/company.rs create mode 100644 crates/rrt-runtime/src/engine/effects/entrypoints.rs create mode 100644 crates/rrt-runtime/src/engine/effects/event_graph.rs create mode 100644 crates/rrt-runtime/src/engine/effects/mod.rs create mode 100644 crates/rrt-runtime/src/engine/effects/model.rs create mode 100644 crates/rrt-runtime/src/engine/effects/players.rs create mode 100644 crates/rrt-runtime/src/engine/effects/territories.rs create mode 100644 crates/rrt-runtime/src/engine/effects/trains.rs create mode 100644 crates/rrt-runtime/src/engine/effects/world.rs create mode 100644 crates/rrt-runtime/src/engine/metrics.rs create mode 100644 crates/rrt-runtime/src/engine/mod.rs create mode 100644 crates/rrt-runtime/src/engine/mutations.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/bankruptcy.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/bond_issue.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/constants.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/dividend.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/entrypoints.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/mod.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/news.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/stock_issue.rs create mode 100644 crates/rrt-runtime/src/engine/service/annual_finance/stock_repurchase.rs create mode 100644 crates/rrt-runtime/src/engine/service/bonds.rs create mode 100644 crates/rrt-runtime/src/engine/service/company_fields.rs create mode 100644 crates/rrt-runtime/src/engine/service/company_lifecycle.rs create mode 100644 crates/rrt-runtime/src/engine/service/mod.rs create mode 100644 crates/rrt-runtime/src/engine/service/near_city_acquisition.rs create mode 100644 crates/rrt-runtime/src/engine/service/periodic.rs create mode 100644 crates/rrt-runtime/src/engine/service/trigger_dispatch.rs create mode 100644 crates/rrt-runtime/src/engine/targets/chairmen.rs create mode 100644 crates/rrt-runtime/src/engine/targets/company.rs create mode 100644 crates/rrt-runtime/src/engine/targets/mod.rs create mode 100644 crates/rrt-runtime/src/engine/targets/players.rs create mode 100644 crates/rrt-runtime/src/engine/targets/shared.rs create mode 100644 crates/rrt-runtime/src/engine/targets/territories.rs create mode 100644 crates/rrt-runtime/src/engine/tests/advance.rs create mode 100644 crates/rrt-runtime/src/engine/tests/annual_finance.rs create mode 100644 crates/rrt-runtime/src/engine/tests/effects.rs create mode 100644 crates/rrt-runtime/src/engine/tests/event_graph.rs create mode 100644 crates/rrt-runtime/src/engine/tests/mod.rs create mode 100644 crates/rrt-runtime/src/engine/tests/periodic.rs create mode 100644 crates/rrt-runtime/src/engine/tests/support.rs create mode 100644 crates/rrt-runtime/src/engine/tests/targets.rs create mode 100644 crates/rrt-runtime/src/event/conditions.rs create mode 100644 crates/rrt-runtime/src/event/effects.rs create mode 100644 crates/rrt-runtime/src/event/metrics.rs create mode 100644 crates/rrt-runtime/src/event/mod.rs create mode 100644 crates/rrt-runtime/src/event/news.rs create mode 100644 crates/rrt-runtime/src/event/packed.rs create mode 100644 crates/rrt-runtime/src/event/records.rs create mode 100644 crates/rrt-runtime/src/event/targets.rs delete mode 100644 crates/rrt-runtime/src/import.rs create mode 100644 crates/rrt-runtime/src/inspect.rs create mode 100644 crates/rrt-runtime/src/inspect/building/bindings.rs create mode 100644 crates/rrt-runtime/src/inspect/building/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/building/probe.rs create mode 100644 crates/rrt-runtime/src/inspect/building/recovered_tables.rs create mode 100644 crates/rrt-runtime/src/inspect/building/scan.rs create mode 100644 crates/rrt-runtime/src/inspect/building/types.rs rename crates/rrt-runtime/src/{campaign_exe.rs => inspect/campaign.rs} (100%) create mode 100644 crates/rrt-runtime/src/inspect/cargo/bindings.rs create mode 100644 crates/rrt-runtime/src/inspect/cargo/cargo_skin.rs create mode 100644 crates/rrt-runtime/src/inspect/cargo/cargo_types.rs create mode 100644 crates/rrt-runtime/src/inspect/cargo/economy.rs create mode 100644 crates/rrt-runtime/src/inspect/cargo/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/cargo/registry.rs create mode 100644 crates/rrt-runtime/src/inspect/cargo/selector.rs create mode 100644 crates/rrt-runtime/src/inspect/cargo/types.rs rename crates/rrt-runtime/src/{ => inspect}/pk4.rs (100%) create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/anchor.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/ascii.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/entrypoints.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/post_span.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/preamble.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/report.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/trailer.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/bundle/variants.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/conditions/cargo.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/conditions/ids.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/conditions/kinds.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/conditions/lookup.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/conditions/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/conditions/table.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/descriptors.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/fixups.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/offsets/bundle.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/offsets/events.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/offsets/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/offsets/regions.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/offsets/special_conditions.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/offsets/world.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/special_conditions.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/catalog/tags.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common/ascii.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common/headers.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common/names.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common/read.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common/recipe.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common/search.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/common/signatures.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/actors.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/catalogs.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/decode.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/row_summary.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/runtime_variables.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/targets.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/trains.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/actions/world.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/collection/control_lane.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/collection/live_ids.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/collection/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/collection/records.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/collection/summary.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/compat.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/conditions/labels.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/conditions/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/conditions/negative_sentinel.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/conditions/row_decode.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/descriptors/cargo.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/descriptors/classification.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/descriptors/locomotives.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/descriptors/metadata.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/descriptors/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/descriptors/runtime_keys.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/descriptors/special_conditions.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/descriptors/targets.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/nondirect.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/real_records.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/events/synthetic.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/map_title.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/map_title/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/profiles/classic.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/profiles/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/profiles/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/profiles/name_table.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/profiles/named_locomotive.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/profiles/rt3_105.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/regions/compare.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/regions/fixed_rows.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/regions/headers.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/regions/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/regions/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/regions/queued_notice.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/regions/triplets.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/save_load/assembly.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/save_load/entrypoints.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/save_load/loaded_regions.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/save_load/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/save_load/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/save_load/notes.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/save_load/summary.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/entries.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/entrypoints.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/gaps.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/hypotheses.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/input_fields.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/projection_status.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/tri_lane.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/notes/acquisition_frontier.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/notes/linked_transit_reference.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/notes/linked_transit_runtime.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/notes/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/notes/save_side_payload.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/peer_site.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/status/linked_transit_fields.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/status/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/status/near_city_fields.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/status/peer_site_fields.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/company/status/report.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/atlas.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/entrypoints.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/family_scan.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/attach_rebuild.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/constructor_families.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/live_entry_directory.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/payload_writer.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/save_side_corpus.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/follow_on.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/footer_flags.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/notes.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/infrastructure/status.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/model/company.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/model/infrastructure.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/model/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/model/region.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/model/shared.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/atlas.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/entries.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/entrypoints.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/later_restore.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/modal_dispatch.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/pending_bonus.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/periodic_producer.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/post_load_generation.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/tagged_load.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/notes.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/region/report.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/services/shared.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/special_conditions/aligned_band.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/special_conditions/locomotive_policy.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/special_conditions/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/special_conditions/model.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/special_conditions/post_scalar.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/special_conditions/recipe.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/special_conditions/table.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/alignment.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/embedded_names.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/evidence.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/live_entry_prelude.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/mode_family/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/candidate_rows.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/candidate_summary.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/candidate_pattern.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/compact_prefix.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/profile_span.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/parse.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/profile_span.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/fixed_policy.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/rows.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/short_profile.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/span_stats.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/prefix_patterns.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/scan.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/summary.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/entrypoints.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/model/alignment.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/core.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/patterns.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/payload.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/prelude.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/model/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/model/triplets.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/queued_notice.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/structures/triplets.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/events/collection.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/events/conditions_actions.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/events/descriptors.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/events/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/profiles.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/regions.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/save_load.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/services/infrastructure.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/services/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/services/periodic_company.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/services/region.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/services/shared.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/special_conditions.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/structures.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/support.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/tests/world.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/analysis.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/catalogs.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/chairman_records.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/company_records/analysis.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/company_records/bonds.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/company_records/capacity.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/company_records/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/company_records/offsets.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/company_records/roster.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/company_records/scan.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/company_records/stat_bands.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/derived_state.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/entrypoints.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/model/analysis.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/model/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/model/roster.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/model/selection.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/model/world_probes.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/notes.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/selection.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/world_probes/economic_tuning.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/world_probes/finance_neighborhood.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/world_probes/issue_37.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/world_probes/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/smp/world/world_probes/selection_context.rs create mode 100644 crates/rrt-runtime/src/inspect/win/entrypoints.rs create mode 100644 crates/rrt-runtime/src/inspect/win/header.rs create mode 100644 crates/rrt-runtime/src/inspect/win/mod.rs create mode 100644 crates/rrt-runtime/src/inspect/win/names.rs create mode 100644 crates/rrt-runtime/src/inspect/win/read.rs create mode 100644 crates/rrt-runtime/src/inspect/win/references.rs create mode 100644 crates/rrt-runtime/src/inspect/win/selectors.rs create mode 100644 crates/rrt-runtime/src/inspect/win/types.rs delete mode 100644 crates/rrt-runtime/src/runtime.rs delete mode 100644 crates/rrt-runtime/src/smp.rs create mode 100644 crates/rrt-runtime/src/state/core/mod.rs create mode 100644 crates/rrt-runtime/src/state/core/profile.rs create mode 100644 crates/rrt-runtime/src/state/core/refresh_market.rs create mode 100644 crates/rrt-runtime/src/state/core/refresh_world.rs create mode 100644 crates/rrt-runtime/src/state/core/runtime_state.rs create mode 100644 crates/rrt-runtime/src/state/core/service.rs create mode 100644 crates/rrt-runtime/src/state/core/validate/catalogs.rs create mode 100644 crates/rrt-runtime/src/state/core/validate/context.rs create mode 100644 crates/rrt-runtime/src/state/core/validate/entities.rs create mode 100644 crates/rrt-runtime/src/state/core/validate/events.rs create mode 100644 crates/rrt-runtime/src/state/core/validate/metadata.rs create mode 100644 crates/rrt-runtime/src/state/core/validate/mod.rs create mode 100644 crates/rrt-runtime/src/state/core/validate/runtime_variables.rs create mode 100644 crates/rrt-runtime/src/state/core/validate/service_state.rs create mode 100644 crates/rrt-runtime/src/state/core/world_restore.rs create mode 100644 crates/rrt-runtime/src/state/entities/actors.rs create mode 100644 crates/rrt-runtime/src/state/entities/companies.rs create mode 100644 crates/rrt-runtime/src/state/entities/company_finance.rs create mode 100644 crates/rrt-runtime/src/state/entities/company_market.rs create mode 100644 crates/rrt-runtime/src/state/entities/mod.rs create mode 100644 crates/rrt-runtime/src/state/entities/near_city.rs create mode 100644 crates/rrt-runtime/src/state/entities/territories.rs create mode 100644 crates/rrt-runtime/src/state/entities/trains.rs create mode 100644 crates/rrt-runtime/src/state/mod.rs delete mode 100644 crates/rrt-runtime/src/step.rs delete mode 100644 crates/rrt-runtime/src/summary.rs create mode 100644 crates/rrt-runtime/src/summary/builders/base.rs create mode 100644 crates/rrt-runtime/src/summary/builders/collections.rs create mode 100644 crates/rrt-runtime/src/summary/builders/mod.rs create mode 100644 crates/rrt-runtime/src/summary/builders/packed_events/apply.rs create mode 100644 crates/rrt-runtime/src/summary/builders/packed_events/blocked_outcomes.rs create mode 100644 crates/rrt-runtime/src/summary/builders/packed_events/collection.rs create mode 100644 crates/rrt-runtime/src/summary/builders/packed_events/mod.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/annual_finance.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/bond_policy.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/distress.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/dividend_policy.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/equity_actions.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/inputs.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/market.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/mod.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/model.rs create mode 100644 crates/rrt-runtime/src/summary/builders/selected_company/periodic.rs create mode 100644 crates/rrt-runtime/src/summary/builders/world/apply.rs create mode 100644 crates/rrt-runtime/src/summary/builders/world/economy.rs create mode 100644 crates/rrt-runtime/src/summary/builders/world/mod.rs create mode 100644 crates/rrt-runtime/src/summary/builders/world/model.rs create mode 100644 crates/rrt-runtime/src/summary/builders/world/overview.rs create mode 100644 crates/rrt-runtime/src/summary/builders/world/policy_state.rs create mode 100644 crates/rrt-runtime/src/summary/builders/world/restore_core.rs create mode 100644 crates/rrt-runtime/src/summary/builders/world/selected_year.rs create mode 100644 crates/rrt-runtime/src/summary/mod.rs create mode 100644 crates/rrt-runtime/src/summary/model.rs create mode 100644 crates/rrt-runtime/src/summary/tests.rs create mode 100644 crates/rrt-runtime/src/test_support/fs.rs create mode 100644 crates/rrt-runtime/src/test_support/mod.rs create mode 100644 crates/rrt-runtime/src/test_support/state.rs create mode 100644 crates/rrt-runtime/src/validation/documents.rs create mode 100644 crates/rrt-runtime/src/validation/mod.rs create mode 100644 crates/rrt-runtime/src/validation/runtime/conditions.rs create mode 100644 crates/rrt-runtime/src/validation/runtime/effects.rs create mode 100644 crates/rrt-runtime/src/validation/runtime/metrics.rs create mode 100644 crates/rrt-runtime/src/validation/runtime/mod.rs create mode 100644 crates/rrt-runtime/src/validation/runtime/targets.rs create mode 100644 crates/rrt-runtime/src/validation/runtime/templates.rs delete mode 100644 crates/rrt-runtime/src/win.rs create mode 100644 docs/history/progress-history.md create mode 100644 docs/rehost-queue/README.md create mode 100644 docs/rehost-queue/archive-2026-04-19.md rename docs/{atlas => subsystem-views}/README.md (51%) rename docs/{atlas => subsystem-views}/company-and-ledger.md (97%) rename docs/{atlas => subsystem-views}/editor-and-site-service.md (100%) rename docs/{atlas => subsystem-views}/multiplayer.md (100%) rename docs/{atlas => subsystem-views}/route-entry-and-trackers.md (99%) rename docs/{atlas => subsystem-views}/runtime-and-world-tools.md (100%) rename docs/{atlas => subsystem-views}/startup-shell-and-content.md (100%) rename fixtures/runtime/{minimal-world-raw-state.json => minimal-world-state-input.json} (80%) diff --git a/.gitignore b/.gitignore index 700a03e..0625d4f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,9 @@ /ghidra_projects/ /rt3_wineprefix/ /rt3_wineprefix2/ +/artifacts/tmp/ /tools/py/__pycache__/ /.codex -/\ %s *.gpr *.rep *.rzdb diff --git a/README.md b/README.md index bde4cdc..6958506 100644 --- a/README.md +++ b/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 ` 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 `, -`runtime inspect-region-service-trace `, and -`runtime inspect-infrastructure-asset-trace `, 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 ` +- `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. diff --git a/artifacts/captures/README.md b/artifacts/captures/README.md new file mode 100644 index 0000000..7e06e30 --- /dev/null +++ b/artifacts/captures/README.md @@ -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. diff --git a/artifacts/tmp/analysis/analysis-context-functions.csv b/artifacts/captures/analysis/analysis-context-functions.csv similarity index 100% rename from artifacts/tmp/analysis/analysis-context-functions.csv rename to artifacts/captures/analysis/analysis-context-functions.csv diff --git a/artifacts/tmp/analysis/analysis-context.md b/artifacts/captures/analysis/analysis-context.md similarity index 100% rename from artifacts/tmp/analysis/analysis-context.md rename to artifacts/captures/analysis/analysis-context.md diff --git a/RT2.LOG b/artifacts/captures/logs/RT2.LOG similarity index 100% rename from RT2.LOG rename to artifacts/captures/logs/RT2.LOG diff --git a/rt3_auto_load_winedbg.log b/artifacts/captures/logs/rt3_auto_load_winedbg.log similarity index 100% rename from rt3_auto_load_winedbg.log rename to artifacts/captures/logs/rt3_auto_load_winedbg.log diff --git a/rt3_manual_load_winedbg.log b/artifacts/captures/logs/rt3_manual_load_winedbg.log similarity index 100% rename from rt3_manual_load_winedbg.log rename to artifacts/captures/logs/rt3_manual_load_winedbg.log diff --git a/artifacts/tmp/BuildingDetail.win.bin b/artifacts/captures/win-bin/BuildingDetail.win.bin similarity index 100% rename from artifacts/tmp/BuildingDetail.win.bin rename to artifacts/captures/win-bin/BuildingDetail.win.bin diff --git a/artifacts/tmp/CompanyDetail.win.bin b/artifacts/captures/win-bin/CompanyDetail.win.bin similarity index 100% rename from artifacts/tmp/CompanyDetail.win.bin rename to artifacts/captures/win-bin/CompanyDetail.win.bin diff --git a/artifacts/exports/rt3-1.06/README.md b/artifacts/exports/rt3-1.06/README.md new file mode 100644 index 0000000..8e48bd9 --- /dev/null +++ b/artifacts/exports/rt3-1.06/README.md @@ -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. diff --git a/artifacts/exports/rt3-1.06/function-map.csv b/artifacts/exports/rt3-1.06/function-map.csv index 79c0d30..903eda8 100644 --- a/artifacts/exports/rt3-1.06/function-map.csv +++ b/artifacts/exports/rt3-1.06/function-map.csv @@ -1,20 +1,20 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,confidence,notes,verified_against 0x004010f0,521,city_compute_connection_bonus_candidate_weight,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Computes one city-side weight for the connection-bonus news and placement lanes. The helper rejects early when the city subtype field `[this+0x23e]` is nonzero, then builds a base float from the city-side scalar fields `[this+0x306]`, `[this+0x30a]`, and `[this+0x30e]`. When the optional stack company id is nonzero it also resolves the city's derived coordinates through `0x00455800` and `0x00455810`, probes the live world root at `0x0062c120` through `0x0044e270`, and rejects the city unless the selected company passes the follow-on world-side ownership or access check through `0x00424010`. The helper then scans the live placed-structure collection at `0x006cec20`, filters peers through `0x0041f6e0` plus the linked-instance class test `0x0047de00 -> 0x0040c990 == 1`, counts qualifying linked sites in the city, and detects whether one rival company already has an eligible linked site there. The final weight is scaled down by the inverse of `(qualifying_site_count + 1)` and then further damped when rival ownership is present, so the current grounded meaning is a city connection-bonus opportunity weight rather than a simple population or size score. Current grounded callers are the company-side news sweep at `0x00406050` and one neighboring setup-side branch at `0x00404d90`.","objdump + caller xrefs + callsite inspection + city-bonus correlation" -0x004013f0,177,world_run_company_start_or_city_connection_chooser_with_region_field_0x2d_temporarily_cleared_if_rule_0x4b07,map,cdecl,inferred,llvm-objdump + local disassembly + caller correlation,3,"Small world-side wrapper above the broader chooser `0x00404ce0`. When scenario slot `[0x006cec78+0x4b07]` is nonzero, the helper walks all `0x18` live region records in `0x006cfc9c`, snapshots dword `[region+0x2d]` into one stack buffer, and temporarily zeros that field across every live entry. It then runs `0x00404ce0(1, 0, 0)` and restores the original `[region+0x2d]` values before returning the chooser result. Current grounded caller is the startup-side branch at `0x00407592`, and current atlas correlation ties slot `0x4b07` to the editor rule `AI Ignore Territories At Startup`, so this is the safest current read for the temporary territory-field suppression wrapper rather than a generic region iterator.","llvm-objdump + local disassembly + caller correlation + region-collection correlation + scenario-rule correlation" -0x004014b0,1194,company_try_buy_unowned_industry_near_city_and_publish_news,simulation,thiscall,inferred,objdump + callsite inspection + RT3.lng strings,2,"Company-side acquisition and headline helper beneath the broader periodic company service pass at `0x004019e0`. The function requires at least two linked transit sites through `company_count_linked_transit_sites` `0x00426590`, rejects when scenario finance toggle `[0x006cec78+0x4abf]` is set, and then scans the live building or structure collection at `0x0062b26c` for the best current acquisition target. Current grounded candidate filters are: the record must not already have an owner in `[site+0x276]`, its linked candidate subtype gate through `0x0040d360` must identify subtype `4`, the company-specific price or affordability metric from `0x0040d540` must stay below the current company metric window, and the record must be at least three years old from `[site+0x3d5]`. Surviving candidates are scored through local profitability or demand helpers around `0x0040cac0`, `0x0042c820`, `0x00455f60`, and the current company support scalar, and the best surviving site id is then committed through `0x004269b0`. On success the helper localizes the acquired structure type through `localization_lookup_display_label_by_stem_or_fallback` `0x0051c920`, resolves the nearby city or region entry through `0x004220b0`, and emits RT3.lng `2880` `%1 has bought a %2 near %3` through the shell news helper at `0x004554e0`. This is now the strongest current match for the acquisition-side sibling beneath the broader company periodic pass, though some lower structure-side helper semantics remain open.","objdump + callsite inspection + RT3.lng strings + acquisition-news correlation + structure-scan correlation" +0x004013f0,177,world_try_publish_startup_company_or_city_connection_news_ignoring_territories,map,cdecl,inferred,llvm-objdump + local disassembly + caller correlation,3,"Small world-side wrapper above the broader chooser `0x00404ce0`. When scenario slot `[0x006cec78+0x4b07]` is nonzero, the helper walks all `0x18` live region records in `0x006cfc9c`, snapshots dword `[region+0x2d]` into one stack buffer, and temporarily zeros that field across every live entry. It then runs `0x00404ce0(1, 0, 0)` and restores the original `[region+0x2d]` values before returning the chooser result. Current grounded caller is the startup-side branch at `0x00407592`, and current atlas correlation ties slot `0x4b07` to the editor rule `AI Ignore Territories At Startup`, so this is now best read as the startup-news wrapper that temporarily ignores territory exclusion rather than as a generic region iterator.","llvm-objdump + local disassembly + caller correlation + region-collection correlation + scenario-rule correlation" +0x004014b0,1194,company_try_acquire_unowned_industry_near_city_and_publish_news,simulation,thiscall,inferred,objdump + callsite inspection + RT3.lng strings,2,"Company-side acquisition and headline helper beneath the broader periodic company service pass at `0x004019e0`. The function requires at least two linked transit sites through `company_count_linked_transit_sites` `0x00426590`, rejects when scenario finance toggle `[0x006cec78+0x4abf]` is set, and then scans the live building or structure collection at `0x0062b26c` for the best current acquisition target. Current grounded candidate filters are: the record must not already have an owner in `[site+0x276]`, its linked candidate subtype gate through `0x0040d360` must identify subtype `4`, the company-specific price or affordability metric from `0x0040d540` must stay below the current company metric window, and the record must be at least three years old from `[site+0x3d5]`. Surviving candidates are scored through local profitability or demand helpers around `0x0040cac0`, `0x0042c820`, `0x00455f60`, and the current company support scalar, and the best surviving site id is then committed through `0x004269b0`. On success the helper localizes the acquired structure type through `localization_lookup_display_label_by_stem_or_fallback` `0x0051c920`, resolves the nearby city or region entry through `0x004220b0`, and emits RT3.lng `2880` `%1 has bought a %2 near %3` through the shell news helper at `0x004554e0`. This is now the strongest current match for the acquisition-side sibling beneath the broader company periodic pass, though some lower structure-side helper semantics remain open.","objdump + callsite inspection + RT3.lng strings + acquisition-news correlation + structure-scan correlation" 0x00401860,221,company_query_cached_linked_transit_route_anchor_entry_id,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Returns one cached route-entry anchor id used by the linked-transit company cache family. The helper first validates the cached id at `[this+0x0d35]` against the live route-entry collection `0x006cfca8`, requiring the resolved record to carry this company id in word `[entry+0x22e]` and byte value `2` in `[entry+0x216]`; otherwise it clears the cache to `-1`. When the cache is empty but the three company-side count lanes `[this+0x7664]`, `[this+0x7668]`, and `[this+0x766c]` still sum positive, it scans the route-entry collection for the first record that satisfies those same owner and class predicates and then caches the record's linked anchor id from `[entry+0x202]`. Current grounded caller is `placed_structure_is_linked_transit_site_reachable_from_company_route_anchor` `0x004801a0`, where the returned route-entry id is used as the company-side anchor for the narrower linked-transit reachability gate.","objdump + caller xrefs + callsite inspection + route-entry-anchor correlation" 0x00401940,152,company_reset_linked_transit_caches_and_reseed_empty_train_routes,simulation,thiscall,inferred,objdump + callsite inspection + linked-transit correlation,2,"Small linked-transit reset helper beneath the broader company service family. The function clears the two company-side linked-transit cache timestamps at `[this+0x0d3e]` and `[this+0x0d3a]`, immediately re-enters `company_service_linked_transit_site_caches` `0x00409720`, and then walks the live train collection `0x006cfcbc` for company-owned trains. For owned trains in operating modes `0x0a` or `0x13` it removes every existing route-list entry through `train_route_list_remove_entry_and_compact` `0x004b3000`; when the route list is empty it then re-enters `train_try_append_linked_transit_autoroute_entry` `0x00409770`. Current grounded meaning is a local linked-transit cache reset plus empty-route reseed pass rather than a broader train-service sweep.","objdump + callsite inspection + linked-transit correlation + train-route-reset correlation" -0x004019e0,611,company_service_periodic_city_connection_finance_and_linked_transit_lanes,simulation,thiscall,inferred,objdump + callsite inspection + caller correlation + RT3.lng strings,2,"Broader periodic company-side service pass above the currently grounded city-connection, finance, and linked-transit lanes. The helper first rejects inactive or special-case companies through `[this+0x3f]` and `0x00425b90`, clears the transient company-side latches at `[this+0x0d17]`, `[this+0x0d18]`, and `[this+0x0d56]`, and temporarily mirrors one locomotive-derived byte from `[this+0x0d17]` into scenario field `[0x006cec78+0x4c74]` while the earlier route-building side of the body runs, restoring the original scenario value on exit. Current evidence now bounds those byte latches more narrowly: `[this+0x0d17]` is this transient route-search preference override, currently seeded only when `company_select_preferred_available_locomotive_id` `0x004078a0` resolves one locomotive whose engine-type dword `[record+0x10]` equals `2`; wider engine-type evidence now makes that best-read as the electric lane, since the linked approval helper around `0x0041d550` dispatches the same `0/1/2` field across three scenario opinion slots while the local language family `706..709` and help text `3848` bound the player-facing triplet as `Steam`, `Diesel`, and `Electric`. The route-search side is tighter now too: this mirrored byte is not just reusing a display preference slot abstractly, it feeds the same initial path-sweep branch in `route_entry_collection_run_initial_candidate_path_sweep` `0x0049bd40` that explicit route-policy byte `4` uses, selecting the larger `1.8` quality multiplier instead of `1.4` before the later acceptance checks. `[this+0x0d18]` is the city-connection announcement-side latch reused by `company_evaluate_and_publish_city_connection_bonus_news` `0x00406050`; and `[this+0x0d56]` is the linked-transit train-service latch later set by the train-add, train-upgrade, and route-builder-side cache-refresh family around `0x00409830`, `0x00409300`, and `0x0040457e -> 0x004093d0`. The ordering matters too: this owner clears those latches up front, runs the city-connection and linked-transit branches first, and only later enters `company_evaluate_annual_finance_policy_and_publish_news` `0x00401c50`, so the finance helper is reading same-cycle side-channel state rather than stale long-lived flags. It then gates and schedules several narrower service families: the city-connection announcement side through `simulation_try_select_and_publish_company_start_or_city_connection_news` `0x00404ce0` and `company_evaluate_and_publish_city_connection_bonus_news` `0x00406050`; the acquisition-side sibling through `company_try_buy_unowned_industry_near_city_and_publish_news` `0x004014b0`; the linked-transit train side through `company_balance_linked_transit_train_roster` `0x00409950`; the broader annual finance and governance helper through `company_evaluate_annual_finance_policy_and_publish_news` `0x00401c50`; and the linked-transit cache refresh tail through either `company_rebuild_linked_transit_site_peer_cache` `0x004093d0` or `company_rebuild_linked_transit_autoroute_site_score_cache` `0x00407bd0` depending on current scenario mode byte `[0x006cec78+0x0f]`. This name stays intentionally conservative: it is the broader periodic owner above those lanes, not a fully split policy map yet.","objdump + callsite inspection + caller correlation + RT3.lng strings + linked-transit correlation + city-connection correlation + acquisition correlation + latch correlation + sequencing correlation + temporary-route-preference correlation + locomotive-choice correlation + engine-type correlation + route-search-threshold correlation" +0x004019e0,611,company_service_periodic_city_connection_finance_and_linked_transit,simulation,thiscall,inferred,objdump + callsite inspection + caller correlation + RT3.lng strings,2,"Broader periodic company-side service pass above the currently grounded city-connection, finance, and linked-transit lanes. The helper first rejects inactive or special-case companies through `[this+0x3f]` and `0x00425b90`, clears the transient company-side latches at `[this+0x0d17]`, `[this+0x0d18]`, and `[this+0x0d56]`, and temporarily mirrors one locomotive-derived byte from `[this+0x0d17]` into scenario field `[0x006cec78+0x4c74]` while the earlier route-building side of the body runs, restoring the original scenario value on exit. Current evidence now bounds those byte latches more narrowly: `[this+0x0d17]` is this transient route-search preference override, currently seeded only when `company_select_preferred_available_locomotive_id` `0x004078a0` resolves one locomotive whose engine-type dword `[record+0x10]` equals `2`; wider engine-type evidence now makes that best-read as the electric lane, since the linked approval helper around `0x0041d550` dispatches the same `0/1/2` field across three scenario opinion slots while the local language family `706..709` and help text `3848` bound the player-facing triplet as `Steam`, `Diesel`, and `Electric`. The route-search side is tighter now too: this mirrored byte is not just reusing a display preference slot abstractly, it feeds the same initial path-sweep branch in `route_entry_collection_run_initial_candidate_path_sweep` `0x0049bd40` that explicit route-policy byte `4` uses, selecting the larger `1.8` quality multiplier instead of `1.4` before the later acceptance checks. `[this+0x0d18]` is the city-connection announcement-side latch reused by `company_try_publish_city_connection_bonus_news` `0x00406050`; and `[this+0x0d56]` is the linked-transit train-service latch later set by the train-add, train-upgrade, and route-builder-side cache-refresh family around `0x00409830`, `0x00409300`, and `0x0040457e -> 0x004093d0`. The ordering matters too: this owner clears those latches up front, runs the city-connection and linked-transit branches first, and only later enters `company_apply_annual_finance_policy_and_publish_news` `0x00401c50`, so the finance helper is reading same-cycle side-channel state rather than stale long-lived flags. It then gates and schedules several narrower service families: the city-connection announcement side through `simulation_try_publish_startup_company_or_city_connection_news` `0x00404ce0` and `company_try_publish_city_connection_bonus_news` `0x00406050`; the acquisition-side sibling through `company_try_acquire_unowned_industry_near_city_and_publish_news` `0x004014b0`; the linked-transit train side through `company_balance_linked_transit_train_roster` `0x00409950`; the broader annual finance and governance helper through `company_apply_annual_finance_policy_and_publish_news` `0x00401c50`; and the linked-transit cache refresh tail through either `company_rebuild_linked_transit_site_peer_cache` `0x004093d0` or `company_rebuild_linked_transit_autoroute_site_score_cache` `0x00407bd0` depending on current scenario mode byte `[0x006cec78+0x0f]`. This name stays intentionally conservative: it is the broader periodic owner above those lanes, not a fully split policy map yet.","objdump + callsite inspection + caller correlation + RT3.lng strings + linked-transit correlation + city-connection correlation + acquisition correlation + latch correlation + sequencing correlation + temporary-route-preference correlation + locomotive-choice correlation + engine-type correlation + route-search-threshold correlation" 0x004078a0,815,company_select_preferred_available_locomotive_id,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Selects one preferred locomotive id for the current company from the live locomotive collection at `0x006ada84`. The helper iterates the live locomotive records, applies company-and-chairman availability gating through the linked approval family around `0x0041d550`, accumulates one weighted preference score from locomotive fields `[record+0x20]`, `[+0x28]`, `[+0x18]`, `[+0x1c]`, `[+0x0c]`, and the linked class or era record at `[record+0x72]`, and keeps the strongest surviving locomotive id, falling back to `locomotive_collection_select_best_era_matched_non_electric_fallback_id` `0x00461cd0` when no scored candidate survives. Current grounded callers are the periodic company service pass `0x004019e0`, the temporary route-side mode chooser around `0x00402d5f`, the linked-transit train-upgrade news helper `0x00409300`, and the linked-transit train-add helper `0x00409830`. Current evidence now also bounds one route-policy side effect above it: `0x004019e0` only arms its temporary `[company+0x0d17] -> [0x006cec78+0x4c74]` override when the chosen locomotive record carries engine-type value `2` in `[record+0x10]`, which now best aligns with the electric lane rather than an unnamed class slot.","objdump + caller xrefs + callsite inspection + locomotive-collection correlation + preferred-choice correlation + engine-type correlation" -0x00401c50,3016,company_evaluate_annual_finance_policy_and_publish_news,simulation,thiscall,inferred,objdump + callsite inspection + RT3.lng strings,2,"Large annual company finance-policy helper beneath the broader periodic service pass at `0x004019e0`. The earliest creditor-pressure or bankruptcy lane is now bounded more tightly: it requires scenario mode `0x0c`, the bankruptcy toggle `[0x006cec78+0x4a8f]` to be clear, at least `13` years since the last bankruptcy stamp at `[this+0x163]`, and at least `4` years since founding year `[this+0x157]`. It then scans the last three years of derived finance lanes through `company_read_year_or_control_transfer_metric_value` `0x0042a5d0`, accumulating the net-profits lane `0x2b`, counting one three-year failure condition from the revenue aggregate `0x2c`, selecting one negative cash-and-debt stress ladder `-600000 / -1100000 / -1600000 / -2000000` from the current slot-`0x2c` band split at roughly `120000 / 230000 / 340000`, requiring the current support-adjusted share-price scalar from `company_compute_public_support_adjusted_share_price_scalar` `0x00424fd0` to be at least `15` or `20` depending on whether all three years failed, checking the current fuel-cost lane in slot `0x09` against `0.08` times that ladder, and requiring both `edi >= 2` plus the three-year slot-`0x2b` accumulator to clear one final `-60000` threshold before it commits bankruptcy through `company_declare_bankruptcy_and_halve_bond_debt` `0x00425a90` and formats RT3.lng `2881` `%1 has declared bankruptcy!`. The helper also has one later deep-distress fallback bankruptcy lane on the no-action fallthrough before the build-`1.03+` stock-issue branch: with the same bankruptcy toggle clear, current cash below `-300000`, at least `3` years since founding, no recent automatic finance action already chosen, the first three recent net-profits lanes each at or below `-20000`, and at least `5` years since the last bankruptcy stamp, it commits the same bankruptcy path and news family again. The later debt-capital restructuring family mutates the live company through `company_repay_bond_slot_and_compact_debt_table` `0x00423d70`, `company_issue_bond_and_record_terms` `0x004275c0`, `company_repurchase_public_shares_and_reduce_capital` `0x004273c0`, and `company_issue_public_shares_and_raise_capital` `0x00427450`, then formats the RT3.lng `2882..2886` headlines `%1 has refinanced %2 of debt.`, `%1 has refinanced %2 of debt and borrowed %3 on top of that.`, `%1 has refinanced %2 and paid off a further %3 of debt.`, `%1 has paid off %2 of its debt.`, and `%1 has borrowed %2 in debt.` The message-side tail is tighter now too: it accumulates total retired principal in one counter and total newly issued principal in another, then chooses the debt headline family by comparing those two totals directly: equal -> `2882` refinance only, issued greater -> `2883` refinance plus extra borrowing, retired greater -> `2884` refinance plus additional payoff, issued zero -> `2885` debt payoff only, and retired zero with issued positive -> `2886` straight new borrowing. The middle annual bond lane is tighter now too: when the bond toggle `[+0x4a8b]` is clear, it first simulates full bond repayment through repeated `company_repay_bond_slot_and_compact_debt_table` `0x00423d70`, then uses the resulting cash-side window with the fixed `-250000` and `-30000` thresholds plus the broader linked-transit train-service latch `[this+0x0d56]` to decide whether to stage one or more `500000` principal, `30`-year bond issues through `company_issue_bond_and_record_terms` `0x004275c0`. The repurchase lane is distinct from the later share-issue path: when the city-connection announcement-side latch `[this+0x0d18]` is set, editor growth setting `2` does not suppress it, and the stock toggle `[+0x4a87]` is clear, it starts from one `1000`-share batch and one default factor `1.0`, can replace that with a linked-chairman personality scalar `([table byte * 39] + 300) / 400`, scales that factor by `1.6` when growth setting `[0x006cec78+0x4c7c] == 1`, uses the resulting factor in one `800000` stock-value gate and one support-adjusted share-price scalar times factor times `1000` times `1.2` affordability gate, requires enough unassigned shares through `company_count_unassigned_shares_after_active_chairman_holdings` `0x004261b0`, and then commits repeated `1000`-share repurchases through `company_repurchase_public_shares_and_reduce_capital` `0x004273c0`; this is the current strongest threshold owner behind RT3.lng `2887`. The repurchase tail also now reads cleanly: it accumulates the total repurchased public-share count in a dedicated counter and publishes `2887` from that total after the debt headline branch. The sequencing above this helper now bounds those two latch reads more clearly: `0x004019e0` clears them first, then the city-connection and linked-transit branches may set them earlier in the same periodic pass, so the bond and repurchase lanes are currently best read as same-cycle reaction policy rather than long-term company-state policy. The later stock-issue lane is now tighter in exact order, not just broad shape: it only opens on build `1.03+`, only after the earlier bankruptcy, bond, and repurchase outcomes stay inactive, and with the bond and stock toggles `[+0x4a8b]` and `[+0x4a87]` clear, at least two bond slots live, and at least one year since founding. It derives one prospective equity tranche from roughly one-tenth of the current outstanding-share count, rounded down to `1000`-share lots with floor `2000`, trims that tranche downward until the pressured support-adjusted share-price scalar times tranche no longer exceeds the `55000` proceeds cap, recomputes the same share-price scalar under the negative tranche pressure term, clamps `Book Value Per Share` from stat-family `0x2329/0x1d` to a minimum `1.0`, and forms the normalized price-to-book ratio before any approval tests. The tested gates then run in a fixed order: share-price floor `>= 22`, pressured proceeds floor `>= 55000`, current cash from `0x2329/0x0d` against the highest-coupon live bond principal plus the fixed `5000` buffer, one later issue-cooldown or last-issue timestamp gate from `[this+0x16b..]`, and only then the piecewise coupon-versus-price-to-book ladder `0.07/1.3`, `0.08/1.2`, `0.09/1.1`, `0.10/0.95`, `0.11/0.8`, `0.12/0.62`, `0.13/0.5`, and `0.14/0.35`. On success it issues two same-sized public-share tranches through repeated `company_issue_public_shares_and_raise_capital` `0x00427450` calls and formats a separate equity-offering news family rooted at localized id `4053`, not the earlier `2882..2887` debt-or-buyback headlines. The dividend-side branch is now bounded too: it requires the dividend toggle `[0x006cec78+0x4a93]` to be clear, scenario mode `0x0c`, at least `1` year since `[this+0x0d2d]`, and at least `2` years since founding; it then converts a weighted `3/2/1` blend of the last three net-profits lanes `0x2b` into one tentative dividend-per-share target, supplements that target with current cash only on the tiny-unassigned-share branch below `1000`, and still folds in the map-editor building-density growth setting `[0x006cec78+0x4c7c]`. Current grounded postblend behavior is: growth setting `1` scales the existing dividend by `0.66`, growth setting `2` zeros it, computed deltas at or below `0.1` collapse to zero, larger deltas are quantized in tenths, and the final value is clamped against `company_compute_board_approved_dividend_rate_ceiling` `0x00426260`. The tail also refreshes `CompanyDetail.win` when the selected company matches `[0x006cfe4c]`. This now grounds the main finance verbs and first-layer threshold ordering under the annual policy pass, though some lower helper semantics still remain open.","objdump + callsite inspection + RT3.lng strings + finance-policy correlation + bankruptcy/debt-news correlation + repurchase-news correlation + equity-offering-news correlation + finance-mutator correlation + threshold correlation + latch correlation + sequencing correlation + stock-data-label correlation + highest-coupon-bond correlation + income-statement-row correlation + derived-report-metric correlation + valuation-vs-borrowing correlation + weighted-dividend-target correlation + deep-distress-bankruptcy correlation + debt-headline-tail correlation + repurchase-headline-tail correlation + stock-issue-ordering correlation" +0x00401c50,3016,company_apply_annual_finance_policy_and_publish_news,simulation,thiscall,inferred,objdump + callsite inspection + RT3.lng strings,2,"Large annual company finance-policy helper beneath the broader periodic service pass at `0x004019e0`. The earliest creditor-pressure or bankruptcy lane is now bounded more tightly: it requires scenario mode `0x0c`, the bankruptcy toggle `[0x006cec78+0x4a8f]` to be clear, at least `13` years since the last bankruptcy stamp at `[this+0x163]`, and at least `4` years since founding year `[this+0x157]`. It then scans the last three years of derived finance lanes through `company_read_year_or_control_transfer_metric_value` `0x0042a5d0`, accumulating the net-profits lane `0x2b`, counting one three-year failure condition from the revenue aggregate `0x2c`, selecting one negative cash-and-debt stress ladder `-600000 / -1100000 / -1600000 / -2000000` from the current slot-`0x2c` band split at roughly `120000 / 230000 / 340000`, requiring the current support-adjusted share-price scalar from `company_compute_public_support_adjusted_share_price_scalar` `0x00424fd0` to be at least `15` or `20` depending on whether all three years failed, checking the current fuel-cost lane in slot `0x09` against `0.08` times that ladder, and requiring both `edi >= 2` plus the three-year slot-`0x2b` accumulator to clear one final `-60000` threshold before it commits bankruptcy through `company_declare_bankruptcy_and_halve_bond_debt` `0x00425a90` and formats RT3.lng `2881` `%1 has declared bankruptcy!`. The helper also has one later deep-distress fallback bankruptcy lane on the no-action fallthrough before the build-`1.03+` stock-issue branch: with the same bankruptcy toggle clear, current cash below `-300000`, at least `3` years since founding, no recent automatic finance action already chosen, the first three recent net-profits lanes each at or below `-20000`, and at least `5` years since the last bankruptcy stamp, it commits the same bankruptcy path and news family again. The later debt-capital restructuring family mutates the live company through `company_repay_bond_slot_and_compact_debt_table` `0x00423d70`, `company_issue_bond_and_record_terms` `0x004275c0`, `company_repurchase_public_shares_and_reduce_capital` `0x004273c0`, and `company_issue_public_shares_and_raise_capital` `0x00427450`, then formats the RT3.lng `2882..2886` headlines `%1 has refinanced %2 of debt.`, `%1 has refinanced %2 of debt and borrowed %3 on top of that.`, `%1 has refinanced %2 and paid off a further %3 of debt.`, `%1 has paid off %2 of its debt.`, and `%1 has borrowed %2 in debt.` The message-side tail is tighter now too: it accumulates total retired principal in one counter and total newly issued principal in another, then chooses the debt headline family by comparing those two totals directly: equal -> `2882` refinance only, issued greater -> `2883` refinance plus extra borrowing, retired greater -> `2884` refinance plus additional payoff, issued zero -> `2885` debt payoff only, and retired zero with issued positive -> `2886` straight new borrowing. The middle annual bond lane is tighter now too: when the bond toggle `[+0x4a8b]` is clear, it first simulates full bond repayment through repeated `company_repay_bond_slot_and_compact_debt_table` `0x00423d70`, then uses the resulting cash-side window with the fixed `-250000` and `-30000` thresholds plus the broader linked-transit train-service latch `[this+0x0d56]` to decide whether to stage one or more `500000` principal, `30`-year bond issues through `company_issue_bond_and_record_terms` `0x004275c0`. The repurchase lane is distinct from the later share-issue path: when the city-connection announcement-side latch `[this+0x0d18]` is set, editor growth setting `2` does not suppress it, and the stock toggle `[+0x4a87]` is clear, it starts from one `1000`-share batch and one default factor `1.0`, can replace that with a linked-chairman personality scalar `([table byte * 39] + 300) / 400`, scales that factor by `1.6` when growth setting `[0x006cec78+0x4c7c] == 1`, uses the resulting factor in one `800000` stock-value gate and one support-adjusted share-price scalar times factor times `1000` times `1.2` affordability gate, requires enough unassigned shares through `company_count_unassigned_shares_after_active_chairman_holdings` `0x004261b0`, and then commits repeated `1000`-share repurchases through `company_repurchase_public_shares_and_reduce_capital` `0x004273c0`; this is the current strongest threshold owner behind RT3.lng `2887`. The repurchase tail also now reads cleanly: it accumulates the total repurchased public-share count in a dedicated counter and publishes `2887` from that total after the debt headline branch. The sequencing above this helper now bounds those two latch reads more clearly: `0x004019e0` clears them first, then the city-connection and linked-transit branches may set them earlier in the same periodic pass, so the bond and repurchase lanes are currently best read as same-cycle reaction policy rather than long-term company-state policy. The later stock-issue lane is now tighter in exact order, not just broad shape: it only opens on build `1.03+`, only after the earlier bankruptcy, bond, and repurchase outcomes stay inactive, and with the bond and stock toggles `[+0x4a8b]` and `[+0x4a87]` clear, at least two bond slots live, and at least one year since founding. It derives one prospective equity tranche from roughly one-tenth of the current outstanding-share count, rounded down to `1000`-share lots with floor `2000`, trims that tranche downward until the pressured support-adjusted share-price scalar times tranche no longer exceeds the `55000` proceeds cap, recomputes the same share-price scalar under the negative tranche pressure term, clamps `Book Value Per Share` from stat-family `0x2329/0x1d` to a minimum `1.0`, and forms the normalized price-to-book ratio before any approval tests. The tested gates then run in a fixed order: share-price floor `>= 22`, pressured proceeds floor `>= 55000`, current cash from `0x2329/0x0d` against the highest-coupon live bond principal plus the fixed `5000` buffer, one later issue-cooldown or last-issue timestamp gate from `[this+0x16b..]`, and only then the piecewise coupon-versus-price-to-book ladder `0.07/1.3`, `0.08/1.2`, `0.09/1.1`, `0.10/0.95`, `0.11/0.8`, `0.12/0.62`, `0.13/0.5`, and `0.14/0.35`. On success it issues two same-sized public-share tranches through repeated `company_issue_public_shares_and_raise_capital` `0x00427450` calls and formats a separate equity-offering news family rooted at localized id `4053`, not the earlier `2882..2887` debt-or-buyback headlines. The dividend-side branch is now bounded too: it requires the dividend toggle `[0x006cec78+0x4a93]` to be clear, scenario mode `0x0c`, at least `1` year since `[this+0x0d2d]`, and at least `2` years since founding; it then converts a weighted `3/2/1` blend of the last three net-profits lanes `0x2b` into one tentative dividend-per-share target, supplements that target with current cash only on the tiny-unassigned-share branch below `1000`, and still folds in the map-editor building-density growth setting `[0x006cec78+0x4c7c]`. Current grounded postblend behavior is: growth setting `1` scales the existing dividend by `0.66`, growth setting `2` zeros it, computed deltas at or below `0.1` collapse to zero, larger deltas are quantized in tenths, and the final value is clamped against `company_compute_board_approved_dividend_rate_ceiling` `0x00426260`. The tail also refreshes `CompanyDetail.win` when the selected company matches `[0x006cfe4c]`. This now grounds the main finance verbs and first-layer threshold ordering under the annual policy pass, though some lower helper semantics still remain open.","objdump + callsite inspection + RT3.lng strings + finance-policy correlation + bankruptcy/debt-news correlation + repurchase-news correlation + equity-offering-news correlation + finance-mutator correlation + threshold correlation + latch correlation + sequencing correlation + stock-data-label correlation + highest-coupon-bond correlation + income-statement-row correlation + derived-report-metric correlation + valuation-vs-borrowing correlation + weighted-dividend-target correlation + deep-distress-bankruptcy correlation + debt-headline-tail correlation + repurchase-headline-tail correlation + stock-issue-ordering correlation" 0x00402c90,19,placed_structure_resolve_linked_candidate_record,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,2,"Tiny placed-structure-to-candidate resolver over the global candidate collection at `0x0062b268`. The helper reads one candidate id from `[this+0x173]`, re-enters the shared indexed-collection record resolver at `0x00518140`, and returns the resulting candidate record pointer. Current grounded caller is the BuildingDetail-side branch at `0x00506441`, where it is used immediately after resolving one placed-structure record from `0x0062b2fc`. This now looks like the direct placed-structure linked-candidate accessor rather than another anonymous local helper.","objdump + caller xrefs + callsite inspection + collection-resolver correlation" -0x00402cb0,3457,city_connection_try_build_route_with_optional_direct_site_placement,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Shared heavy route-builder and optional direct-placement helper beneath the city-connection route and news family. The function starts at a clean prologue at `0x00402cb0`, seeds one builder-state latch band at `[this+0xf5]`, `[this+0xf6]`, `[this+0xf8]`, `[this+0xfa]`, `[this+0xfc]`, and `[this+0x10a]`, and then splits into three grounded internal lanes. The first is an early route-entry search or synthesis lane through `route_entry_collection_try_build_path_between_optional_endpoint_entries` `0x004a01a0` over the global route-entry store `0x006cfca8`, which can seed the builder-state block and succeed without placing a new site. The second is a single-endpoint direct-placement lane around `0x00403d92..0x00403ef3`: it scans the live placed-structure collection `0x0062b2fc` for `Maintenance` and `ServiceTower` stems, projects one candidate placement through `0x00417840`, validates it through `0x004197e0`, and then commits through the first direct placement branch `0x00403ed5 -> placed_structure_collection_allocate_and_construct_entry` `0x004134d0` -> `placed_structure_finalize_creation_or_rebuild_local_runtime_state` `0x0040ef10`. The third is a later paired-endpoint fallback lane around `0x00403f41..0x00404489`: it seeds two endpoint candidates from the same `Maintenance` and `ServiceTower` stem scan, builds one temporary route-entry candidate list, iterates that list against span and year-scaled step terms, projects trial placements through `0x00417840`, and on success commits through the second direct placement branch `0x0040446b -> 0x004134d0 -> 0x0040ef10` before clearing a small exclusion window in the temporary list. Outside those lanes it also re-enters geometry, region, and route-store helpers around `0x004423a0`, `0x00482e00`, `0x004931e0`, `0x00494310`, and the global route-entry stores `0x006cfcb4` / `0x006cfca8`, and it can still unwind through route-state cleanup without committing new placed structures. Current grounded external callers are still entirely in the city-connection family: the compact region-entry wrapper `city_connection_bonus_try_compact_route_builder_from_region_entry` `0x00404640`, the peer-route candidate builder `city_connection_bonus_build_peer_route_candidate` `0x004046a0`, the direct region-entry pair wrapper `city_connection_try_build_route_between_region_entry_pair` `0x00404c60`, and the direct retry paths inside `simulation_try_select_and_publish_company_start_or_city_connection_news` `0x00404ce0`. This now bounds the old unresolved `0x00403xxx..0x00404631` placement chooser as one shared city-connection route or placement helper with a cleaner internal policy split, even though some lower helper semantics remain open.","objdump + caller xrefs + callsite inspection + placement-correlation + route-builder correlation + Maintenance/ServiceTower scan correlation + route-entry search correlation" +0x00402cb0,3457,city_connection_try_build_route_and_optionally_place_direct_site,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Shared heavy route-builder and optional direct-placement helper beneath the city-connection route and news family. The function starts at a clean prologue at `0x00402cb0`, seeds one builder-state latch band at `[this+0xf5]`, `[this+0xf6]`, `[this+0xf8]`, `[this+0xfa]`, `[this+0xfc]`, and `[this+0x10a]`, and then splits into three grounded internal lanes. The first is an early route-entry search or synthesis lane through `route_entry_collection_try_build_path_between_optional_endpoint_entries` `0x004a01a0` over the global route-entry store `0x006cfca8`, which can seed the builder-state block and succeed without placing a new site. The second is a single-endpoint direct-placement lane around `0x00403d92..0x00403ef3`: it scans the live placed-structure collection `0x0062b2fc` for `Maintenance` and `ServiceTower` stems, projects one candidate placement through `0x00417840`, validates it through `0x004197e0`, and then commits through the first direct placement branch `0x00403ed5 -> placed_structure_collection_allocate_and_construct_entry` `0x004134d0` -> `placed_structure_finalize_creation_or_rebuild_local_runtime_state` `0x0040ef10`. The third is a later paired-endpoint fallback lane around `0x00403f41..0x00404489`: it seeds two endpoint candidates from the same `Maintenance` and `ServiceTower` stem scan, builds one temporary route-entry candidate list, iterates that list against span and year-scaled step terms, projects trial placements through `0x00417840`, and on success commits through the second direct placement branch `0x0040446b -> 0x004134d0 -> 0x0040ef10` before clearing a small exclusion window in the temporary list. Outside those lanes it also re-enters geometry, region, and route-store helpers around `0x004423a0`, `0x00482e00`, `0x004931e0`, `0x00494310`, and the global route-entry stores `0x006cfcb4` / `0x006cfca8`, and it can still unwind through route-state cleanup without committing new placed structures. Current grounded external callers are still entirely in the city-connection family: the compact region-entry wrapper `city_connection_bonus_try_compact_route_builder_from_region_entry` `0x00404640`, the peer-route candidate builder `city_connection_bonus_build_peer_route_candidate` `0x004046a0`, the direct region-entry pair wrapper `city_connection_try_build_route_between_region_entry_pair` `0x00404c60`, and the direct retry paths inside `simulation_try_publish_startup_company_or_city_connection_news` `0x00404ce0`. This now bounds the old unresolved `0x00403xxx..0x00404631` placement chooser as one shared city-connection route or placement helper with a cleaner internal policy split, even though some lower helper semantics remain open.","objdump + caller xrefs + callsite inspection + placement-correlation + route-builder correlation + Maintenance/ServiceTower scan correlation + route-entry search correlation" 0x004046a0,1388,city_connection_bonus_build_peer_route_candidate,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Reusable candidate builder beneath the city-connection bonus news and status family. The helper starts from one region or city entry in collection `0x0062bae0`, rounds that entry's two normalized coordinate terms through `0x00455800/0x00455810 -> 0x005a10d0`, and then re-enters `city_connection_bonus_select_first_matching_peer_site` `0x00420280` with both selector flags forced on so it can recover one representative peer site. When a peer is found it resolves that site's derived coordinates through `0x0047df30/0x0047df50 -> 0x005a10d0`. If the caller supplies a live explicit route-anchor tuple, the helper first tries the heavy builder `0x00402cb0` directly with that tuple and, when no peer was found, falls back to the smaller wrapper `0x00404640`. If neither early path succeeds it builds one mixed local candidate list in 10-byte stack records from both global route stores `0x006cfcb4` and `0x006cfca8`: the first pass appends the two endpoint ids from each dense pair record, while the second appends the six-byte route-entry ids stepped by `+0x06`. It then marks each candidate's byte `+0x04` for route-anchor eligibility only when the site is class `2`, still has no competing runtime or route-owner state in the `0x020e..0x0244` band, and passes the `0x0048a1c0/0x0048a1a0/0x0048f290` route-anchor tests against the selected peer coordinates. The scoring tail skips ineligible or peer-owned entries, prefers the smallest span from `[site+0x206/+0x20a/+0x20e]` and the peer-coordinate rectangle, optionally upgrades the chosen route anchor through `0x0048e3c0`, and finally retries `0x00402cb0` with the best surviving candidate. Current grounded callers are the wider company-side city-connection bonus sweep at `0x00406050` and one neighboring branch at `0x00406b73`, which makes this the shared peer-route candidate builder above the city bonus peer-selector family rather than a direct UI formatter.","objdump + caller xrefs + callsite inspection + peer-selector correlation + route-builder correlation + candidate-list correlation + route-anchor-eligibility correlation" 0x00404640,82,city_connection_bonus_try_compact_route_builder_from_region_entry,map,thiscall,inferred,objdump + caller inspection + route-builder correlation,3,"Small route-builder helper beneath `city_connection_bonus_build_peer_route_candidate` `0x004046a0`. The helper resolves one caller-supplied region or city entry id through collection `0x0062bae0`, derives that entry's compact coordinate pair through `0x00401000`, reads one explicit route-anchor or peer-site id from `[this+0x00]`, and then re-enters the heavier route builder at `0x00402cb0` with that resolved coordinate pair plus default wildcard arguments `-1/0/-1` in the remaining route slots. Current grounded caller is the city-connection bonus candidate builder at `0x004047bf`, where this helper is used as the smaller fallback path when the earlier explicit-anchor route attempt does not apply. This now looks like the compact region-entry wrapper around the shared route builder rather than a generic coordinate helper.","objdump + caller inspection + route-builder correlation + city-bonus fallback correlation" -0x00404c60,124,city_connection_try_build_route_between_region_entry_pair,map,fastcall,inferred,objdump + caller inspection + route-builder correlation,3,"Compact fastcall route-builder above the shared route store at `0x006cfca8`. The helper resolves the two caller-supplied region or city entry ids through collection `0x0062bae0`, derives both endpoint coordinate pairs through `0x00401000`, and then re-enters the heavier route builder at `0x00402cb0` with those two endpoints plus the caller's remaining stack-side policy tuple. Current grounded callers are the pair-selection sweeps inside `simulation_try_select_and_publish_company_start_or_city_connection_news` `0x00404ce0`, where it is used both for the early dense score matrix and the later selected-pair retry. This now looks like the direct region-entry pair wrapper around the shared route builder rather than another anonymous internal callsite.","objdump + caller inspection + route-builder correlation + pair-selection correlation" -0x00404ce0,3124,simulation_try_select_and_publish_company_start_or_city_connection_news,simulation,fastcall,inferred,objdump + caller inspection + RT3.lng strings,3,"Broad fastcall city-pair chooser and news publisher above `city_connection_try_build_route_between_region_entry_pair` `0x00404c60`. When the stack company-id argument is zero the helper sweeps up to `0xa0` region-or-city entries from `0x0062bae0`; when it is nonzero it first validates that company through collection `0x0062be10`, stat family `0x2329` mode `0x0d` via `0x0042a5d0`, and the territory-access gate `0x00424010`. Eligible city entries are filtered through `city_connection_bonus_exists_matching_peer_site` `0x00420030`, weighted through `city_compute_connection_bonus_candidate_weight` `0x004010f0`, damped by map-size terms, territory access, and current region flags, and stored into temporary score bands. The helper then builds one dense pair matrix, repeatedly re-enters `0x00404c60` to validate candidate city pairs, can update one company-side selected endpoint pair through `0x00426f20`, and finally publishes shell news through `0x004554e0`. Current grounded publication ids are `2889` `%1 has started a new company - the %2` and `2890` `%1 has connected %2 to %3.`. Current grounded callers are `0x00401455`, which temporarily clears region-state dwords `[0x006cfc9c+0x2d]` before a global pass, and `0x00401b36`, which re-enters the same chooser with the active company and linked chairman after the smaller company-side city-connection bonus lane falls through. This now looks like the broader company-start-or-city-connection headline chooser above the smaller city-connection bonus sweep at `0x00406050` rather than another anonymous route-builder.","objdump + caller inspection + RT3.lng strings + route-builder correlation + pair-selection correlation + publication-path correlation" -0x00405920,189,company_query_min_linked_site_distance_to_xy,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Queries the minimum distance from one company to an input X or Y point pair. The helper walks the live placed-structure collection at `0x006cec20`, keeps only sites whose linked company id from `placed_structure_query_linked_company_id` at `0x0047efe0` matches `[this+0x00]`, samples each surviving site's derived coordinates through `0x0047ded0` and `0x0047df00`, and then computes one scalar distance against the caller-supplied coordinate pair through `0x0051dbe0`. It returns the minimum observed distance across all linked sites, clamped to a floor of `1.0` when a closer or degenerate result would go lower. The current grounded caller is `company_evaluate_and_publish_city_connection_bonus_news` at `0x00406050`, where this helper provides the distance term later blended into each city candidate score.","objdump + caller xrefs + callsite inspection + distance-term correlation" -0x00406050,2966,company_evaluate_and_publish_city_connection_bonus_news,simulation,thiscall,inferred,objdump + caller inspection + RT3.lng strings,4,"Broader company-side city-connection bonus sweep above `city_connection_bonus_build_peer_route_candidate` at `0x004046a0`. The function first builds one announcement-value floor from several company-side helpers: stat family `0x2329` mode `0x0d` through `0x0042a5d0`, the wider company-state reader `0x00426ef0`, linked transit-site count `company_count_linked_transit_sites` `0x00426590`, the unlock gate `company_connection_bonus_lane_is_unlocked` `0x00427590`, and the bounded value ladder `company_compute_connection_bonus_value_ladder` `0x00425320`. It also enforces the version gate `0x00482e00 >= 0x67`, a transit-count cap `< 4`, and a minimum weighted city-value threshold above `50000`. Once those gates hold it walks the region-or-city collection at `0x0062bae0`, derives one per-city opportunity score from the city-side weight `city_compute_connection_bonus_candidate_weight` `0x004010f0`, the chairman-profile-scaled company metric, and current normalized city coordinates from `0x00455800/0x00455810`, and then keeps the best candidate ids in the persistent ten-entry band rooted at `[this+0x0d42]`. A second sweep validates those entries by re-entering `city_connection_bonus_build_peer_route_candidate`, then re-samples each surviving candidate's three route-anchor lanes `[site+0x206/+0x20a/+0x20e]`, counts reachable flagged peers, accumulates one integer bonus total through `0x0048b100`, refreshes transient route state through `0x0048f4c0`, and preserves `[site+0x240]` across that refresh on version `>= 0x67`. When at least one validated winner survives it publishes the winning score into company stat slot `0x0f` through `0x0042a040`, formats the city and company name lanes through `0x0051c000/0x0051b700/0x005193f0/0x00518de0`, and then emits localized news through `0x004554e0`. Current grounded publication ids remain `2888` `%1 has connected to %2.`, `2890` `%1 has connected %2 to %3.`, and `2921` `%1 has put a station in %2, but to win the %3 connection bonus, this station must be connected to ANOTHER city.`. When no winner survives it falls out without that publish and the surrounding caller can use the separate pending-bonus owner at `0x004358d0`. This is now the first grounded announcement owner above the city-connection bonus status formatter and peer-selector pair rather than just an anonymous caller around `0x004064c0`.","objdump + caller inspection + RT3.lng strings + peer-route candidate correlation + publication-path correlation + score-component correlation + validation-sweep correlation" +0x00404c60,124,city_connection_try_build_route_between_region_entry_pair,map,fastcall,inferred,objdump + caller inspection + route-builder correlation,3,"Compact fastcall route-builder above the shared route store at `0x006cfca8`. The helper resolves the two caller-supplied region or city entry ids through collection `0x0062bae0`, derives both endpoint coordinate pairs through `0x00401000`, and then re-enters the heavier route builder at `0x00402cb0` with those two endpoints plus the caller's remaining stack-side policy tuple. Current grounded callers are the pair-selection sweeps inside `simulation_try_publish_startup_company_or_city_connection_news` `0x00404ce0`, where it is used both for the early dense score matrix and the later selected-pair retry. This now looks like the direct region-entry pair wrapper around the shared route builder rather than another anonymous internal callsite.","objdump + caller inspection + route-builder correlation + pair-selection correlation" +0x00404ce0,3124,simulation_try_publish_startup_company_or_city_connection_news,simulation,fastcall,inferred,objdump + caller inspection + RT3.lng strings,3,"Broad fastcall city-pair chooser and news publisher above `city_connection_try_build_route_between_region_entry_pair` `0x00404c60`. When the stack company-id argument is zero the helper sweeps up to `0xa0` region-or-city entries from `0x0062bae0`; when it is nonzero it first validates that company through collection `0x0062be10`, stat family `0x2329` mode `0x0d` via `0x0042a5d0`, and the territory-access gate `0x00424010`. Eligible city entries are filtered through `city_connection_bonus_exists_matching_peer_site` `0x00420030`, weighted through `city_compute_connection_bonus_candidate_weight` `0x004010f0`, damped by map-size terms, territory access, and current region flags, and stored into temporary score bands. The helper then builds one dense pair matrix, repeatedly re-enters `0x00404c60` to validate candidate city pairs, can update one company-side selected endpoint pair through `0x00426f20`, and finally publishes shell news through `0x004554e0`. Current grounded publication ids are `2889` `%1 has started a new company - the %2` and `2890` `%1 has connected %2 to %3.`. Current grounded callers are `0x00401455`, which temporarily clears region-state dwords `[0x006cfc9c+0x2d]` before a global pass, and `0x00401b36`, which re-enters the same chooser with the active company and linked chairman after the smaller company-side city-connection bonus lane falls through. This is now best read as the broader try-publish startup-company-or-connection news chooser above the smaller city-connection bonus sweep at `0x00406050` rather than another anonymous route-builder.","objdump + caller inspection + RT3.lng strings + route-builder correlation + pair-selection correlation + publication-path correlation" +0x00405920,189,company_query_min_linked_site_distance_to_xy,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Queries the minimum distance from one company to an input X or Y point pair. The helper walks the live placed-structure collection at `0x006cec20`, keeps only sites whose linked company id from `placed_structure_query_linked_company_id` at `0x0047efe0` matches `[this+0x00]`, samples each surviving site's derived coordinates through `0x0047ded0` and `0x0047df00`, and then computes one scalar distance against the caller-supplied coordinate pair through `0x0051dbe0`. It returns the minimum observed distance across all linked sites, clamped to a floor of `1.0` when a closer or degenerate result would go lower. The current grounded caller is `company_try_publish_city_connection_bonus_news` at `0x00406050`, where this helper provides the distance term later blended into each city candidate score.","objdump + caller xrefs + callsite inspection + distance-term correlation" +0x00406050,2966,company_try_publish_city_connection_bonus_news,simulation,thiscall,inferred,objdump + caller inspection + RT3.lng strings,4,"Broader company-side city-connection bonus sweep above `city_connection_bonus_build_peer_route_candidate` at `0x004046a0`. The function first builds one announcement-value floor from several company-side helpers: stat family `0x2329` mode `0x0d` through `0x0042a5d0`, the wider company-state reader `0x00426ef0`, linked transit-site count `company_count_linked_transit_sites` `0x00426590`, the unlock gate `company_connection_bonus_lane_is_unlocked` `0x00427590`, and the bounded value ladder `company_compute_connection_bonus_value_ladder` `0x00425320`. It also enforces the version gate `0x00482e00 >= 0x67`, a transit-count cap `< 4`, and a minimum weighted city-value threshold above `50000`. Once those gates hold it walks the region-or-city collection at `0x0062bae0`, derives one per-city opportunity score from the city-side weight `city_compute_connection_bonus_candidate_weight` `0x004010f0`, the chairman-profile-scaled company metric, and current normalized city coordinates from `0x00455800/0x00455810`, and then keeps the best candidate ids in the persistent ten-entry band rooted at `[this+0x0d42]`. A second sweep validates those entries by re-entering `city_connection_bonus_build_peer_route_candidate`, then re-samples each surviving candidate's three route-anchor lanes `[site+0x206/+0x20a/+0x20e]`, counts reachable flagged peers, accumulates one integer bonus total through `0x0048b100`, refreshes transient route state through `0x0048f4c0`, and preserves `[site+0x240]` across that refresh on version `>= 0x67`. When at least one validated winner survives it publishes the winning score into company stat slot `0x0f` through `0x0042a040`, formats the city and company name lanes through `0x0051c000/0x0051b700/0x005193f0/0x00518de0`, and then emits localized news through `0x004554e0`. Current grounded publication ids remain `2888` `%1 has connected to %2.`, `2890` `%1 has connected %2 to %3.`, and `2921` `%1 has put a station in %2, but to win the %3 connection bonus, this station must be connected to ANOTHER city.`. When no winner survives it falls out without that publish and the surrounding caller can use the separate pending-bonus owner at `0x004358d0`. This is now the first grounded try-publish owner above the city-connection bonus status formatter and peer-selector pair rather than just an anonymous caller around `0x004064c0`.","objdump + caller inspection + RT3.lng strings + peer-route candidate correlation + publication-path correlation + score-component correlation + validation-sweep correlation" 0x00407bd0,1697,company_rebuild_linked_transit_autoroute_site_score_cache,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Slower per-company follow-on above the fast linked-transit peer cache. The helper stamps the current scenario tick into `[this+0x0d3a]`, then walks the live placed-structure collection and keeps only sites whose company-side cache cell at `[site+0x5bd][company_id]` is present and eligible. For each such site it refreshes the cache-cell tick at `+0x0a`, zeroes the three accumulated float lanes at `+0x0e`, `+0x12`, and `+0x16`, and then rebuilds those lanes from two phases. First it uses `placed_structure_count_candidates_with_local_service_metrics` `0x0047e330`, `0x0047e620`, and the issue-opinion helper `0x00437d20` to populate bounded candidate-local amount bands at stack `0x1a0..` and normalized scaling bands at `0x350..`. Then it re-enters the per-site peer buffer at cache `+0x06`, resolves each peer site's live service words through `0x0047de20`, and only keeps candidate lanes whose local amount still exceeds the peer baseline. The surviving excess amount accumulates raw into cache float `+0x12`, while the continuity share at peer float `+0x09` and route-step count at peer dword `+0x05` feed a second step-biased weighted accumulator in cache float `+0x0e` through the local `+0x28` denominator bias. A later grouped promotion pass chooses the strongest adjacent candidate bands and folds their weighted contribution into cache float `+0x16`, which is the final site-ranking lane consumed by the downstream site selector. That split now matters for the tracker compatibility question too: the pre-`1.03` versus `1.03+` pair-metric dispatcher at `0x004a65b0` can perturb peer step counts and continuity share, but on current evidence that only flows into the weighted `+0x0e` and promoted `+0x16` lanes, not the raw `+0x12` amount total. Current grounded callers are the company-side mode gate at `0x00401c2a`, the timed wrapper at `0x00409766`, and the fast-cache tail path at `0x004093cd`. This now reads as the slower autoroute-site score rebuild over the linked-transit peer cache family rather than an unnamed tail call.","objdump + caller xrefs + callsite inspection + linked-transit score-cache correlation + linked-transit consumer correlation + version-compatibility impact correlation" 0x00408280,255,company_select_best_owned_linked_transit_site_by_autoroute_score,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Selects one owned linked transit site from the company-side autoroute cache family. The helper walks the live placed-structure collection, keeps only sites whose company cache cell `[site+0x5bd][company_id]` is present and eligible, whose linked company id matches the current company, and which still pass the station-or-transit gate `0x0047fd50`. It then ranks the surviving sites by cache float `[cell+0x16]`, applying a small bonus when the placed-structure-side lanes `[site+0x5c1]` and `[site+0x5c5]` are both clear, and returns the winning site id or `-1` when no candidate survives. Current grounded caller is `company_build_linked_transit_autoroute_entry` `0x00408380`, where this helper provides the fallback start site when the caller does not already supply one.","objdump + caller xrefs + callsite inspection + linked-transit autoroute correlation" 0x00408380,3215,company_build_linked_transit_autoroute_entry,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Builds one `0x33`-byte train route-list entry from the company-side linked-transit autoroute caches. When the caller-supplied site id is absent or invalid, the helper first falls back to `company_select_best_owned_linked_transit_site_by_autoroute_score` `0x00408280`. It then walks the chosen site's peer buffer from `[site+0x5bd][company_id]+0x06`, recomputes grouped candidate-local deltas against the peer site's service words from `0x0047de20`, chooses the strongest surviving peer site, and finally formats one route-list record into the caller-owned output buffer: it clears the `0x33`-byte record, preserves the low nibble of flag byte `+0x28`, writes the chosen target site id into word `+0x29`, seeds the route-kind dword at `+0x24` with `0x384`, and fills the remaining route-anchor or metadata lanes through the auxiliary route-entry tracker family at `0x006cfcb4` when a linked train record is supplied. The downstream effect boundary is tighter now too: because this builder ranks peer-site candidates from the weighted peer-derived bands rather than the raw site totals, the pre-`1.03` versus `1.03+` tracker metric split is now best read as perturbing autoroute peer choice and seeded route-entry selection here, not the later company-wide train-pressure target at `0x00408f70`. Current grounded callers are `train_try_seed_route_list_from_company_linked_transit_sites` `0x00409770` and two neighboring stack-built retry branches in the same family. This now looks like the shared route-entry builder above the linked-transit autoroute cache rather than another raw site query.","objdump + caller xrefs + callsite inspection + route-list-entry correlation + linked-transit autoroute correlation + version-compatibility impact correlation" @@ -26,8 +26,8 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x00409830,274,company_try_add_linked_transit_train_and_publish_news,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection + RT3.lng strings,3,"Local add-train branch beneath the linked-transit company train-roster balancer. The helper first respects scenario gate `[0x006cec78+0x4aa3]`, clears and seeds one staged route-entry buffer through `0x004b2ba0`, services the owning company's linked-transit caches, asks `company_build_linked_transit_autoroute_entry` `0x00408380` for two route-list entries, inserts those entries through `train_route_list_insert_staged_entry_at_index` `0x004b3160`, chooses one preferred locomotive id through `0x004078a0`, and then hands the staged route plus locomotive choice into the train-construction helper at `0x004b2140`. On success it emits RT3.lng id `2896` `%1 has added a new train (%2) at %3` through `0x004554e0` and sets the byte latch at `[company+0x0d56]`. The weighted linked-transit cache lanes stay indirect here too: this add-train path inherits ranked site and peer choice only through the two `0x00408380` builder calls, while the separate roster-pressure decision above it still comes from raw cache `+0x12` through `0x00408f70`. Current grounded callers are the local add branches inside `company_balance_linked_transit_train_roster` `0x00409950`, while multiplayer callers package opcode `0x75` instead.","objdump + caller xrefs + callsite inspection + RT3.lng strings + linked-transit train-add correlation + weighted-consumer-boundary correlation" 0x00409950,923,company_balance_linked_transit_train_roster,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Balances one company's linked-transit train roster against the linked-site cache family. The helper first requires at least one linked transit site through `company_count_linked_transit_sites` `0x00426590`, then computes one aggregate train-pressure target through `company_compute_owned_linked_transit_site_score_total` `0x00408f70` and counts currently owned trains through `company_count_owned_trains` `0x004264c0`. It walks the live train collection `0x006cfcbc`, keeps only trains owned by the current company, derives one year-dependent age scalar from current scenario year `[0x006cec78+0x0d]`, and applies that scaled age against two thresholds. Very old trains above the upper band are either removed outright through the train collection vtable when the roster already exceeds target, or upgraded in place through `company_publish_train_upgrade_news` `0x00409300` when the target still needs capacity. Mid-age trains above the lower band can trigger one narrower upgrade branch when the roster is still below target. After pruning, it computes the remaining deficit between the aggregate target and the owned-train count, then repeatedly either packages multiplayer opcode `0x75` through `0x00469d30` or locally re-enters `company_try_add_linked_transit_train_and_publish_news` `0x00409830` until that deficit collapses, with one later add-train retry branch when the residual target is still high enough. Current grounded callers are the company-side service sweep at `0x00401b9d` and a neighboring company wrapper at `0x004097b8`, so this now looks like the live train-balance owner above the linked-transit autoroute and news helpers rather than an unnamed local maintenance pass.","objdump + caller xrefs + callsite inspection + linked-transit train-balance correlation + linked-transit consumer correlation" 0x004264c0,96,company_count_owned_trains,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Counts the live trains owned by the current company. The helper walks the train collection at `0x006cfcbc`, compares each record's owning company byte at `[train+0x51]` against `[this+0x00]`, and returns the number of matches. Current grounded callers are the linked-transit train-roster balancer at `0x00409950` and the `LoadScreen.win` company train-list page at `0x004e7670`, where the count gates the no-trains fallback before the detailed roster rows are built.","objdump + caller xrefs + callsite inspection + company-owned train count correlation" -0x00426590,135,company_count_linked_transit_sites,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Counts the live linked placed structures for this company that also pass the station-or-transit gate. The helper walks the placed-structure collection at `0x006cec20`, keeps only peers whose linked company id from `placed_structure_query_linked_company_id` at `0x0047efe0` matches `[this+0x00]`, requires the linked-instance class byte through `0x0047de00 -> 0x0040c990 == 1`, and optionally enforces the narrower station-or-transit predicate through `0x0047fd50` when the caller passes a nonzero stack flag. The current grounded caller is `company_evaluate_and_publish_city_connection_bonus_news` at `0x00406050`, where the helper contributes the current linked transit-site count used in the bonus-value ladder and later building-density caps.","objdump + caller xrefs + callsite inspection + connection-bonus count correlation" -0x00427590,47,company_credit_rating_score_meets_threshold_5,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Small boolean gate over the bounded company credit-rating score at `0x00425320`. The helper rejects immediately when scenario-state flag `0x006cec78+0x4a8b` is set or when the company-side count or age-like field `[this+0x5b]` has reached `0x14`; otherwise it computes the zero-argument credit-rating score and returns true only when the resulting integer value is at least `5`. Current grounded callers are `company_evaluate_and_publish_city_connection_bonus_news` at `0x00406050` and the neighboring branch around `0x004064a9`, so this is currently safest as a small reusable threshold gate over the same debt-market or company-strength score rather than a dedicated shell-only bond predicate.","objdump + caller xrefs + callsite inspection + credit-rating correlation + threshold-gate correlation" +0x00426590,135,company_count_linked_transit_sites,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Counts the live linked placed structures for this company that also pass the station-or-transit gate. The helper walks the placed-structure collection at `0x006cec20`, keeps only peers whose linked company id from `placed_structure_query_linked_company_id` at `0x0047efe0` matches `[this+0x00]`, requires the linked-instance class byte through `0x0047de00 -> 0x0040c990 == 1`, and optionally enforces the narrower station-or-transit predicate through `0x0047fd50` when the caller passes a nonzero stack flag. The current grounded caller is `company_try_publish_city_connection_bonus_news` at `0x00406050`, where the helper contributes the current linked transit-site count used in the bonus-value ladder and later building-density caps.","objdump + caller xrefs + callsite inspection + connection-bonus count correlation" +0x00427590,47,company_credit_rating_score_meets_threshold_5,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Small boolean gate over the bounded company credit-rating score at `0x00425320`. The helper rejects immediately when scenario-state flag `0x006cec78+0x4a8b` is set or when the company-side count or age-like field `[this+0x5b]` has reached `0x14`; otherwise it computes the zero-argument credit-rating score and returns true only when the resulting integer value is at least `5`. Current grounded callers are `company_try_publish_city_connection_bonus_news` at `0x00406050` and the neighboring branch around `0x004064a9`, so this is currently safest as a small reusable threshold gate over the same debt-market or company-strength score rather than a dedicated shell-only bond predicate.","objdump + caller xrefs + callsite inspection + credit-rating correlation + threshold-gate correlation" 0x00409e80,192,world_set_selected_year_and_refresh_calendar_presentation_state,simulation,thiscall,inferred,objdump + caller inspection + local disassembly,3,"Small world-side year setter beneath the periodic step family and the later world-entry staged-profile rehydrate band. The helper stores the caller-supplied selected absolute calendar counter at `[this+0x15]`, resolves the paired mixed-radix calendar-point tuple through `0x0051d460` into `[this+0x0d/+0x11]`, recomputes several derived year-fraction or presentation scalars rooted at `[this+0x0bfa]`, refreshes the live shell controller at `0x006d4024` through `0x0051f070`, `0x00527c70`, and `0x00527c20`, and updates the controller's cached doubled-year field at `[controller+0x2c]`. When the unpacked year field at `[this+0x0d]` actually changes it also re-enters `world_refresh_selected_year_bucket_scalar_band` `0x00433bd0`. Current grounded callers are the smaller periodic-step wrapper at `0x0040a150`, the larger simulation-advance family through `0x0040a590/0x0040ab50`, and the late staged-profile rehydrate lane inside `world_entry_transition_and_runtime_bringup` `0x00443a50`, so this is the safest current read for the shared selected-year/calendar refresh helper rather than a one-off world-entry setter.","objdump + caller inspection + local disassembly + periodic-step correlation + world-entry rehydrate correlation + year-bucket-companion correlation + calendar-counter model correlation" 0x0040a590,892,simulation_service_periodic_boundary_work,simulation,cdecl,inferred,objdump + analysis-context,3,"Periodic simulation-maintenance dispatcher inside the world-step family. It switches on the local calendar or phase byte at [this+0x0f] routes one heavy mode through the larger recurring service branch at 0x0040a160 falls back to the simpler one-step advance at 0x00409e80 for other modes and runs several global manager sweeps across 0x0062be10 0x006ceb9c 0x006cfcbc 0x006cec20 and 0x0062bae0 before handing control back to the enclosing stepper. The pre-recipe scalar seam is tighter here now too: after the year and threshold refresh strip, the helper derives one bounded bucket from the selected-year gap `[world+0x0d] - [world+0x05]`, reads the corresponding float from the five-slot table `[world+0x0be6/+0x0bea/+0x0bee/+0x0bf2/+0x0bf6]`, applies the later bucket-specific scale cuts, adds one fixed bias, and multiplies the running scalar `[world+0x0bde]` by that result before it refreshes the live company and profile collections. Its scenario-runtime handoff is now explicit too: the grounded calls at `0x0040a276`, `0x0040a55f`, `0x0040a6cb`, and `0x0040a7a3` re-enter `scenario_event_collection_service_runtime_effect_records_for_trigger_kind` `0x00432f40` with trigger kinds `1`, `0`, `3`, and `2` respectively. The route-style lane is tighter here now too: the neighboring recurring branch at `0x0040a91f` re-enters `placed_structure_collection_refresh_quarter_subset_route_style_state` `0x00413580`, then later drives the same runtime-effect collection loop with trigger kinds `5` and `4` at `0x0040a930` and `0x0040a9ac`. One conditional branch also re-enters shell_map_file_world_bundle_coordinator at 0x00445de0 which keeps the save-or-package family connected to the live simulation cadence.",objdump + analysis-context + caller xrefs + runtime-effect trigger-kind correlation + pre-recipe-scalar-table correlation 0x0040ab50,339,simulation_advance_to_target_calendar_point,simulation,cdecl,inferred,objdump + analysis-context,3,"Advances the active world state toward one caller-selected mixed-radix calendar target while guarded by the recursion counter at `0x0062b240`. The helper compares the current local calendar-point tuple fields `[this+0x0d]`, `[this+0x0f]`, `[this+0x11]`, and `[this+0x14]` against a target resolved through `0x0051d550` and `0x0051d5f0`, then either recurses in larger `0x168`-sized chunks or advances through the smaller periodic step family rooted at `0x00409e80` and `0x0040a9c0`. Current tuple evidence now bounds this as the same mixed-radix counter family used by `0x0051d3c0/0x0051d460`: a packed year-plus-subfield calendar point rather than a plain year or month-only target. Each successful step notifies the live world root at `0x0062c120` through `0x00450030`. Grounded callers include the frame-time accumulator at `0x00439140`, the larger fast-forward helper at `0x00437b20`, and one shell UI command path.","objdump + analysis-context + caller xrefs + calendar-counter model correlation" @@ -180,7 +180,7 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x004196c0,280,aux_candidate_collection_construct_stream_load_records_and_refresh_runtime_followons,map,thiscall,inferred,objdump + local disassembly + caller correlation + tagged-import correlation,4,"Load-side constructor and import owner for the auxiliary or source record pool rooted at `0x0062b2fc`. The helper builds two temporary tagged import contexts around the fixed stem `0x005c93fc` through the callback trio `0x005c8190/0x005c8194/0x005c819c`, then repeatedly opens the next imported payload and re-enters `0x00414490` to decode one fixed source record into the local stack image. When that decode succeeds it dispatches the collection's allocate-or-insert vtable slot `+0x04` with the decoded record body, posts shell progress through `0x004834e0`, and continues until the tagged source is exhausted. After the import loop it re-enters `0x00419230` for the broader runtime follow-on refresh. Current grounded caller is the world-entry load branch at `0x00438c70`, where the resulting object is stored into global `0x0062b2fc`, so this is the safest current read for the load-side constructor plus stream-import companion to `aux_candidate_collection_serialize_records_into_bundle_payload` `0x00416a70` rather than a generic constructor wrapper.","objdump + local disassembly + caller correlation + tagged-import correlation + decode-owner correlation" 0x0041a990,184,aux_candidate_collection_allocate_helper_bands_and_tail_into_stream_load_refresh,map,thiscall,inferred,objdump + local disassembly + caller correlation + helper-band correlation,3,"Lower helper beneath the `0x0062b2fc` constructor family. The function clears scratch global `0x0062b304`, allocates and zero-initializes three owned helper bands, stores them into `[this+0x88]`, `[this+0x90]`, and `[this+0x8c]`, seeds those bands through `0x53dbf0` and `0x53dcf0` with literal capacities `0x65`, `0x08`, and `0x74e`, and then tail-jumps directly into `aux_candidate_collection_construct_stream_load_records_and_refresh_runtime_followons` `0x004196c0`. Current grounded caller is the outer constructor `0x0041aa50`, so this is the safest current read for the helper-band allocator and import tail beneath the auxiliary or source record pool rather than a standalone collection owner.","objdump + local disassembly + caller correlation + helper-band correlation + import-tail correlation" 0x0041aa50,130,aux_candidate_collection_construct_seed_globals_and_helper_bands_then_import_records,map,thiscall,inferred,objdump + local disassembly + caller correlation + constructor correlation,3,"Constructor-side world-load owner for the auxiliary or source record pool rooted at `0x0062b2fc`. The helper runs the shared indexed-collection base initializer `0x00517ce0`, installs vtable `0x005c9718`, clears constructor-side globals `0x0062b2f0`, `0x0062b2f4`, and `0x0062b2f8`, zeroes the owned helper pointers `[this+0x88]`, `[this+0x8c]`, `[this+0x90]`, seeds `[this+0x98] = 1`, `[this+0x9c] = 0`, and the fixed float-like pair `[this+0xa0/+0xa4]`, resets shell-side dword `[0x006cec74+0x4fd]`, seeds the indexed-collection configuration through `0x00518570` with literal tuple `(1, 0x1a7, 0x0a, 5, 1, 0, 0)`, and then re-enters `aux_candidate_collection_allocate_helper_bands_and_tail_into_stream_load_refresh` `0x0041a990`. Current grounded caller is the world-entry load branch at `0x00438cc5`, which stores the finished object into global `0x0062b2fc`, so this is the current safest read for the real constructor above the `0x0062b2fc` load/import family rather than a generic collection allocator.","objdump + local disassembly + caller correlation + constructor correlation + world-load correlation" -0x004197e0,5232,placed_structure_validate_projected_candidate_placement,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Heavy placement validator beneath the city-connection chooser and several direct mutation paths. The helper resolves one anchor placed-structure id from the current collection at `0x0062b2fc`, optionally validates the caller-supplied company through `0x0062be10`, scenario-state and shell-mode gates around `0x004338c0` and `[0x006cec7c+0x82]`, the current world tile through `0x00414bd0` and `0x00534e10`, and territory access through `0x00424010`, and then walks a large footprint-validation pass over the linked candidate record from `0x0062b268`. That deeper pass uses the candidate footprint bytes `[candidate+0xb8]` and `[candidate+0xb9]`, multiple temporary occupancy banks on the stack, route or road-side probes through `0x00448af0`, `0x004499c0`, and `0x00413df0`, and subtype-specific follow-on branches keyed by `[candidate+0x32]` plus optional route-entry and company-side arguments. The strongest current subtype-specific branch is `[candidate+0x32] == 1`, where the helper re-enters `placed_structure_project_candidate_grid_extent_offset_by_rotation` `0x00417840`, checks route-entry ownership and company track-laying capacity through `0x004240a0`, tries one explicit track-attachment path through `0x00494cb0`, and then falls back to one steeper world-space sweep through `0x00448bd0`. In that branch the optional failure buffer now has a concrete station-attachment or upgrade-style family: `0x0b55` `2901` not enough room to upgrade the station, `0x0b56` `2902` ground not flat enough for the upgraded station, `0x0b57` `2903` not your track, `0x0b58` `2904` not enough available track laying capacity, `0x0b59` `2905` cannot connect to existing track but too close to lay new track, and `0x0b5a` `2906` ground too steep for this building, with older fallback strings `0x00be/0x00bf` still used on neighboring exits. The helper returns a placement-success boolean and is currently grounded as the shared go-or-no-go gate immediately before direct placement commits, without yet proving that every caller is station-only. Current grounded callers include both direct-placement lanes inside `city_connection_try_build_route_with_optional_direct_site_placement` `0x00402cb0`, the placed-structure local rebuild branch at `0x0040dedb`, later mutation or editor-side branches at `0x00422afa`, `0x0046ef6b`, `0x0047070f`, `0x00472bcc`, `0x00472cd4`, and two shell-side callers at `0x00507f57` and `0x005083cc`.","objdump + caller xrefs + callsite inspection + company-access correlation + footprint-validation correlation + RT3.lng failure-text correlation" +0x004197e0,5232,placed_structure_validate_projected_candidate_placement,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Heavy placement validator beneath the city-connection chooser and several direct mutation paths. The helper resolves one anchor placed-structure id from the current collection at `0x0062b2fc`, optionally validates the caller-supplied company through `0x0062be10`, scenario-state and shell-mode gates around `0x004338c0` and `[0x006cec7c+0x82]`, the current world tile through `0x00414bd0` and `0x00534e10`, and territory access through `0x00424010`, and then walks a large footprint-validation pass over the linked candidate record from `0x0062b268`. That deeper pass uses the candidate footprint bytes `[candidate+0xb8]` and `[candidate+0xb9]`, multiple temporary occupancy banks on the stack, route or road-side probes through `0x00448af0`, `0x004499c0`, and `0x00413df0`, and subtype-specific follow-on branches keyed by `[candidate+0x32]` plus optional route-entry and company-side arguments. The strongest current subtype-specific branch is `[candidate+0x32] == 1`, where the helper re-enters `placed_structure_project_candidate_grid_extent_offset_by_rotation` `0x00417840`, checks route-entry ownership and company track-laying capacity through `0x004240a0`, tries one explicit track-attachment path through `0x00494cb0`, and then falls back to one steeper world-space sweep through `0x00448bd0`. In that branch the optional failure buffer now has a concrete station-attachment or upgrade-style family: `0x0b55` `2901` not enough room to upgrade the station, `0x0b56` `2902` ground not flat enough for the upgraded station, `0x0b57` `2903` not your track, `0x0b58` `2904` not enough available track laying capacity, `0x0b59` `2905` cannot connect to existing track but too close to lay new track, and `0x0b5a` `2906` ground too steep for this building, with older fallback strings `0x00be/0x00bf` still used on neighboring exits. The helper returns a placement-success boolean and is currently grounded as the shared go-or-no-go gate immediately before direct placement commits, without yet proving that every caller is station-only. Current grounded callers include both direct-placement lanes inside `city_connection_try_build_route_and_optionally_place_direct_site` `0x00402cb0`, the placed-structure local rebuild branch at `0x0040dedb`, later mutation or editor-side branches at `0x00422afa`, `0x0046ef6b`, `0x0047070f`, `0x00472bcc`, `0x00472cd4`, and two shell-side callers at `0x00507f57` and `0x005083cc`.","objdump + caller xrefs + callsite inspection + company-access correlation + footprint-validation correlation + RT3.lng failure-text correlation" 0x00480210,448,placed_structure_construct_linked_site_record_from_anchor_and_coords,map,thiscall,inferred,objdump + caller inspection + constructor inspection,3,"Lower constructor beneath the linked-site allocator at `0x00481390`. The helper writes the new placed-structure id into `[this+0x00]`, stages one anchor or parent placed-structure id at `[this+0x04]`, clears the route-anchor field at `[this+0x08]`, the display-name buffer at `[this+0x46b]`, and several local list or scratch bands rooted at `[this+0x18]`, `[this+0x112]`, and `[this+0x5bd]`, then seeds local world-space state from the anchor site through `0x00455730`, `placed_structure_project_candidate_grid_extent_offset_by_rotation` `0x00417840`, and the grid helper at `0x0040cec0`. It quantizes the caller-supplied coordinate pair into `[this+0x4a8]` and `[this+0x4ac]`, initializes one grid-keyed owner lane through `0x0042bbb0`, and then chooses an initial route-entry anchor into `[this+0x08]` through `0x00417b40` when one compatible route entry already covers the projected point window. When that early anchor path does not hold, the helper falls back into the neighboring literal-policy-`1` route-entry synthesis family around `0x00493cf0`: current caller correlation says that byte is the direct linked-site endpoint-anchor creation or replacement lane, after which the helper rebinds `[this+0x08]` through `0x0048abc0` and updates the boolean marker at `[this+0x46a]`. Current direct caller is `placed_structure_collection_allocate_and_construct_linked_site_record` `0x00481390`, which makes this the clearest current lower constructor for the linked-site records later published through `[site+0x2a8]`.","objdump + caller inspection + constructor inspection + route-anchor correlation + linked-site correlation + linked-site policy-byte split correlation" 0x0048a1e0,336,runtime_object_attach_infrastructure_child_with_optional_first_child_triplet_clone,map,thiscall,inferred,objdump + caller xrefs + local disassembly + rdata correlation,2,"Shared `Infrastructure` child-attach helper over the broader `0x23a` runtime-object family. The function first checks the caller's child list at `[this+0x08]`. When that list holds more than one child, it samples the first child's two triplet scalar bands through `0x0052e880` and `0x0052e720`, destroys the caller-selected prior child through vtable slot `+0x18(0)`, allocates a fresh `0x23a` object, installs vtable `0x005cfd00`, seeds it through `0x00455b70` with literal stem `Infrastructure`, attaches it back to the owner through `0x005395d0`, and republishes the sampled triplets through `0x0052e8b0` and `0x00530720`. When the owner has at most one child, it still allocates and seeds the same `Infrastructure` object but attaches it through `0x0053a5d0` without the cloned-triplet republish. Current grounded callers are the repeated builder branches around `0x004a2cde..0x004a37d9`, so this is the safest current read for attaching one seeded `Infrastructure` child with an optional first-child triplet clone rather than a subtype-specific UI helper.","objdump + caller xrefs + local disassembly + rdata correlation + child-attach correlation" 0x0048abc0,11,linked_site_set_route_entry_anchor_id_field_0x222,map,thiscall,inferred,objdump + caller xrefs + local disassembly,2,"Tiny direct setter over linked-site dword `[this+0x222]`. The helper stores the caller-supplied route-entry id and returns. Current grounded callers are the linked-site constructor and refresh family at `0x00480210`, `0x00480bb0`, and nearby repair branches `0x00480463`, `0x00480968`, `0x00480ac1`, and `0x00480da1`, where this field is treated as the currently chosen route-entry anchor id. The safest current read is therefore a direct linked-site route-entry-anchor setter rather than a generic scalar write.","objdump + caller xrefs + local disassembly + route-anchor correlation" @@ -480,7 +480,7 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x005158f0,1120,shell_train_list_window_refresh_controls,shell,thiscall,inferred,objdump + strings correlation + control-flow inspection + caller correlation,4,"Refreshes the visible control state for the shell-side `TrainList.win` family rooted at `0x006d3b34`. The helper clears and rebuilds the two top list controls `0x32c9/0x32ca`, walks the full train collection `0x006cfcbc`, filters rows to the current company through `0x004337a0`, formats each surviving train name through `0x005193f0` and `0x00518de0`, and publishes mirrored entries into both list controls through `0x00540120`. It then installs the row renderers and callbacks on `0x32c9/0x32ca`, refreshes the shared selected-row latch `[this+0x78]`, updates the empty-state labels `0x32cf/0x32d0`, republishes the optional secondary selection from `0x00622b2c`, and when the world-side selected support-entry id at `[world+0x4cb6]` still resolves to one live train owned by the current company it reselects that same train in both list controls. Current grounded callers are the family constructor `0x00515f50`, the family message dispatcher `0x00515c60`, the sibling focus helper `0x00515820`, and the broader shell follow-on sweep `0x00436170`. This is therefore the real `TrainList.win` refresh owner rather than a generic company-list helper.","objdump + strings correlation + control-flow inspection + caller correlation + singleton correlation + selected-train-mirror correlation" 0x00515c60,752,shell_train_list_window_handle_message,shell,thiscall,inferred,objdump + strings correlation + control-flow inspection + caller correlation,4,"Primary message dispatcher for the shell-side `TrainList.win` family rooted at `0x006d3b34`. The handler processes shell messages `0`, `0xca`, `0xcb`, `0xce`, and the recurring refresh message `0x3e9`. Message `0` services the timed secondary-selection refresh through `0x0053f830` and updates `0x00622b2c`. Message `0xca` routes either the lower train-action row block `0x332c..0x36b0` through `0x004bf9d0` or direct top-list row clicks on controls `0x32c9/0x32ca`: those top-list clicks resolve the chosen train from `0x006cfcbc`, validate its linked route object through `0x004a77b0`, mirror the train-side support-entry id `[train+0x25]` into `[world+0x4cb6]`, republish the world focus through `0x00433900`, and transition the active detail panel through `0x004ddbd0(2, train_id)`. Message `0xcb` toggles the current top-side latch `0x006d3b30`, rewrites the paired list-action controls `0x32cb/0x32cc`, repopulates the lower action range `0x332c..0x36b0`, and clears the opposite top selection. Message `0xce` mirrors externally supplied top-list row payloads directly into `[world+0x4cb6]`. Message `0x3e9` simply re-enters `shell_train_list_window_refresh_controls` `0x005158f0`. This is the current grounded message owner for the full `TrainList.win` family.","objdump + strings correlation + control-flow inspection + caller correlation + detail-panel-transition correlation + selected-train-mirror correlation" 0x00515f50,181,shell_train_list_window_construct,shell,thiscall,inferred,objdump + strings correlation + control-wiring inspection + singleton correlation,4,"Constructs the shell-side `TrainList.win` singleton later published at `0x006d3b34`. The constructor binds the `TrainList.win` resource directly through `0x0053fa50` using the embedded string at `0x005d19dc`, seeds the local vtable at `0x005d19d0`, stores the live singleton pointer `0x006d3b34`, initializes the shared top-row selection latch at `[this+0x78] = -1`, wires the two top list controls `0x32c9/0x32ca`, the top-side toggle controls `0x32cb/0x32cc`, and the row-selection callbacks through `0x00540120`, and then tail-calls `shell_train_list_window_refresh_controls` `0x005158f0(-1)`. Current evidence is strong enough to treat this as the full `TrainList.win` constructor rather than another unnamed train-side shell family.","objdump + strings correlation + control-wiring inspection + singleton correlation + local disassembly" -0x004a01a0,789,route_entry_collection_try_build_path_between_optional_endpoint_entries,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Heavy route-entry collection method over the global route store at `0x006cfca8`. The helper accepts up to two optional endpoint-entry pointers or ids, two endpoint coordinate pairs, a route-policy or mode byte, and optional output slots for the chosen endpoint-entry ids. It first resolves any supplied route-entry ids through `0x00518140` and projects them back to compact coordinate pairs through `0x0048a170`, rejects early when either endpoint lands on an invalid world tile through `0x00449980`, and computes one span scalar through `0x004952f0`. When the leading endpoint entry is absent it can synthesize one fresh route entry from the supplied coordinates and policy arguments through `0x00493cf0`; after that it resolves the chosen entry, copies four `3*dword` route-shape or bounding blocks from that entry into the collection-owned builder-state area at `[this+0x139]..[this+0x167]` through `0x005394b0`, stores the chosen endpoint coordinates at `[this+0x11d]` and `[this+0x121]`, latches one active-builder flag at `[this+0x118]`, and stages one linked entry field from `[entry+0x202]` into `[this+0x125]`. On one newly synthesized-entry path and policy bytes `1/2` it also touches the auxiliary route-entry family at `0x006cfcb4` through `0x004a42b0`, `0x00494f00`, and `0x004a4340`. Current caller evidence now narrows one more policy case: both the later world-side caller at `0x00480cd0` and the linked-site refresh helper `placed_structure_refresh_linked_site_display_name_and_route_anchor` `0x00480bb0` enter this helper with both optional endpoint-entry ids unset and literal policy byte `2`, so the strongest current read for that byte is a full linked-site route-anchor rebuild between optional endpoint entries rather than the narrower direct endpoint-anchor creation or replacement lane carried by literal byte `1` in `0x00493cf0`. The helper then resets the collection's transient path-search state through `0x00495480`, re-enters the deeper route-search core at `0x0049d380`, optionally writes the chosen endpoint-entry ids back through the caller-supplied output pointers on success, and finally tears down the transient search state through `0x00495540` and `0x0049ad90`. Current grounded callers are the early route-search lane inside `city_connection_try_build_route_with_optional_direct_site_placement` `0x00402cb0`, two neighboring retry branches at `0x004030d0` and `0x00403330`, a paired startup-connection branch at `0x00403ac0`, that later world-side caller at `0x00480cd0`, and the linked-site refresh helper at `0x00480bb0`. This now looks like the shared route-entry search or synthesis owner above the deeper path-search core rather than a generic route-store mutator.","objdump + caller xrefs + callsite inspection + route-entry search correlation + builder-state correlation + partial-mode-byte correlation + linked-site refresh correlation + linked-site policy-byte split correlation" +0x004a01a0,789,route_entry_collection_try_build_path_between_optional_endpoint_entries,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Heavy route-entry collection method over the global route store at `0x006cfca8`. The helper accepts up to two optional endpoint-entry pointers or ids, two endpoint coordinate pairs, a route-policy or mode byte, and optional output slots for the chosen endpoint-entry ids. It first resolves any supplied route-entry ids through `0x00518140` and projects them back to compact coordinate pairs through `0x0048a170`, rejects early when either endpoint lands on an invalid world tile through `0x00449980`, and computes one span scalar through `0x004952f0`. When the leading endpoint entry is absent it can synthesize one fresh route entry from the supplied coordinates and policy arguments through `0x00493cf0`; after that it resolves the chosen entry, copies four `3*dword` route-shape or bounding blocks from that entry into the collection-owned builder-state area at `[this+0x139]..[this+0x167]` through `0x005394b0`, stores the chosen endpoint coordinates at `[this+0x11d]` and `[this+0x121]`, latches one active-builder flag at `[this+0x118]`, and stages one linked entry field from `[entry+0x202]` into `[this+0x125]`. On one newly synthesized-entry path and policy bytes `1/2` it also touches the auxiliary route-entry family at `0x006cfcb4` through `0x004a42b0`, `0x00494f00`, and `0x004a4340`. Current caller evidence now narrows one more policy case: both the later world-side caller at `0x00480cd0` and the linked-site refresh helper `placed_structure_refresh_linked_site_display_name_and_route_anchor` `0x00480bb0` enter this helper with both optional endpoint-entry ids unset and literal policy byte `2`, so the strongest current read for that byte is a full linked-site route-anchor rebuild between optional endpoint entries rather than the narrower direct endpoint-anchor creation or replacement lane carried by literal byte `1` in `0x00493cf0`. The helper then resets the collection's transient path-search state through `0x00495480`, re-enters the deeper route-search core at `0x0049d380`, optionally writes the chosen endpoint-entry ids back through the caller-supplied output pointers on success, and finally tears down the transient search state through `0x00495540` and `0x0049ad90`. Current grounded callers are the early route-search lane inside `city_connection_try_build_route_and_optionally_place_direct_site` `0x00402cb0`, two neighboring retry branches at `0x004030d0` and `0x00403330`, a paired startup-connection branch at `0x00403ac0`, that later world-side caller at `0x00480cd0`, and the linked-site refresh helper at `0x00480bb0`. This now looks like the shared route-entry search or synthesis owner above the deeper path-search core rather than a generic route-store mutator.","objdump + caller xrefs + callsite inspection + route-entry search correlation + builder-state correlation + partial-mode-byte correlation + linked-site refresh correlation + linked-site policy-byte split correlation" 0x004a3db0,272,route_entry_collection_service_recent_entry_state_promotions_and_followon_refreshes,map,thiscall,inferred,objdump + caller inspection + local disassembly,3,"Collection-wide post-change sweep over the global route-entry store at `0x006cfca8`. The helper first snapshots collection field `[this+0x90]` into `[this+0x94]`, then, when shell-state gate `[0x006cec74+0x94]` is set, walks every live route entry through `indexed_collection_slot_count` `0x00517cf0`, `indexed_collection_get_nth_live_entry_id` `0x00518380`, and `indexed_collection_resolve_live_entry_by_id` `0x00518140`. The current changed-entry lane is now bounded: for entries newer than collection threshold `[this+0x102]` via `[entry+0x240]` and still carrying state byte `[entry+0x216] == 1`, it promotes that state through `0x0048b830(entry, 2)` and then releases any queued side list at `[entry+0x244]` through `0x00489f40`. After that sweep it drains the collection-owned linked list at `[this+0x98]`, resets `[this+0x88] = -1`, and, when shell runtime `0x006d401c` is live while collection latch `[this+0xed]` is clear, posts shell status id `0xbb` through `0x00538e00`. When any entry was promoted it also re-enters `0x004358d0` on the active mode object at `0x006cec78`, triggers `scenario_event_collection_service_runtime_effect_records_for_trigger_kind` `0x00432f40` with kind `6`, and then tails into `placed_structure_route_link_collection_recompute_all_endpoint_pair_state` `0x004682c0` on `0x006ada90`. Current grounded callers include the city-connection route-builder tail at `0x00404564`, the linked-site route-anchor refresh tail at `0x00480d1c`, and TrackLay-side follow-ons at `0x0050d7ed` and `0x0050db24`, so this is now the safest current read for the route-entry collection's recent-entry state-promotion and follow-on refresh sweep rather than a generic collection walk.","objdump + caller inspection + local disassembly + route-entry collection correlation + state-promotion correlation + runtime-effect trigger-kind correlation + route-link followon correlation" 0x00489f80,11,route_entry_assign_aux_tracker_group_id,map,thiscall,inferred,objdump + caller xrefs + field-layout inspection,3,"Tiny route-entry helper that writes the caller-supplied auxiliary tracker id into route-entry field `+0x212`. Current grounded callers include the synthesis-side lane in `route_entry_collection_try_build_path_between_optional_endpoint_entries` `0x004a01a0`, the route-search core `0x0049d380`, the broader regrouping pass `0x004a45f0`, and neighboring tracker-update branches at `0x004996e0`, `0x004a4380`, `0x004a4ce0`, and `0x004a4ff0`. This now looks like the direct route-entry aux-tracker group-id assignment helper rather than a generic field store.","objdump + caller xrefs + field-layout inspection + tracker-family correlation" 0x00493cf0,456,route_entry_collection_create_endpoint_entry_from_coords_and_policy,map,thiscall,inferred,objdump + caller xrefs + callsite inspection,3,"Creates one fresh route-entry endpoint record from caller-supplied coordinates and a small policy tuple. The helper rejects when either coordinate pair falls outside the live world bounds in `0x0062c120`, optionally validates the supplied company id through `0x0062be10` and `company_query_available_track_laying_capacity_or_unlimited` `0x004240a0`, allocates a new route-entry-like record from the current collection through `0x00518900` with type `0x257`, applies one company-side setup branch through `company_adjust_available_track_laying_capacity_with_floor_zero` `0x00423ec0` when the company id is nonzero, and then commits the detailed endpoint payload through `0x00490ac0`. It can also re-enter `0x00491e60` when the created record or the owning collection keeps one follow-on latch set, and if the collection field `[this+0x88]` is still unset it seeds that field from the created record's word at `+0x240`. Current mode-byte evidence is still partial but tighter now. Literal policy byte `1` is the strongest current match for direct linked-site endpoint-anchor creation or replacement, because the linked-site constructor and its nearby repair branches at `0x00480463`, `0x00480a77`, and `0x00480b69` all pass that byte before rebinding one chosen anchor through `0x0048abc0`. The TrackLay-side callers at `0x0050df1e` and `0x0050eec6` are tighter now too: they derive the passed policy byte from the shared TrackLay mode `0x00622b0c` through `route_entry_collection_map_track_lay_mode_to_endpoint_policy_byte` `0x004955b0`, which currently gives the strongest read `policy 1 = single-track endpoint synthesis` and `policy 4 = double-track endpoint synthesis`. Bytes `1/2` are also the ones later reused by `0x004a01a0` to enable the auxiliary tracker lane, while the company-side charge split is no longer vague: when a company id is present, ordinary company-bound synthesis passes `-1` into `0x00423ec0`, while byte `4` instead passes `-2`, so the current strongest read is that policy byte `4` consumes a larger company-side available-track-laying-capacity unit rather than skipping the company setup branch outright. The older builder-state path at `0x0046f2d1` still passes a dynamic byte from `[esi+0x05]`. The function returns the newly created route-entry id or `-1` on failure. Current grounded callers include `route_entry_collection_try_build_path_between_optional_endpoint_entries` `0x004a01a0`, those neighboring route-building branches, and the TrackLay-side callers at `0x0050df1e` and `0x0050eec6`, so this now looks like the shared endpoint-entry synthesis helper rather than a generic collection allocator.","objdump + caller xrefs + callsite inspection + route-entry creation correlation + partial-mode-byte correlation + caller-pattern correlation + route-build-capacity correlation + linked-site policy-byte split correlation + TrackLay mode correlation" @@ -543,7 +543,7 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x00518680,625,indexed_collection_refresh_header_auxiliary_bands_and_live_entry_payloads_from_stream,simulation,thiscall,inferred,objdump + local disassembly + caller correlation + collection-layout inspection,4,"Shared indexed-collection load helper paired with serializer `0x00517d90`. The helper first invokes collection vtable slot `+0x00`, then reads the fixed scalar header dwords back into `[this+0x04/+0x08/+0x0c/+0x10]`, one temporary live-id bound later copied into `[this+0x14]`, current slot-count `[this+0x18]`, allocator root `[this+0x1c]`, the four auxiliary-band roots `[this+0x20..+0x2c]`, and their paired descriptor dwords `[this+0x48..+0x64]`. When the incoming live-id bound exceeds current `[this+0x14]` it re-enters `0x00517f90` to grow the payload and tombstone backing first. It then reloads the tombstone bitset at `[this+0x34]` and walks every positive id below `[this+0x14]`, zero-filling tombstoned rows while rehydrating live payloads either as direct fixed-stride records in `[this+0x30]` when `[this+0x04]` is nonzero or as indirect `u16`-length-plus-pointer payload rows when `[this+0x04]` is zero. The same loop also replays the nonzero auxiliary-band rows through `0x00518260` and refreshes the small live-id cache band `[this+0x68..+0x74]` back to `-0x10`. Current grounded callers are the tagged and packed-state load owners across support, company, train, route-entry, placed-structure, region, event, and city-database families, where this is the shared collection-header-and-live-entry refresh helper rather than one family-specific loader.","objdump + local disassembly + caller correlation + collection-layout inspection + direct-vs-indirect payload correlation + load-helper correlation" 0x0051c920,223,localization_lookup_display_label_by_stem_or_fallback,shell,thiscall,inferred,objdump + caller inspection + language-table correlation + static-table inspection,4,"Shared shell-side label helper that turns one ASCII stem string into one player-facing display label. The helper case-folds the first letter, scans the static `(stem, string-id)` table rooted at `0x006243c8`, compares candidate stems case-insensitively, and returns the localized text for the first matching string id through `0x005193f0` when that localized entry is nonempty. The current table correlation already grounds entries such as `Alcohol`, `Aluminum Mill`, `Automobiles`, `Bauxite`, and `Big Boy` against RT3.lng ids `3202..3220`. When no table entry matches, the helper falls back to localized id `3866` `Could not find a translation table entry for '%1'`. Current grounded callers include `shell_building_detail_refresh_subject_cargo_and_service_rows` `0x004ba3d0`, where it sits directly above candidate field `[record+0x04]`, and several neighboring list and detail renderers that need one display label from the same stem family.","objdump + caller inspection + language-table correlation + static-table inspection + RT3.lng correlation" 0x0051d390,37,calendar_pack_unpacked_components_to_absolute_counter,support,fastcall,inferred,objdump + local arithmetic correlation + restore-path correlation,3,"Tiny calendar helper that converts one unpacked component set into the same mixed-radix absolute counter family later stored at `[world+0x15]`. It combines the fastcall component pair in `ECX/EDX` plus three stack-supplied scalar subfields as one `12 x 28 x 3 x 60` progression and returns the resulting absolute counter. Current grounded load or restore-side caller correlation comes from the selected-year rehydrate path in `world_load_saved_runtime_state_bundle` `0x00446d40`, where the helper is used after the mode-sensitive year adjustment to rebuild the final absolute counter from the staged calendar components before `world_set_selected_year_and_refresh_calendar_presentation_state` `0x00409e80` republishes the derived tuple fields.","objdump + local arithmetic correlation + restore-path correlation + mixed-radix decode" -0x0051d3c0,44,calendar_point_pack_tuple_to_absolute_counter,support,fastcall,inferred,objdump + caller inspection + local arithmetic correlation,3,"Tiny packed-calendar helper that converts one local tuple rooted at the caller pointer in `ECX` into the same absolute calendar counter family later stored at `[world+0x15]`. It reads one year word plus four packed calendar subfields from `[tuple+0x00/+0x02/+0x04/+0x05/+0x06]`, combines them as one mixed-radix `12 x 28 x 3 x 60` progression, and returns the resulting scalar for direct subtraction against `[0x006cec78+0x15]`. That is now the strongest current read for the world-side calendar model: a packed year-plus-subfield point converted into one absolute counter rather than a plain year or month stamp. Current grounded finance caller is the stock-issue cooldown gate inside `company_evaluate_annual_finance_policy_and_publish_news` `0x00401c50`, where it converts the current issue tuple at `[company+0x16b/+0x16f]` before comparing that scalar against the active world counter.","objdump + caller inspection + local arithmetic correlation + finance-cooldown correlation + world-calendar-counter correlation + mixed-radix decode" +0x0051d3c0,44,calendar_point_pack_tuple_to_absolute_counter,support,fastcall,inferred,objdump + caller inspection + local arithmetic correlation,3,"Tiny packed-calendar helper that converts one local tuple rooted at the caller pointer in `ECX` into the same absolute calendar counter family later stored at `[world+0x15]`. It reads one year word plus four packed calendar subfields from `[tuple+0x00/+0x02/+0x04/+0x05/+0x06]`, combines them as one mixed-radix `12 x 28 x 3 x 60` progression, and returns the resulting scalar for direct subtraction against `[0x006cec78+0x15]`. That is now the strongest current read for the world-side calendar model: a packed year-plus-subfield point converted into one absolute counter rather than a plain year or month stamp. Current grounded finance caller is the stock-issue cooldown gate inside `company_apply_annual_finance_policy_and_publish_news` `0x00401c50`, where it converts the current issue tuple at `[company+0x16b/+0x16f]` before comparing that scalar against the active world counter.","objdump + caller inspection + local arithmetic correlation + finance-cooldown correlation + world-calendar-counter correlation + mixed-radix decode" 0x0051d3f0,103,calendar_pack_year_and_component_bytes_to_packed_tuple_dwords,support,cdecl,inferred,objdump + local arithmetic correlation + restore-path correlation,3,"Packs one explicit year-plus-component set into the broader world-calendar tuple format. The helper writes the caller-supplied year word and input bytes into the tuple root on the stack, derives the companion mixed-radix byte fields used by `calendar_point_pack_tuple_to_absolute_counter` `0x0051d3c0`, and returns the resulting packed tuple dwords in `EAX:EDX`. Current grounded load or restore-side caller correlation comes from `world_load_saved_runtime_state_bundle` `0x00446d40` and the shared scenario reset owner `0x00436d10`, where the raw selected-year lane `[profile+0x77]` is first expanded through this helper with Jan-1-style constants before later adjustment and republish through `world_set_selected_year_and_refresh_calendar_presentation_state` `0x00409e80`.","objdump + local arithmetic correlation + restore-path correlation + packed-tuple correlation" 0x0051d460,131,calendar_point_unpack_absolute_counter_to_tuple,support,fastcall,inferred,objdump + caller inspection + local arithmetic correlation,3,"Inverse packed-calendar helper for the absolute counter family rooted at `[world+0x15]`. It accepts one absolute calendar counter in `ECX`, decomposes it back into the same mixed-radix year-plus-subfield tuple, and returns the reconstructed pair of dwords in `EAX:EDX` for storage into fields such as `[world+0x0d]` and `[world+0x11]`. Current local arithmetic now bounds the inverse against the same `12 x 28 x 3 x 60` model as `0x0051d3c0`, which keeps this tuple in the world-side calendar-point family rather than as an unrelated packed timestamp. Current grounded callers are `world_set_selected_year_and_refresh_calendar_presentation_state` `0x00409e80`, which refreshes `[world+0x0d/+0x11]` from the selected absolute counter at `[world+0x15]`, plus the target-resolution wrappers `0x0051d550` and `0x0051d5f0` used by `simulation_advance_to_target_calendar_point` `0x0040ab50`.","objdump + caller inspection + local arithmetic correlation + world-calendar-tuple correlation + target-resolution correlation + mixed-radix decode" 0x004f2e80,14,shell_has_live_overview_window,shell,cdecl,inferred,objdump + caller inspection + nearby-family correlation,4,"Tiny presence probe for the shell-side `Overview.win` singleton rooted at `0x006d12bc`. The helper returns `1` when that live overview object pointer is nonnull and `0` otherwise. Current grounded callers include the post-step shell-window ladder inside `simulation_frame_accumulate_and_step_world` `0x00439140`, where it sits beside other shell-window probes, and the overview family itself around `0x004f3a10`, which uses the same singleton to suppress duplicate opens.","objdump + caller inspection + nearby-family correlation + singleton correlation" @@ -594,7 +594,7 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x00434870,23,scenario_state_get_selected_chairman_company_record,map,thiscall,inferred,objdump + global-state inspection,4,"Returns the currently selected company record for the shell-side scenario state object at `0x006cec78`. The helper reads `[this+0x21]` as a company id and resolves that id through the live company collection at `0x0062be10`; zero or negative ids return null. This is the clearest direct accessor yet for the summary field written by the post-load chairman-profile lane.","objdump + global-state inspection + caller correlation" 0x00434890,35,scenario_state_set_selected_chairman_profile,map,thiscall,inferred,objdump + global-state inspection,4,"Sets the currently selected chairman profile on the shell-side scenario state object at `0x006cec78`. The helper stores the incoming persona id into `[this+0x25]`, resolves that persona through the global profile collection at `0x006ceb9c`, and then copies the linked owner-company id from `[profile+0x1dd]` into `[this+0x21]`. This grounds the summary pair used by the post-load chairman-profile setup lane: `[state+0x25]` is the selected chairman profile id and `[state+0x21]` is the corresponding owning company id.","objdump + global-state inspection + caller correlation" 0x004348c0,23,scenario_state_get_selected_chairman_profile_record,map,thiscall,inferred,objdump + global-state inspection,4,"Returns the currently selected chairman profile record for the shell-side scenario state object at `0x006cec78`. The helper reads `[this+0x25]` as a profile id and resolves it through the global persona collection at `0x006ceb9c`; zero or negative ids return null. This pairs directly with scenario_state_set_selected_chairman_profile at `0x00434890`.","objdump + global-state inspection + caller correlation" -0x004337c0,128,scenario_state_append_runtime_effect_or_queue_record,map,thiscall,inferred,objdump + local disassembly + caller correlation,4,"Allocates one zeroed `0x20`-byte node through `0x005a125d`, copies one caller-supplied string or payload seed into `[node+0x04..]` through `0x0051d820`, stores six further caller dwords into the fixed field band `[node+0x08]`, `[node+0x0c]`, `[node+0x10]`, `[node+0x14]`, `[node+0x18]`, and `[node+0x1c]`, and appends the finished node to the singly linked list rooted at `[this+0x66a6]`. Current grounded callers include `world_region_collection_pick_periodic_candidate_region_and_queue_scaled_event_record` `0x00422100`, which appends payload seed `0x005c87a8`, literal kind `7`, zero promotion-latch dword `[node+0x0c]`, one chosen class-0 region id at `[node+0x10]`, one scaled amount at `[node+0x14]`, and sentinel tails `-1/-1` in `[node+0x18/+0x1c]`, plus the shell-facing side path inside `world_region_try_place_candidate_structure` `0x00422ee0`, several startup-company branches around `0x0047d282/0x0047d409/0x0047d609`, and the broader runtime-effect service family. The exact gameplay label of this linked `0x20`-byte record family is still broader than one subsystem, so the safest current name stays structural around appending one runtime-effect or queue record node beneath scenario state.","objdump + local disassembly + caller correlation + queue-node-list correlation + node-layout correlation + periodic-region-bonus correlation" +0x004337c0,128,scenario_state_append_runtime_effect_queue_node,map,thiscall,inferred,objdump + local disassembly + caller correlation,4,"Allocates one zeroed `0x20`-byte node through `0x005a125d`, copies one caller-supplied string or payload seed into `[node+0x04..]` through `0x0051d820`, stores six further caller dwords into the fixed field band `[node+0x08]`, `[node+0x0c]`, `[node+0x10]`, `[node+0x14]`, `[node+0x18]`, and `[node+0x1c]`, and appends the finished node to the singly linked list rooted at `[this+0x66a6]`. Current grounded callers include `world_region_collection_pick_periodic_candidate_region_and_queue_region_focus_modal_record` `0x00422100`, which appends payload seed `0x005c87a8`, literal kind `7`, zero promotion-latch dword `[node+0x0c]`, one chosen class-0 region id at `[node+0x10]`, one scaled amount at `[node+0x14]`, and sentinel tails `-1/-1` in `[node+0x18/+0x1c]`, plus the shell-facing side path inside `world_region_try_place_candidate_structure` `0x00422ee0`, several startup-company branches around `0x0047d282/0x0047d409/0x0047d609`, and the broader runtime-effect service family. The exact gameplay label of this linked `0x20`-byte node family is still broader than one subsystem, so the safest current name stays structural around appending one runtime-effect queue node beneath scenario state.","objdump + local disassembly + caller correlation + queue-node-list correlation + node-layout correlation + periodic-region-bonus correlation" 0x00436590,372,scenario_state_compute_issue_opinion_multiplier,simulation,thiscall,inferred,objdump + caller inspection,4,"Computes one bounded opinion multiplier for a caller-selected issue slot on the active scenario or shell state rooted at `0x006cec78`. The helper starts from the base issue term at `[this + issue*4 + 0x8a]`, clamps that raw value to a floor of `-99`, normalizes it into a multiplier around `1.0`, and then optionally folds in up to three issue-specific override tables: a company-side term from `[company + issue*4 + 0x35b]`, a chairman-profile term from `[profile + issue*4 + 0x2ab]`, and a territory-side term from `[territory + issue*4 + 0x3b5]`. When the profile argument is omitted but a valid company is supplied, it implicitly reuses that company's linked chairman id from `[company+0x3b]`. The final multiplier is clamped to a small positive floor near `0.01` before return. Current grounded callers include the broader support-adjusted share-price helper at `0x00424fd0`, the merger vote resolver at `0x004ebd10` with issue id `0x3a`, and several other company-policy and shell-side opinion branches. The merger-side `0x3a` use now lines up directly with `RT3.lng` id `726`, which says public merger votes depend on their attitude toward the management of the two companies, so this issue slot is now best read as the merger-management-attitude multiplier.","objdump + caller inspection + issue-table correlation + merger-text correlation" 0x004768c0,53,chairman_profile_owns_all_company_shares,simulation,thiscall,inferred,objdump + caller inspection,4,"Boolean ownership predicate over one chairman profile and company id. The helper resolves the requested company through the live company collection at `0x0062be10`, reads the company's full outstanding-share count from `[company+0x47]`, and compares it against the current profile's holding slot for that same company at `[profile + company_id*4 + 0x15d]`. It returns `1` only when the profile holds the full outstanding-share band and `0` otherwise. Current grounded caller is the CompanyDetail section-0 overview formatter at `0x004e5cf0`, where this is the decision point between the wholly-owned text family `3046/3047` and the investor-attitude text family `3048/3049`.","objdump + caller inspection + ownership-predicate correlation" 0x00436710,163,scenario_state_sum_issue_opinion_terms_raw,simulation,thiscall,inferred,objdump + caller xrefs + callsite inspection,4,"Raw additive companion to `scenario_state_compute_issue_opinion_multiplier` on the active scenario or shell state rooted at `0x006cec78`. The helper starts from the base issue term at `[this + issue*4 + 0x8a]`, then optionally adds the company override term at `[company + issue*4 + 0x35b]`, the chairman-profile override term at `[profile + issue*4 + 0x2ab]`, and the territory override term at `[territory + issue*4 + 0x3b5]` without normalizing or clamping the result into a multiplier. When the profile argument is omitted but a valid company is supplied, it implicitly reuses that company's linked chairman id from `[company+0x3b]`. Current grounded callers include the city-connection bonus lane through `company_compute_issue39_opinion_bias_scalar` at `0x00424580` and several neighboring policy or setup branches that treat the returned integer as one raw issue-opinion total rather than a finished probability or vote scalar.","objdump + caller xrefs + callsite inspection + issue-table correlation + raw-sum correlation" @@ -754,7 +754,6 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x0044cf70,56,world_runtime_construct_root_and_seed_global_0x62c120,map,thiscall,inferred,objdump + world-load correlation + local disassembly,3,"Top-level constructor for the live world root later published at `0x0062c120`. The helper enters the shared world-presentation base init `0x00534f60`, installs vtable `0x005ca9fc`, clears dword `[this+0x2125]`, stores `this` into global `0x0062c120`, and then forwards the three caller-supplied constructor arguments into the heavier initialization body `0x00449200`. Current grounded caller is the world-load allocation strip inside `0x00438c70`.","objdump + world-load correlation + local disassembly + global-publication correlation" 0x0044cfb0,1120,world_load_runtime_grid_and_secondary_raster_tables_from_bundle,map,thiscall,inferred,objdump + caller xrefs + local disassembly + bundle-tag correlation,3,"Heavy world-root bundle-load body beneath `world_construct_root_and_load_bundle_payload_thunk` `0x0044e910`. After the immediate base setup and neighboring local initializers `0x00449270` and `0x00448260`, the helper reads one rooted payload through repeated `0x00531150/0x00531360` calls, storing the core grid and stride scalars into `[this+0x2145/+0x2149/+0x214d/+0x2151/+0x2155/+0x2159/+0x215d/+0x2161/+0x2201]`. It allocates the route-entry collection `0x006cfca8` through `0x00491af0`, allocates or zero-fills the main world arrays `[this+0x2139/+0x213d/+0x2141/+0x2131/+0x2135/+0x212d/+0x2129]` from chunk families `0x2ee2/0x2ee3/0x2ef4/0x2ef5/0x2ef6/0x2ee4/0x2ee5/0x2f43/0x2f44`, and on one loaded-raster path clears low bit `0x01` across every byte in `[this+0x2135]` after the bundle read succeeds. The tail then seeds every live world-grid cell record through `0x0042ae50`, re-enters `world_compute_transport_and_pricing_grid` `0x0044fb70`, refreshes neighboring presentation state through `0x00449f20`, and issues one shell-mode pulse through `0x00484d70` before returning. Current grounded caller is the world-root constructor thunk `0x0044e910` at `0x0044e931`, reached from the world-allocation branch inside `world_entry_transition_and_runtime_bringup` `0x00443a50`, so this is the safest current read for the heavy world-grid and secondary-raster bundle-load body rather than the broader bringup owner.","objdump + caller xrefs + local disassembly + bundle-tag correlation + world-grid-array correlation" 0x0044e270,63,world_resolve_secondary_raster_class_record_at_float_xy,map,thiscall,inferred,objdump + caller xrefs + local disassembly,3,"Small float-XY lookup over the live world secondary-raster class table. The helper rounds the two caller-supplied float coordinates through `0x005a10d0`, indexes the active byte raster at `[world+0x2131]` with row stride `[world+0x2155]`, shifts the selected byte right by three, and resolves the resulting class token through collection `0x006cfc9c`. Current grounded callers include `city_compute_connection_bonus_candidate_weight` `0x004010f0` and the linked-instance wrapper `0x0047f1f0`, so this is the safest current read for the world-side secondary-raster class-record lookup rather than a city-only predicate.","objdump + caller xrefs + local disassembly + secondary-raster-class correlation" -0x0044e910,42,world_construct_root_and_load_bundle_payload_thunk,map,thiscall,inferred,objdump + world-load correlation + local disassembly,3,"Small world-root load thunk above the heavier bundle-body `0x0044cfb0`. The helper enters the shared world-presentation base init `0x00534f60`, installs vtable `0x005ca9fc`, re-enters `0x00448260` for the neighboring local setup, and then forwards the two caller-supplied bundle arguments into `world_load_runtime_grid_and_secondary_raster_tables_from_bundle` `0x0044cfb0`. Current grounded caller is the world-load allocation strip inside `0x00438c70`, which allocates the `0x0062c120` world root and then immediately enters this thunk.","objdump + world-load correlation + local disassembly + thunk-to-bundle-body correlation" 0x00491af0,295,route_entry_collection_construct_indexed_owner_and_zero_slot_array,map,thiscall,inferred,objdump + caller inspection + local disassembly,2,"Concrete constructor for the route-entry collection rooted at `0x006cfca8`. The helper installs vtable `0x005cfe18`, re-enters the shared indexed-collection constructor path `0x00518570` with capacity-like scalars `0x3e8` and `0x1f4`, zeros the local side bands `[this+0x88..+0x10a]`, seeds `[this+0x88]` to `-1` and `[this+0x206]` to `-1.0f`, clears the small trailing triplet at `[this+0x10b]`, and allocates then zero-fills the separate slot-array pointer `[this+0x8c]` from caller count argument `arg0 * 4`. Current grounded caller is the world bundle-load body `world_load_runtime_grid_and_secondary_raster_tables_from_bundle` `0x0044cfb0`, so this is the safest current read for the route-entry collection constructor rather than a generic list init.","objdump + caller inspection + local disassembly + indexed-collection correlation + route-entry-collection correlation" 0x00491c20,56,route_entry_collection_refresh_all_live_entries_derived_visual_bands,map,thiscall,inferred,objdump + caller inspection + local disassembly,2,"Small collection-wide post-pass over the route-entry collection `0x006cfca8`. The helper enumerates all live entries through `indexed_collection_slot_count` `0x00517cf0`, `indexed_collection_get_nth_live_entry_id` `0x00518380`, and `indexed_collection_resolve_live_entry_by_id` `0x00518140`, then re-enters `route_entry_recompute_derived_visual_scalar_bands_and_optional_display_buffer` `0x00539640` on each resolved route entry. Current grounded caller is the later `319`-lane post-load pipeline inside `world_run_post_load_generation_pipeline` `0x004384d0`, where the same pass runs after placed-structure local-runtime refresh and flagged world-grid cleanup, so this is the safest current read for the collection-wide route-entry visual-band refresh pass rather than an anonymous indexed sweep.","objdump + caller inspection + local disassembly + indexed-collection correlation + post-load-pipeline correlation" 0x00491c60,134,route_entry_collection_serialize_records_into_tagged_bundle,simulation,thiscall,inferred,objdump + local disassembly + caller correlation + tag-family correlation,3,"Collection-wide tagged save owner for the main route-entry collection rooted at `0x006cfca8`. The helper emits tag family `0x38a5/0x38a6/0x38a7` through `0x00531340`, serializes collection-side metadata through `0x00517d90`, writes dword `[this+0x90]` directly into the active bundle through `0x00531030`, and then walks every live route-entry record through `0x00517cf0/0x00518380/0x00518140` before dispatching each record through `0x0048a6c0` with the caller bundle. Current grounded caller is the early package-save prelude `0x00444dd0`, where this helper runs before the auxiliary route-entry tracker collection serializer. This is the safest current read for the main route-entry tagged save owner rather than another anonymous collection walk.","objdump + local disassembly + caller correlation + tag-family correlation + route-entry correlation" @@ -1077,7 +1076,6 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x00477110,202,profile_entry_buy_company_holding_units_and_subtract_cash_metric,map,thiscall,inferred,objdump + local disassembly + caller inspection,3,"Buy-side mutation over one named-profile record's per-company holding slot. Unless the caller explicitly bypasses the precheck, the helper first requires `profile_entry_query_can_buy_company_holding_units_with_optional_cash_fallback` `0x00476ff0` to succeed for the requested company and unit count. It then resolves the target company from `0x0062be10`, applies the company-side update path through `0x00424fd0(1, units)` and `0x00424fd0(0,0)`, adds the requested units into holding slot `[this + company_id*4 + 0x15d]`, converts the resulting purchase cost through `0x005a10d0` and constant `0x005c8930`, subtracts that scaled cost from qword cash-like field `[this+0x154]`, and finally refreshes scenario-side chairman state through `0x00433850` and `0x0050a670`. Current grounded callers are the shell-side buy branches around `0x0050c1ae` and the broader transaction family beside `0x00476460`, so this is the clearest current buy companion to the sell-side helper.","objdump + local disassembly + caller inspection + holding-slot correlation + cash-metric correlation + trade-branch correlation" 0x00477820,59,profile_collection_count_active_chairman_records,map,thiscall,inferred,objdump + callsite inspection,4,"Counts active chairman-profile records in the global persona collection at `0x006ceb9c`. The helper iterates the collection and increments the result only for entries whose live flag at `[profile+0x4]` is nonzero. Current grounded caller is `world_conditionally_seed_named_starting_railroad_companies` at `0x0047d440`, where count `>= 2` gates whether the second seeded railroad can be bound to another active chairman profile.","objdump + callsite inspection + caller correlation" 0x00477860,90,profile_collection_get_nth_active_chairman_record,map,thiscall,inferred,objdump + callsite inspection,4,"Returns one zero-based active chairman-profile record from the global persona collection at `0x006ceb9c`. The helper skips any entry whose live flag `[profile+0x4]` is zero and decrements the requested ordinal across the remaining active entries until it resolves one record or returns null. Current grounded caller is `world_conditionally_seed_named_starting_railroad_companies` at `0x0047d440`, which requests ordinal `1` to bind the second seeded railroad to the second active chairman profile when present.","objdump + callsite inspection + caller correlation" -0x004778c0,84,profile_collection_get_nth_active_chairman_record_id,map,thiscall,inferred,objdump + local disassembly + caller correlation,2,"Returns the live collection id of the zero-based active chairman-profile entry selected by the caller ordinal. The helper walks the global persona collection at `0x006ceb9c`, skips any entry whose live flag `[profile+0x4]` is zero, decrements the requested ordinal across the remaining active entries, and returns the matched live record id through `indexed_collection_get_nth_live_entry_id` `0x00518380`; on failure it returns `0`. Current grounded caller is the `LoadScreen.win` player-list renderer `0x004e6ef0`.","objdump + local disassembly + caller correlation + active-chairman correlation" 0x00477920,91,profile_collection_count_active_chairmen_before_record_id,map,thiscall,inferred,objdump + local disassembly + caller correlation,2,"Counts how many active chairman-profile records appear before one caller-supplied live record id in the global persona collection at `0x006ceb9c`. The helper walks the live collection in order, stops when the current live id equals the target id, increments the running count only for entries whose live flag `[profile+0x4]` is nonzero, and returns that active-prefix count; missing targets return `0`. Current grounded caller is the `LoadScreen.win` player-list renderer `0x004e6ef0`, where it provides the active-rank position for one selected profile id.","objdump + local disassembly + caller correlation + active-chairman correlation" 0x00477a70,94,shell_overlay_state_measure_scaled_max_world_span_with_min_floor,shell,thiscall,inferred,objdump + local disassembly + field correlation,2,"Small scalar helper over the local shell overlay-state strip. The function compares the current world-cell span widths `[this+0xbb]-[this+0xb3]` and `[this+0xbf]-[this+0xb7]`, clamps the larger span upward to minimum floor `[this+0x11f]`, scales that result by factor `[this+0xdb]`, rounds through `0x005a10d0`, and returns the final floating-point span. Current grounded caller is the nearby clamp-and-refresh owner `0x00479510`, so this is the safest current read for the overlay state's scaled max-span query rather than a generic arithmetic helper.","objdump + local disassembly + field correlation + caller correlation" 0x00477ad0,16,shell_overlay_try_get_primary_active_mode_collection,shell,thiscall,inferred,objdump + local disassembly + flag correlation,1,"Tiny mode-gated accessor over shell flag band `[this+0x33]`. When bit `0x00010000` is set it tail-jumps into `0x005130f0` and returns that active-mode collection root; otherwise it returns `0`. Current grounded caller is the broader overlay-list family around `0x00478330`.","objdump + local disassembly + flag correlation + caller correlation" @@ -1131,8 +1129,6 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x00477770,11,profile_collection_release_collection_storage,simulation,thiscall,inferred,objdump + world-shutdown correlation + local disassembly,2,"Release-side companion to `profile_collection_construct` `0x00477740`. The helper reinstalls vtable `0x005ce828` and tails into the common collection teardown `0x00518bd0`. Current grounded caller is the ordered world-root shutdown strip `0x00434300`.","objdump + world-shutdown correlation + local disassembly + companion correlation" 0x00477780,92,profile_collection_refresh_tagged_header_counts_from_bundle,map,thiscall,inferred,objdump + world-load correlation + local disassembly + tag correlation,3,"Bundle-load sibling for the live profile collection `0x006ceb9c`. The helper reads the three tagged header dwords `0x5209/0x520a/0x520b` through repeated `0x00531360` calls around the collection refresh `0x00518680`, accumulating the returned byte counts. Current grounded caller is the world-entry bring-up strip, which re-enters this helper to refresh the profile collection's tagged header state during runtime load.","objdump + world-load correlation + local disassembly + tag correlation + collection-refresh correlation" 0x004777e0,92,profile_collection_serialize_tagged_header_counts_into_bundle,map,thiscall,inferred,objdump + package-save correlation + local disassembly + tag correlation,3,"Package-save sibling for the live profile collection `0x006ceb9c`. The helper opens the same three tagged header dwords `0x5209/0x520a/0x520b` through repeated `0x00531340` calls around the collection serializer `0x00517d90`, accumulating the returned byte counts before closing the final tag. Current grounded caller is the package-save strip, which mirrors the profile collection's tagged header state back into the runtime bundle during world save.","objdump + package-save correlation + local disassembly + tag correlation + collection-save correlation" -0x00477820,59,profile_collection_count_active_chairman_records,simulation,thiscall,inferred,objdump + local disassembly + caller correlation,3,"Counts the currently active chairman-profile records in the live profile collection `0x006ceb9c`. The helper walks every live record through `0x00517cf0/0x00518380/0x00518140`, tests dword `[profile+0x04]`, and increments the result only when that field is nonzero. Current grounded callers include the re-entrant chairman refresh owner `0x00433850` and neighboring chairman-company branches, so this is the safest current read for the active-chairman count helper rather than a generic slot-count query.","objdump + local disassembly + caller correlation + active-chairman correlation" -0x00477860,92,profile_collection_get_nth_active_chairman_record,simulation,thiscall,inferred,objdump + local disassembly + caller correlation,3,"Resolves the caller-selected nth active chairman-profile record in the live profile collection `0x006ceb9c`. The helper walks every live record through `0x00517cf0/0x00518380/0x00518140`, tests dword `[profile+0x04]`, decrements the caller-supplied ordinal only on active rows, and returns the first record reached when that active-only ordinal hits zero. Current grounded callers include `scenario_state_refresh_active_chairman_profiles_with_reentrancy_guard` `0x00433850`, where it feeds the per-record refresh loop over the active chairman family. This is the safest current read for the nth-active-chairman resolver rather than a plain nth-live-entry wrapper.","objdump + local disassembly + caller correlation + active-chairman correlation" 0x004778c0,107,profile_collection_get_nth_active_chairman_id,simulation,thiscall,inferred,objdump + local disassembly + caller correlation,2,"Sibling helper in the same active-chairman strip over the live profile collection `0x006ceb9c`. The function walks every live record through `0x00517cf0/0x00518380`, treats only rows whose resolved record has nonzero dword `[profile+0x04]` as active, decrements the caller-supplied ordinal on those active rows, and returns the live entry id when that active-row ordinal hits zero, or `0` when the walk exhausts first. Current local evidence keeps this bounded as the id-returning companion beside `profile_collection_count_active_chairman_records` `0x00477820` and `profile_collection_get_nth_active_chairman_record` `0x00477860`.","objdump + local disassembly + caller correlation + active-chairman correlation" 0x0047e9a0,315,placed_structure_query_candidate_route_or_local_service_comparison_score,map,thiscall,inferred,objdump + caller xrefs + local disassembly + callsite inspection,2,"Bridges the heavier directional-route overlay summary path and the simpler local-service comparison lane. When the requested candidate carries a grouped routing class at `0x0062ba8c+0x9a`, the helper forwards the full query to `placed_structure_query_candidate_directional_route_overlay_summary` `0x0047e690` with the final flag forced to `0`. Otherwise it resolves the current placed structure's linked instance through `0x00455f60`, samples the current site's local float table at `[cell+0x103]` for the requested candidate lane, optionally re-enters `0x0042c080` on each local sample record at `[this+0x34/+0x38]`, and writes the bounded result back into the same comparison-cache bands rooted at `[this+0x1e6]`, `[this+0x2ba]`, and `[this+0x38e]`. Current grounded caller is the candidate-side score branch at `0x0041bbe2`, so this is the safest current read for the route-or-local comparison wrapper above `0x0047e690` rather than another route-list helper.","objdump + caller xrefs + local disassembly + callsite inspection + cache-band correlation" 0x0047d440,845,world_conditionally_seed_named_starting_railroad_companies,map,cdecl,inferred,objdump + caller xrefs + global-state inspection,4,"Conditional company-side setup helper adjacent to the `Setting up Players and Companies...` lane. Current grounded callers are the neighboring bring-up flow after world_seed_default_chairman_profile_slots at `0x004377a0` and a second setup-side branch around `0x00438300`, both under the same sandbox or non-editor shell-state conditions: it runs only when the Multiplayer preview dataset owner at `0x006cd8d8` is absent and either sandbox flag `[0x006cec7c+0x82]` is set or shell-state flag `[0x006cec74+0x14c]` is set while editor-map mode `[0x006cec74+0x68]` is clear. The helper first realigns the selected company from the chosen chairman profile when the current summary pair points at a missing company, then iterates exactly three fixed localized railroad-name ids through `0x00428420`: `0x23f` `Missouri Pacific`, `0x240` `New York Central`, and `0x241` `Grand Trunk Railroad` from `RT3.lng` ids `575..577`. That makes the branch look like a seeded trio of named starting railroad companies rather than a generic company refresh. The first seeded company is tied to the selected chairman-profile summary from `0x006cec78`, tuned from that selected chairman's per-profile fields `[profile+0x154]` and `[profile+0x158]`, and then written back as the selected company id through `0x00433790`. The second company is narrower now too: it only binds when `profile_collection_count_active_chairman_records` at `0x00477820` reports at least two live chairman records, and it then links through `profile_collection_get_nth_active_chairman_record` at `0x00477860` with ordinal `1`, so the second railroad is specifically the second active chairman-owned company rather than an arbitrary extra bind. The third railroad currently gets no matching chairman-link branch in the grounded code and therefore remains an unchaired named company in the live roster. All three records are constructed or refreshed from the live company collection at `0x0062be10` through `0x00428420`, while chairman-to-company ownership links are applied through `0x00427c70` and neighboring writes to `[profile+0x1dd]`. The tail is tighter now too: once at least three startup companies have been considered, the branch at `0x0047d6de` re-enters `scenario_event_collection_service_runtime_effect_records_for_trigger_kind` `0x00432f40` with trigger kind `7`.","objdump + caller xrefs + global-state inspection + state-accessor correlation + RT3.lng strings + company-constructor inspection + profile-helper inspection + runtime-effect trigger-kind correlation" @@ -1187,7 +1183,6 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x004b2b30,38,train_route_list_reset_descriptor_and_trailing_scalar_band,simulation,thiscall,inferred,objdump + local disassembly + field-layout correlation,2,"Resets one local train route-list descriptor or adjunct block in place. The helper clears the two leading dwords, sets dword `+0x08` to `-1`, clears byte `+0x0c` and dword `+0x0d`, then clears the trailing five dwords rooted at `+0x11`. This is the safest current read for the in-place route-list descriptor reset helper rather than a train-record constructor.","objdump + local disassembly + field-layout correlation" 0x004b2b60,12,train_route_list_get_row_ptr_by_index,simulation,thiscall,inferred,objdump + local disassembly + caller correlation,2,"Returns one `0x33`-stride route-list row pointer by index from the train route-list buffer rooted at `[this+0x00]`. Current grounded callers include the cleanup helpers `0x004af610/0x004af760` and several shell-side adjunct families, so this is the narrow indexed route-row accessor rather than a train collection helper.","objdump + local disassembly + caller correlation + route-list-buffer correlation" 0x004b2b70,2,train_route_adjunct_identity_passthrough,simulation,thiscall,inferred,objdump + local disassembly + caller correlation,1,"Single-instruction identity helper that returns the caller train pointer unchanged. Current grounded callers are the selector-`0x42` multiplayer train-adjunct branch and the shell overlay owner that already resolved a live train collection entry before re-entering this helper, so this is best read as a structural train-adjunct passthrough placeholder.","objdump + local disassembly + caller correlation" -0x004b2b80,29,train_route_list_count_rows_with_nonnegative_sign_byte,simulation,thiscall,inferred,objdump + local disassembly + route-list-row correlation,2,"Counts how many `0x33`-stride route-list rows carry a nonnegative sign byte at offset `+0x28`. The helper walks the live route-list buffer and increments the result only when that byte is not negative. This is the safest current read for the positive-sign row counter beside the route-row accessors.","objdump + local disassembly + route-list-row correlation" 0x004b2ba0,40,train_route_list_reset_descriptor_and_trailing_scalar_band_alt,simulation,thiscall,inferred,objdump + local disassembly + field-layout correlation,1,"Register-allocation variant of `train_route_list_reset_descriptor_and_trailing_scalar_band` `0x004b2b30`. The helper performs the same in-place reset over the leading descriptor band and trailing five-dword scalar band. This is the safest current read for the duplicate reset sibling rather than a separate train-record owner.","objdump + local disassembly + field-layout correlation + duplicate-reset correlation" 0x004b2bd0,18,train_route_list_get_current_row_ptr_or_null,simulation,thiscall,inferred,objdump + local disassembly + route-list-row correlation,2,"Returns the current `0x33`-stride route-list row pointer selected by descriptor dword `+0x08`, or null when that selector is negative. This is the safest current read for the current-row accessor beside the route-list helper strip.","objdump + local disassembly + route-list-row correlation" 0x004b2bf0,24,train_route_list_get_current_or_last_row_ptr_or_null,simulation,thiscall,inferred,objdump + local disassembly + route-list-row correlation,2,"Returns one `0x33`-stride route-list row pointer from the same helper descriptor: a negative selector yields null, selector `0` maps to the last live row, and any other positive selector maps to the immediately preceding row. This is the safest current read for the current-or-last route-row accessor beside `0x004b2bd0`.","objdump + local disassembly + route-list-row correlation" @@ -1270,7 +1265,7 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x004dbfca,32,shell_event_conditions_window_select_grouped_effect_target_scope_mode_and_refresh,shell,thiscall,inferred,objdump + local disassembly + dispatcher correlation + grouped-band dispatch-table decode + RT3.lng correlation,3,"Grouped target-scope mode selector case beneath `EventConditions.win` on the `0xcf` grouped-band path. The helper first commits the currently visible selected-event text panels through `shell_event_conditions_window_commit_current_selected_event_text_panels_before_selection_change` `0x004d8ea0`, then refreshes the grouped-effect target-scope strip through `shell_event_conditions_window_refresh_selected_grouped_effect_target_scope_strip` `0x004dab60`, and finally stores the clicked control id into global `0x00622074` so the same target-scope selector can be re-enabled during later selected-event bring-up at `0x004db120`. The `0x4dc09c` dispatch table now keeps the control family honest: this case is only reached by controls `0x5001` and `0x5002`, not by visible rows `0x5014..0x501c`. Current RT3.lng adjacency gives those two controls their strongest current player-facing fit too: `0x5001` is the condition-side `Test against...` mode and `0x5002` is the grouped-effect-side `Apply effects...` mode. This is therefore the safest current read for grouped target-scope mode selection and refresh rather than for the whole visible target-scope display family.","objdump + local disassembly + dispatcher correlation + grouped-band dispatch-table decode + persisted-selection correlation + RT3.lng correlation" 0x004dbf93,55,shell_event_conditions_window_select_grouped_effect_summary_band_and_refresh,shell,thiscall,inferred,objdump + local disassembly + dispatcher correlation,3,"Grouped-band selector verb beneath `EventConditions.win`. The helper first commits the current grouped-effect summary editor state through `shell_event_conditions_window_commit_current_grouped_effect_summary_state_before_group_switch` `0x004d8d50`, stores the newly chosen grouped selector ordinal `control_id - 0x4fed` into `[this+0x9c]`, refreshes the visible grouped rows through `0x004d88f0`, re-enters the surrounding condition-window repaint through `0x004da0f0`, and then finishes with the smaller grouped summary tail at `0x004da9a0`. Current grounded dispatcher caller is the `0x4fed..0x4ff0` case family under `0x004dbb80`, so this is the safest current read for grouped-effect summary band selection and refresh rather than a generic selector write.","objdump + local disassembly + dispatcher correlation + grouped-band-selector correlation" 0x004c8670,11,shell_mark_custom_modal_dialog_dirty,shell,cdecl,inferred,objdump + nearby-constructor correlation + frame-caller inspection,4,"Tiny dirty-latch setter paired with the shared callback-driven custom modal family rooted at `0x006cfeb4`. The helper stores `1` into `0x006cfeac`, which current nearby call patterns treat as the custom-modal refresh or service request latch after the lower constructor at `0x004c86c0` and opener `shell_open_custom_modal_dialog_with_callbacks` have published a live modal object. Current grounded caller is the post-step shell-window ladder inside `simulation_frame_accumulate_and_step_world` `0x00439140`.","objdump + nearby-constructor correlation + frame-caller inspection + custom-modal correlation" -0x004c7520,190,shell_open_region_focus_modal_and_center_world_on_confirmed_region,shell,cdecl,inferred,objdump + local disassembly + caller correlation + modal-callback correlation,4,"Opens the region-focused custom modal used by queued runtime-effect kind `7` under `simulation_dispatch_runtime_effect_queue_record_by_kind_into_shell_or_world_handlers` `0x00437c00`. The helper first requires scenario latch `[0x006cec74+0x277]`, stores the incoming region id in global `0x006cfe80`, allocates one helper object through `0x0053c6f0(2)` and stores that handle in `0x006cfe88`, then opens the callback-driven custom modal through `shell_open_custom_modal_dialog_with_callbacks` `0x004c98a0` with localized root `0x005c87a8`, callbacks `0x004c72f0` and `0x004c73c0`, mode `0x0a`, and scalar `0.5f`. On teardown it releases the helper through `0x0053c000`, and when the modal returns `0x3f2` it resolves the selected region id back through collection `0x0062bae0` and recenters the live world through `0x00433900`. Callback `0x004c72f0` formats one region-specific callout from the selected region name at `[region+0x356]`, the rounded region scalar `[region+0x276]`, and the helper root in `0x006cfe88`; callback `0x004c73c0` builds one interactive control payload strip from the supplied float pair. So the current strongest read is a region-focus or region-confirm modal owner, not a generic one-arg message path.","objdump + local disassembly + caller correlation + modal-callback correlation + region-collection correlation + world-center correlation" +0x004c7520,190,shell_open_region_focus_modal_and_center_world_on_confirmed_region,shell,cdecl,inferred,objdump + local disassembly + caller correlation + modal-callback correlation,4,"Opens the region-focused custom modal used by queued runtime-effect kind `7` under `simulation_dispatch_runtime_effect_queue_node_by_kind` `0x00437c00`. The helper first requires scenario latch `[0x006cec74+0x277]`, stores the incoming region id in global `0x006cfe80`, allocates one helper object through `0x0053c6f0(2)` and stores that handle in `0x006cfe88`, then opens the callback-driven custom modal through `shell_open_custom_modal_dialog_with_callbacks` `0x004c98a0` with localized root `0x005c87a8`, callbacks `0x004c72f0` and `0x004c73c0`, mode `0x0a`, and scalar `0.5f`. On teardown it releases the helper through `0x0053c000`, and when the modal returns `0x3f2` it resolves the selected region id back through collection `0x0062bae0` and recenters the live world through `0x00433900`. Callback `0x004c72f0` formats one region-specific callout from the selected region name at `[region+0x356]`, the rounded region scalar `[region+0x276]`, and the helper root in `0x006cfe88`; callback `0x004c73c0` builds one interactive control payload strip from the supplied float pair. So the current strongest read is a region-focus or region-confirm modal owner, not a generic one-arg message path.","objdump + local disassembly + caller correlation + modal-callback correlation + region-collection correlation + world-center correlation" 0x004c8680,14,shell_has_live_custom_modal_dialog,shell,cdecl,inferred,objdump + nearby-constructor correlation + frame-caller inspection,4,"Tiny presence probe for the shared callback-driven custom modal family. The helper reads the live modal singleton at `0x006cfeb4`, returns `1` when that pointer is positive or nonnull, and returns `0` otherwise. Current grounded caller is the post-step shell-window ladder inside `simulation_frame_accumulate_and_step_world` `0x00439140`, where this branch now sits beside the `LoadScreen`, file-options, and other shell-window probes rather than any detached gameplay-only UI owner.","objdump + nearby-constructor correlation + frame-caller inspection + custom-modal correlation" 0x004e1d60,1169,shell_load_screen_refresh_page_strip_and_page_kind_controls,shell,thiscall,inferred,objdump + descriptor-table decode + caller inspection,4,"Shared refresh helper beneath the shell-side `LoadScreen.win` family. The helper reads the active 13-byte page descriptor record at `0x006220a0 + page*0x0d` using current page id `[this+0x78]`, where the record now grounds as `{ page_kind_dword, title_string_id, back_link_page_dword, selected_company_header_flag_byte }`. It copies the descriptor kind into `[this+0x80]`, refreshes the page-strip and companion controls `0x3e80`, `0x3e81`, `0x3eee..0x3ef7`, `0x3ef8`, `0x3efb`, `0x3efc`, `0x3f0a`, `0x3f0b`, and `0x3f0e` through the generic shell control path at `0x00540120`, and applies page-specific row-band styling from current page-local row state `[this+0x7c]`, row count `[this+0x84]`, and stacked selection history `[this+0x0a0..0x118]`. The descriptor decode now also bounds control `0x3ef8` as the table-driven backlink affordance: page `0` keeps backlink `0`, page `1` backlinks to page `2`, page `8` backlinks to page `7`, page `0x0b` backlinks to page `0x0a`, and page `0x0d` backlinks to page `0x0c`. Current descriptor decode therefore keeps page `0` as kind `0`, title id `1200` `XXX`, backlink `0`, and header flag `0`; page `1` as company-overview kind `1` with backlink page `2`; page `8` as player-detail kind `3` with backlink page `7`; page `0x0b` as dormant train-detail title with backlink page `0x0a`; and page `0x0d` as dormant station-detail title with backlink page `0x0c`. Current grounded caller is `shell_load_screen_select_page_subject_and_refresh` at `0x004e2c10`, which uses this helper after storing a new page or subject selection.","objdump + descriptor-table decode + caller inspection + control-style correlation + backlink-control correlation" 0x004e2c10,3472,shell_load_screen_select_page_subject_and_refresh,shell,thiscall,inferred,objdump + caller inspection + state-flow correlation,4,"Shared page or subject selector beneath the shell-side `LoadScreen.win` family rooted at `0x006d10b0`. The helper accepts a page id, a page-local subselector, and an optional subject id; defaults of `-1` reuse the current fields at `[this+0x78]`, `[this+0x7c]`, and caller-selected company or profile state. It stores the chosen page in `[this+0x78]`, the subselector in `[this+0x7c]`, resets several page-local fields including `[this+0x84]` and the report-row latch at `[this+0x75c]`, updates the current company id `[this+0x88]` or profile id `[this+0x8c]` depending on page ownership, and then repaints the active page controls through `0x00563250` and `0x0053fe00`. Several page-specific branches also recompute bounded row-count state for the report lists on pages `0x0a`, `0x0c`, `0x0f`, and `0x0e`, including row styling over control band `0x3f48..`. The surrounding descriptor table now makes one narrower part of this family explicit too: when control `0x3ef8` is used as a backlink affordance, the selected page target comes from the current descriptor backlink dword rather than from a separate hardcoded detail-page branch. Current grounded callers include the broad `LoadScreen.win` dispatcher at `0x004e3a80`, the page-specific stock-holdings branch at `0x004e45d0`, and the selected-company or selected-profile step helpers at `0x004e3a00` plus `0x004e45d0`, so this now reads as the family-wide page or subject refresh owner rather than a CompanyDetail helper.","objdump + caller inspection + state-flow correlation + page-refresh correlation + backlink-target correlation" @@ -1495,9 +1490,9 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x00501f20,270,shell_query_registry_open_command_for_http_or_rtf_target,shell,cdecl,inferred,objdump + literal inspection,3,"Queries one shell-open command string from the registry and formats it into the shared command buffer at `0x006d14d8`. Selector `0` uses the literal `http\\shell\\open` and validates the returned command against the substrings `netsc` and the quoted percent-one placeholder; selector `1` uses `rtffile\\shell\\open`, truncates at the percent-one placeholder, and appends the literal `readme.rtf` through the format `%s %s`. The helper returns `0x006d14d8` on success or `0` on failure. Current grounded callers are the early `Setup.win` dispatcher buttons at `0x00503585/0x0050359f/0x005035c7/0x005035ce`, which then forward the formatted command through import `0x005c8088` instead of re-entering the normal setup launch-mode family.","objdump + literal inspection + caller correlation" 0x00468b00,10,multiplayer_toggle_preview_dataset_service_gate,shell,cdecl,inferred,objdump + local disassembly + global-state inspection,2,Tiny helper that flips the preview-dataset frame-service gate at `0x006cd910` by XORing it with `1` and then returns `0`. Current evidence only grounds the store itself so this row stays structural for now.,objdump + local disassembly + global-state inspection 0x00468b10,58,multiplayer_preview_dataset_register_selector_callback_if_absent,shell,thiscall,inferred,objdump + local disassembly + registration-pass correlation,3,"Small selector-callback registration helper over the keyed descriptor owner at `[this+0x04]`. It first probes the keyed store through `0x0053daa0(selector)` and, when no existing entry is present, inserts the caller-supplied callback pointer through `0x0053d960(selector, callback)` before returning success. Current grounded caller is the broader preview-dataset callback seeding pass `0x00473280`, which uses this helper to populate selector handlers `1..0x83`, so this is the clean local registration primitive beneath that callback-registry owner rather than a generic keyed insert.","objdump + local disassembly + registration-pass correlation + keyed-store correlation" -0x00468b50,7,multiplayer_preview_dataset_set_default_callback_root,shell,thiscall,inferred,objdump + local disassembly + caller inspection,2,Tiny setter that stores one caller-supplied callback root into `[this+0x08]`. Current grounded caller is the broader preview-dataset callback registration pass `0x00473280`, which seeds this slot with `0x0046f4a0` before populating the keyed selector table.,objdump + local disassembly + caller inspection + callback-root correlation +0x00468b50,7,multiplayer_preview_dataset_set_default_callback_root,shell,thiscall,inferred,objdump + local disassembly + caller inspection,2,"Tiny setter that stores one caller-supplied callback root into `[this+0x08]`. Current grounded caller is the broader preview-dataset callback registration pass `0x00473280`, which seeds this slot with `0x0046f4a0` before populating the keyed selector table.",objdump + local disassembly + caller inspection + callback-root correlation 0x00468d00,222,multiplayer_update_semicolon_name_list,shell,cdecl,inferred,ghidra-headless,4,Adds or removes one player-name token in the semicolon-delimited moderation list rooted at `[ecx+0x905c]`. With mode `1` it appends the supplied token only when not already present writing `;` as the delimiter; with mode `0` it removes the matched token collapses the remainder left and skips an adjacent delimiter when present.,ghidra + rizin + llvm-objdump + strings -0x00468de0,14,multiplayer_session_event_forward_action1_request,shell,unknown,inferred,ghidra-headless,2,Session-event callback wrapper that always forwards request id `1` through multiplayer_set_pending_session_substate with a zero auxiliary payload. The callback clears EDX before the shared setter call and currently lands on a request id whose visible pending-substate label is not surfaced locally, so the row remains at that structural request-forwarding level.,ghidra + rizin + llvm-objdump +0x00468de0,14,multiplayer_session_event_forward_action1_request,shell,unknown,inferred,ghidra-headless,2,"Session-event callback wrapper that always forwards request id `1` through multiplayer_set_pending_session_substate with a zero auxiliary payload. The callback clears EDX before the shared setter call and currently lands on a request id whose visible pending-substate label is not surfaced locally, so the row remains at that structural request-forwarding level.",ghidra + rizin + llvm-objdump 0x00468e00,188,multiplayer_session_event_publish_pair_chat_template,shell,unknown,inferred,ghidra-headless,3,Session-event callback that formats one two-string chat/status line through multiplayer_route_chat_line when the callback status in EDX is zero and the current live session count is not positive. A selector near `[esp+0x214]` chooses the template `%s* %s` `%s %s` or `%s > %s`; the helper length-checks both inputs against the local 0x1f4-byte buffer before formatting and returns without publishing when either string is null or too long.,ghidra + rizin + llvm-objdump + strings 0x00468ec0,144,multiplayer_session_event_publish_action2_single_name,shell,unknown,inferred,ghidra-headless,3,Session-event callback wrapper for the one-name action-2 status path. When the callback status in EDX is zero and the current session count is positive it length-checks the supplied name formats localized text id `0xb73` routes the resulting line through multiplayer_route_chat_line and then forwards request id `2` through multiplayer_set_pending_session_substate with that same name payload. The currently grounded setter does not visibly store a pending substate for id `2` so this row stays partly structural.,ghidra + rizin + llvm-objdump 0x00468f50,192,multiplayer_session_event_publish_action2_pair,shell,unknown,inferred,ghidra-headless,3,Session-event callback wrapper for the two-string action-2 status path. When the callback status in EDX is zero and the supplied name lengths fit within the local buffer it formats localized text id `0xb74` or `0xe34` depending on whether the first string is present routes the resulting line through multiplayer_route_chat_line and then forwards request id `2` through multiplayer_set_pending_session_substate with the second string as the target payload and the first string as the auxiliary payload. The currently grounded setter does not visibly store a pending substate for id `2` so this row stays partly structural.,ghidra + rizin + llvm-objdump @@ -1805,7 +1800,7 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x00421e30,275,world_region_collection_resolve_nth_region_with_profile_label_matching_candidate_name,map,thiscall,inferred,objdump + local disassembly + caller correlation,3,"Resolves the Nth live region in the current region collection whose profile subcollection `[region+0x37f]` contains one label equal to the candidate-name string for the caller-supplied candidate id. The helper uses the same first-byte lowercase prefilter and full casefolded compare as `0x00421d20`, skips nonzero-class regions whose `[region+0x242]` count is nonpositive, and increments one match counter once per region rather than once per profile row. When that counter reaches the caller-supplied 1-based ordinal, it returns the matching region record pointer; otherwise it returns null. Current grounded caller is the editor-side enumerator at `0x004cdeab`, which immediately formats the returned region name for display.","objdump + local disassembly + caller correlation + region-profile correlation + editor-summary correlation" 0x00422010,155,world_region_collection_resolve_nearest_class0_region_id_covering_normalized_rect,map,thiscall,inferred,objdump + local disassembly + caller correlation,2,"Scans the live region collection for class-0 region records whose normalized primary and secondary coordinates fall inside the caller-supplied inclusive float bounds, refreshes a small region-local helper through `0x00455a70(0)` on each in-bounds candidate, and keeps the nearest matching region id `[region+0x23a]` by squared XY distance through `math_measure_float_xy_pair_distance` `0x0051db80`. The helper returns `0` when no class-0 region lies inside the supplied rectangle. Current grounded callers include the world-side selection branch at `0x00452658`, where integer world arguments are converted to floats before the query, and the small string wrapper `0x004220b0`.","objdump + local disassembly + caller correlation + region-distance correlation" 0x004220b0,73,world_region_collection_query_name_of_nearest_class0_region_covering_normalized_rect_or_default,map,thiscall,inferred,objdump + local disassembly + caller correlation,2,"Small wrapper above `world_region_collection_resolve_nearest_class0_region_id_covering_normalized_rect` `0x00422010`. When the inner query returns a live region id, the helper resolves that region and returns name pointer `[region+0x356]`; otherwise it copies the fallback localized string `0x00d0` into global scratch buffer `0x0062ba90` and returns that buffer instead. Grounded callers include the early world-status formatter `0x00401815`, the later world-side branch `0x00411c41`, and the terrain-side formatter at `0x004f2b97`.","objdump + local disassembly + caller correlation + region-name correlation" -0x00422100,543,world_region_collection_pick_periodic_candidate_region_and_queue_region_focus_modal_record,map,thiscall,inferred,objdump + local disassembly + caller correlation,3,"Periodic region-side selector reached from `simulation_service_periodic_boundary_work` `0x0040a590`. The helper first requires several live world-state gates to be clear, derives one year-sensitive random threshold from selected-year fields `[world+0x05/+0x0d/+0x0f]` plus live world width `[world+0x2155]`, and returns immediately unless one bounded random test passes. On the active path it scans the live region collection twice. The first pass counts eligible class-0 regions whose transient dwords `[region+0x276]` and `[region+0x302]` are clear and which fail the city-connection peer probe `0x00420030(1,1,0,0)`. The second pass picks one random eligible region, derives one small severity bucket from cached scalar `[region+0x25e]`, stores the resulting scaled amount into `[region+0x276]`, and appends one `0x20`-byte queued record through `0x004337c0` with fixed payload `0x005c87a8`, literal kind `7`, zero promotion-latch dword `[node+0x0c]`, the chosen region id `[node+0x10]`, that scaled amount at `[node+0x14]`, and sentinel tails `-1/-1` in `[node+0x18/+0x1c]`. Kind `7` is now tighter than a generic queued-effect lane: `simulation_dispatch_runtime_effect_queue_record_by_kind_into_shell_or_world_handlers` `0x00437c00` routes it into `shell_open_region_focus_modal_and_center_world_on_confirmed_region` `0x004c7520`, which opens a callback-driven custom modal and recenters the live world on the chosen region after confirmation. So the current strongest read is a periodic class-0 region picker that queues one region-focus modal record rather than an anonymous scaled event node.","objdump + local disassembly + caller correlation + periodic-maintenance correlation + queued-record correlation + kind-7-modal correlation" +0x00422100,543,world_region_collection_pick_periodic_candidate_region_and_queue_region_focus_modal_record,map,thiscall,inferred,objdump + local disassembly + caller correlation,3,"Periodic region-side selector reached from `simulation_service_periodic_boundary_work` `0x0040a590`. The helper first requires several live world-state gates to be clear, derives one year-sensitive random threshold from selected-year fields `[world+0x05/+0x0d/+0x0f]` plus live world width `[world+0x2155]`, and returns immediately unless one bounded random test passes. On the active path it scans the live region collection twice. The first pass counts eligible class-0 regions whose transient dwords `[region+0x276]` and `[region+0x302]` are clear and which fail the city-connection peer probe `0x00420030(1,1,0,0)`. The second pass picks one random eligible region, derives one small severity bucket from cached scalar `[region+0x25e]`, stores the resulting scaled amount into `[region+0x276]`, and appends one `0x20`-byte queue node through `0x004337c0` with fixed payload `0x005c87a8`, literal kind `7`, zero promotion-latch dword `[node+0x0c]`, the chosen region id `[node+0x10]`, that scaled amount at `[node+0x14]`, and sentinel tails `-1/-1` in `[node+0x18/+0x1c]`. Kind `7` is now tighter than a generic queued-effect lane: `simulation_dispatch_runtime_effect_queue_node_by_kind` `0x00437c00` routes it into `shell_open_region_focus_modal_and_center_world_on_confirmed_region` `0x004c7520`, which opens a callback-driven custom modal and recenters the live world on the chosen region after confirmation. So the current strongest read is a periodic class-0 region picker that queues one region-focus modal record rather than an anonymous scaled event node.","objdump + local disassembly + caller correlation + periodic-maintenance correlation + queued-node correlation + kind-7-modal correlation" 0x00421f50,181,world_region_collection_refresh_position_triplet_for_class0_regions_inside_normalized_rect,map,thiscall,inferred,objdump + local disassembly + caller correlation,2,"Scans the current live region collection for class-0 records whose normalized primary and secondary coordinates fall inside the caller-supplied inclusive float rectangle, then refreshes each qualifying region's current-position triplet through `runtime_object_publish_current_position_triplet_with_height_bias` `0x00455a70(0)`. The helper performs no further aggregation and simply walks the surviving records through the live region collection interfaces `0x00517cf0/0x00518380/0x00518140`. Current grounded caller is the rectangle-side height-support owner `0x0044d410`, where this acts as one dependent region refresh across the updated work window rather than a broader collection rebuild.","objdump + local disassembly + caller correlation + region-rectangle correlation" 0x00421700,32,world_region_collection_remove_entry_by_id_release_record_and_erase_id,map,thiscall,inferred,objdump + local disassembly + collection-layout correlation,2,"Small collection-side removal helper for the live world-region manager. The function resolves the caller-supplied region id through `0x00518140`, releases the resulting record through `world_region_release_profile_collection_clear_linked_structure_site_chain_and_tail_base_cleanup` `0x00420670`, and then removes that id from the current collection through `0x00518a30`. Current evidence grounds this as the collection-owned remove-and-release wrapper for the world-region family rather than a wider rebuild pass.","objdump + local disassembly + collection-layout correlation + region-family correlation" 0x00421730,560,world_region_collection_clear_cell_region_word_and_assign_nearest_region_ids,map,thiscall,inferred,objdump + local disassembly + caller inspection + region-correlation,3,"Region-side world-grid finalizer beneath the default region seeding path. The helper first clears the per-cell region word at `[world+0x212d] + cell*4 + 1` across the full live world raster using dimensions `[world+0x214d/+0x2151]`. It then seeds each live region entry with default bounds and helper fields `[region+0x242/+0x24e/+0x252] = 0` and `[region+0x246/+0x24a] = 0x270f`, and for every class-0 region computes one square neighborhood around the region center from `[region+0x256]`, `0x00455800`, and `0x00455810`. Within that bounded cell rectangle it evaluates one distance-like score through `0x0051dbb0` and writes the current region id `[region+0x23a]` into the per-cell word when the slot is empty or when the current region beats the previously assigned region on the same score. Current grounded callers are `world_region_collection_seed_default_regions` `0x00421b60` and the broader world-build path around `0x004476ec`, so this is the safest current read for clearing and repopulating the world-cell region-id raster rather than a generic collection sweep.","objdump + local disassembly + caller inspection + region-correlation + raster-assignment correlation" @@ -2898,7 +2893,7 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x0051e720,29,support_set_active_debug_log_filename_and_clear_init_flags,support,thiscall,inferred,objdump + caller xrefs + local disassembly + string correlation,3,"Small debug-log filename setter over one caller string. The helper copies up to `0x64` bytes into global buffer `0x006d3ca8`, clears byte `0x006d3d0c`, and clears dword `0x006d3d10`. Current grounded callers are the neighboring bootstrap leaves at `0x0051cff5` and `0x0051d02b`, which seed the default names `rt3_debug.txt` and `language_debug.txt`, so this is the safest current read for the active debug-log filename setter plus init-flag clear.","objdump + caller xrefs + local disassembly + string correlation + debug-log correlation" 0x0051e740,197,support_open_or_create_active_debug_log_and_append_separator_once,support,cdecl,inferred,objdump + caller xrefs + local disassembly + string correlation,3,"Debug-log bootstrap helper over the active global filename at `0x006d3ca8`. The function gates on byte `0x006d3ca0`, uses dword `0x006d3d10` as an in-progress latch, seeds the default filename when needed, formats the path `.\\data\\log\\%s`, opens or creates the target stream through the local file wrapper, appends the fixed separator line `------------------\\n`, closes the stream, and clears the in-progress latch. Current grounded callers are `0x0051cffa` and `0x00521089`, so this is the safest current read for the active debug-log open-or-create plus one-time separator writer.","objdump + caller xrefs + local disassembly + string correlation + debug-log correlation" 0x0051e810,363,shell_mouse_cursor_adjust_showcursor_refcount_and_sync_screen_position_if_live,shell,thiscall,inferred,objdump + caller xrefs + local disassembly + import correlation + source-string correlation,3,"Shared mouse-cursor visibility and position helper beneath the live shell family. The function adjusts global refcount `0x00624b20` up or down from one caller boolean, repeatedly calls `ShowCursor` until the OS cursor visibility count crosses the desired state, and when the live shell bundle exists plus `0x0051f0b0` says the cursor service is active, it derives current screen coordinates from the display tuple via `0x0051f100` and `0x00543c10` and may call `SetCursorPos` when `0x0051f1a0` passes. The grounded source string `\\RT3\\exe\\Src\\Engine\\MouseCursor.cpp` and callers around `0x0053f075`, `0x0053f146`, `0x0053f208`, `0x0053f2af`, `0x0053f480`, `0x0053f492`, and `0x0051e9b7` make this the safest current read for the mouse-cursor ShowCursor-refcount and live-position sync helper.","objdump + caller xrefs + local disassembly + import correlation + source-string correlation + mouse-cursor correlation" -0x0051e980,246,shell_publish_warning_modal_once_and_force_cursor_visible,shell,cdecl,inferred,objdump + caller xrefs + local disassembly + import correlation + string correlation,3,"Guarded warning-modal publisher. The helper uses dword `0x006d3d18` as a reentrancy latch, flips shell byte `[0x006d4024+0x57]` when the live shell bundle exists, forces cursor visibility through `0x0051e810(1, \"\\\\RT3\\\\exe\\\\Src\\\\Engine\\\\Misc.cpp\", 0x110)`, formats the warning body through `0x0051d680`, and shows the shared `MessageBoxA` warning box with localized text and caption ids `0x44/0x45`. Current grounded callers include `0x0040cd70` and many neighboring shell, runtime-object, and setup-warning branches, so this is the safest current read for the shared guarded warning-modal publisher rather than a one-off formatter.","objdump + caller xrefs + local disassembly + import correlation + string correlation + warning-modal correlation" +0x0051e980,246,shell_publish_warning_modal_once_and_force_cursor_visible,shell,cdecl,inferred,objdump + caller xrefs + local disassembly + import correlation + string correlation,3,"Guarded warning-modal publisher. The helper uses dword `0x006d3d18` as a reentrancy latch, flips shell byte `[0x006d4024+0x57]` when the live shell bundle exists, forces cursor visibility through `0x0051e810(1, \""\\RT3\\exe\\Src\\Engine\\Misc.cpp\"", 0x110)`, formats the warning body through `0x0051d680`, and shows the shared `MessageBoxA` warning box with localized text and caption ids `0x44/0x45`. Current grounded callers include `0x0040cd70` and many neighboring shell, runtime-object, and setup-warning branches, so this is the safest current read for the shared guarded warning-modal publisher rather than a one-off formatter.",objdump + caller xrefs + local disassembly + import correlation + string correlation + warning-modal correlation 0x0044bd10,152,world_query_region_category_byte_at_world_coords,map,thiscall,inferred,objdump + caller xrefs + local disassembly + region-raster correlation,3,"Small world-grid lookup helper over the per-cell region word family rooted at `[world+0x212d]`. The function converts caller float coordinates into rounded cell indices through the world-cell scale at `0x005c8568`, clamps those indices to live dimensions `[world+0x214d/+0x2151]`, resolves the current cell record from `[world+0x212d] + cell*4`, and treats the record as a mixed byte-or-word id: when the trailing `u16` lane at `+1` is nonzero it uses that wider id, otherwise it falls back to the first byte. It then resolves the resulting region or city entry through collection `0x0062bae0` and returns entry dword `[entry+0x272]`, with fixed fallback `5` when no entry resolves. Current grounded callers include the structure-side formatter at `0x00403ba7` and the StationPlace world-surface branches at `0x00508b80`, `0x00508d59`, and `0x0050a4e6`, where this helper feeds the current selected-site category latch `0x00622af0`.","objdump + caller xrefs + local disassembly + region-raster correlation + StationPlace category correlation" 0x0044b160,936,world_try_bulldoze_near_coords_and_format_failure_message,map,thiscall,inferred,objdump + caller xrefs + local disassembly + RT3.lng strings,3,"Shared bulldoze-side chooser and commit helper beneath the TrackLay and sibling world-surface bulldoze lanes. The function first clears the caller-owned failure-text buffer and one output slot, validates the supplied company id through the live company collection `0x0062be10`, and on the ordinary non-editor path formats RT3.lng `419` `You can't bulldoze without a company.` when no valid company is available. It then performs two grounded passes over nearby runtime collections around the caller's world coordinates and radius: the early pass estimates or selects the best current bulldoze target while tracking the strongest nearby route-entry distance split, and the later pass performs the actual delete-or-commit branches. The target-family split is now real too. It scans the live route-entry collection `0x006cfca8`, the city-or-region collection `0x0062bae0`, the broader structure collection `0x006cea50`, linked placed structures in `0x0062b26c`, the route-side blocker collection `0x0062ba84`, and the live placed-structure collection `0x006cec20`, repeatedly sampling coordinates through `0x00455800/0x00455810`, `0x0048a1a0/0x0048a1c0`, `0x0047ded0/0x0047df00`, and the radial helper `0x0051db80`. The rejection ladder is now grounded by string ids: mismatched building ownership formats `421` `You can't bulldoze this building - it's owned by another railroad!`, mismatched track ownership formats `422`, nearby rolling-stock blockage formats `423`, and the placed-structure blocker lane localizes the two colliding structure stems through `0x0051c920` before formatting `424` `You can't bulldoze that track - it's too close to a %1. Try bulldozing the %2 first.`. The cost gate is grounded too: on the ordinary company path it reads the company treasury through `company_read_year_or_control_transfer_metric_value` `0x0042a5d0` on stat family `0x2329` mode `0x0d`, formats `420` when the chosen bulldoze cost exceeds available cash, and otherwise commits the cost through `company_apply_bulldoze_or_local_action_cost_scalar` `0x0042a080`. On success the helper dispatches the actual bulldoze branch through the selected owner family: route-entry and route-anchor targets commit through `0x004937f0`, `0x004941a0`, and `0x0048abf0`, while building or linked-site targets commit through the surrounding placed-structure virtual slot `+0x58` and neighboring collection erases. Current grounded direct callers are the TrackLay bulldoze branches at `0x0050d83e` and `0x0050d8fc` plus the neighboring world-surface branches at `0x00473050` and `0x004b8685`, so this is the safest current read for the shared bulldoze chooser-plus-failure-message helper rather than a generic proximity scan.","objdump + caller xrefs + local disassembly + RT3.lng strings + TrackLay bulldoze correlation + rejection-ladder correlation" 0x0044bdb0,107,world_count_neighbor_cells_in_secondary_raster_class_set_2_4_5,map,thiscall,inferred,objdump + caller xrefs + local disassembly + secondary-raster correlation,3,"Eight-neighbor count helper over the secondary-raster class subset owned by `world_secondary_raster_query_cell_class_in_set_2_4_5` `0x00534ec0`. Starting from one caller cell coordinate pair, the helper walks the shared `0x00624b28/0x00624b48` offset tables across the eight immediate neighbors, bounds-checks each neighbor against world dimensions `[world+0x2155/+0x2159]`, and increments the result only when `0x00534ec0` reports that the neighbor cell belongs to class set `2/4/5`. Current grounded callers are the neighboring secondary-raster service branches at `0x0044bf9d`, `0x0044bfc8`, `0x0044c0d5`, `0x0044c348`, and `0x0044c37b`, so this is the safest current read for the local class-subset neighbor counter rather than another generic lattice scan.","objdump + caller xrefs + local disassembly + secondary-raster correlation + 8-neighbor offset-table correlation" @@ -3651,7 +3646,6 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x00435ac0,143,shell_handle_train_detail_lower_action_0x3f9_follow_current_route_object,shell,cdecl,inferred,llvm-objdump + caller correlation,3,"Special-case shell action handler gated to message `0xcb` and control `0x3f9`. It resolves the current train id from `0x0062be44`, validates that train through collection `0x006cfcbc`, resolves its linked route object twice through `train_resolve_linked_route_object` `0x4a77b0`, clears one auxiliary world-side latch through `0x52d160`, recenters the world view on that linked route object through `0x433900`, and then re-enters `shell_detail_panel_transition_manager` `0x4ddbd0` with detail-manager mode `2` and the current train id. The helper returns `1` only for nonmatching messages or controls; the grounded success path is the lower-action follow-current-route branch above the already-mapped train detail owners.","llvm-objdump + caller correlation + train-route-object correlation + shell-transition correlation" 0x00436070,24,shell_refresh_active_window_followons_without_subject_gate,shell,cdecl,inferred,llvm-objdump + callee correlation,3,"No-argument shell follow-on refresh strip keyed off the current active detail-manager mode at `[0x006d0818+0x8c]`. It refreshes the company-detail branch when mode `7` is active through `0x4c6c30`, the stock-buy branch when mode `0x0b` is active through `0x50c130`, and the `Overview.win` side when mode `9` is active through `0x452f20` with the fixed `0x270f` bounds. Unlike its argument-sensitive sibling `shell_refresh_active_window_followons_for_subject_and_optional_company_match` `0x4360d0`, this helper has no mode-`8` subject-id gate.","llvm-objdump + callee correlation + detail-manager-mode correlation" 0x004360d0,39,shell_refresh_active_window_followons_for_subject_and_optional_company_match,shell,cdecl,inferred,llvm-objdump + callee correlation,3,"Small shell follow-on refresh helper keyed off the current active detail-manager mode at `[0x006d0818+0x8c]`. It first refreshes the live company-detail branch when mode `7` is active through `0x4c6c30`, then refreshes the stock-buy branch when mode `0x0b` is active through `0x50c130`. For active mode `8` it also requires that the current subject id at `[state+0x90]` matches the caller dword before it triggers the world-side timed overlay refresh through `0x482150`. Finally, when mode `9` is active it refreshes the `Overview.win` side through `0x452f20` with the fixed `0x270f` bounds. This is the argument-sensitive sibling of the smaller no-arg follow-on strip at `0x436070`.","llvm-objdump + callee correlation + detail-manager-mode correlation" -0x00436170,24,shell_refresh_active_window_followons_and_adjacent_station_or_company_lists,shell,cdecl,inferred,llvm-objdump + callee correlation,3,"Broader shell follow-on wrapper above `shell_refresh_active_window_followons_for_subject_and_optional_company_match` `0x4360d0`. It first forwards the caller subject id into `0x4360d0`, then adds two adjacent list-window refresh branches keyed by the current active detail-manager mode: when mode `4` is active and singleton `0x006d1708` is live it refreshes the station list panel through `0x506f30`, and when mode `1` is active and singleton `0x006d3b34` is live it refreshes the company-list side through `0x5158f0(-1)`. This is the current grounded owner for the active-window follow-on sweep that spans company detail, station list, stock buy, overview, and the mode-`8` subject-gated overlay branch.","llvm-objdump + callee correlation + detail-manager-mode correlation + shell-singleton correlation" 0x00436170,24,shell_refresh_active_window_followons_and_adjacent_station_or_train_lists,shell,cdecl,inferred,llvm-objdump + callee correlation,3,"Broader shell follow-on wrapper above `shell_refresh_active_window_followons_for_subject_and_optional_company_match` `0x4360d0`. It first forwards the caller subject id into `0x4360d0`, then adds two adjacent list-window refresh branches keyed by the current active detail-manager mode: when mode `4` is active and singleton `0x006d1708` is live it refreshes the station list panel through `0x506f30`, and when mode `1` is active and singleton `0x006d3b34` is live it refreshes the train-list side through `shell_train_list_window_refresh_controls` `0x005158f0(-1)`. This is the current grounded owner for the active-window follow-on sweep that spans company detail, station list, train list, stock buy, overview, and the mode-`8` subject-gated overlay branch.","llvm-objdump + callee correlation + detail-manager-mode correlation + shell-singleton correlation + TrainList.win string correlation" 0x004361d0,120,world_refresh_collection_side_effects_after_broad_state_change,map,thiscall,inferred,llvm-objdump + caller correlation,3,"Shared broad refresh sweep over several live world collections after larger state changes. It walks the region collection at `0x006cfca8`, first re-entering `0x48b660` and then `0x48a9e0` on every region record; walks the placed-structure collection at `0x0062b26c` and refreshes each entry through `placed_structure_refresh_linked_peer_overlay_runtime` `0x40d2d0`; and then walks three more live collections rooted at `0x006ada80`, `0x0062bae0`, and `0x006cea50`, invoking each record's vtable slot `+0x54` with argument `0`. Current grounded callers include the broader world-side owner at `0x44b160` and the TrackLay teardown branch at `0x50dba7`, so this is the safest current read for a shared collection-side post-change refresh sweep rather than a subsystem-local helper.","llvm-objdump + caller correlation + collection-sweep correlation + vtable-refresh correlation" 0x00436350,576,simulation_service_world_outcome_mode_prompt_and_transition_effects,simulation,thiscall,inferred,llvm-objdump + caller correlation + outcome-latch correlation,3,"Frame-serviced owner for the live world outcome-mode latch at `[world+0x4a73]`. The helper immediately stamps `[world+0x4a77] = 1`, maps the current mode band `1..4` to selector values `0..3` for the shared helper `0x517a80`, and then runs a larger presentation/transition split keyed by scenario bytes `[0x006cec7c+0xc4/+0xc5/+0x7d]` plus the current mode. One early branch closes the active detail panel through `0x4ddbd0(-1, 0)`, clears one world-view scalar at `[0x006cec74+0x14]+0x110`, arms shell latch `0x006d4000`, and triggers the fixed world-side transition path through `0x482150(..., 0xcc, 0, 3, 0, 0)`. The broader common branch refreshes two presentation scalars through `0x531560`, re-enters `0x4e1af0` with the outcome-mode-derived selector, and then opens one callback-driven shell modal rooted at localized id `0x169` through `0x5193f0 -> 0x4c98a0`. On modal dismissal or fallback it can close the current detail panel through `0x4ddbd0`, tear down the live multiplayer session object at `0x006cd8d8`, trigger the alternate world-side transition through `0x482150` with selector `7` or `2` depending on `[0x006cec7c+0xc5]`, and refresh the same two presentation scalars again. Current grounded caller is the simulation frame cadence `simulation_frame_accumulate_and_step_world` `0x439140`, which reaches this owner only after the shell-window dirty-mark ladder when the outcome latch is live and the completion latch `[world+0x4a77]` is still clear. This is therefore the safest current owner for outcome-mode prompt and transition servicing above the already-grounded outcome text buffer `[world+0x4b47]`, not just another shell modal wrapper.","llvm-objdump + caller correlation + outcome-latch correlation + shell-modal correlation + transition-side correlation" @@ -3893,11 +3887,11 @@ address,size,name,subsystem,calling_convention,prototype_status,source_tool,conf 0x005b07d7,50,math_apply_abstract_floating_control_value_and_mask,support,cdecl,inferred,llvm-objdump + local disassembly + caller correlation,3,"Primary older CRT floating-control owner beneath `math_apply_floating_point_control_word_value_and_mask_with_bit0x80000_cleared` `0x005b0809`. The helper snapshots the current x87 control word with `fstcw`, maps it into the abstract flag space through `0x005b06b7`, merges the caller value and mask there, converts the merged result back through `0x005b0749`, loads the new control word with `fldcw`, and returns the previous abstract control setting. Current grounded callers are `0x005b0809` and the legacy bootstrap wrapper `0x005a63b8`.","llvm-objdump + local disassembly + caller correlation + x87-control correlation" 0x005b08ba,245,math_build_matherr_report_record_and_dispatch_default_runtime_message,support,cdecl,inferred,llvm-objdump + local disassembly + caller correlation,2,"Broader older-CRT math-report owner beneath the x87 wrappers `0x005a77f7` and `0x005a790a`. The helper reads the staged math-error record, selects one operand-format width from record kind `[record+0x00]`, formats the operand payload through `0x005a8af0` and `0x005a884b`, updates the floating-point report mode through `0x005a9070`, and then either consults the default matherr-style hook stub `0x005b09b8` or falls through directly to the default runtime-message path `0x005a8d12`. This is the safest current read for the common matherr-report formatter and dispatcher rather than overclaiming one single formatting helper.","llvm-objdump + local disassembly + caller correlation + math-error-report correlation" 0x005b09b8,3,math_default_matherr_hook_stub_return_zero,support,cdecl,inferred,llvm-objdump + local disassembly + caller correlation,1,"Trivial default hook stub in the older CRT math-report seam. The function just returns `0`, and current grounded caller `0x005b08ba` uses that zero result before falling through to the default runtime-message path.","llvm-objdump + local disassembly + caller correlation + math-error-report correlation" -0x00437c00,237,simulation_dispatch_runtime_effect_queue_record_by_kind_into_shell_or_world_handlers,simulation,thiscall,inferred,llvm-objdump + local disassembly + caller correlation + queue-family correlation,4,"Kind-dispatch owner for one queued runtime-effect or queue-record node. The helper switches on dword `[node+0x08]`, and the jump table is now resolved exactly. Kinds `1` and `2` are no-op success arms that return `1` immediately. Kinds `0` and `4` both open the fixed callback-driven custom modal through `shell_open_custom_modal_dialog_with_callbacks` `0x004c98a0`, using localized id `[node+0x04]` plus the standard all-zero payload and `-1.0f` scalar pair. Kind `3` forwards `[node+0x10]` plus `[node+0x14]` into `0x004dc540` and returns `0`. Kind `5` dispatches `[node+0x10]` into `0x004f29c0` with `dl=1`. Kind `6` first validates route-entry id `[node+0x10]` against collection `0x006cfcbc` and then forwards that id plus `[node+0x04]` into `0x00436820`, with failed validation falling back to the fixed modal arm. Kind `7` forwards `[node+0x10]` into `0x004c7520`. Kind `8` forwards `[node+0x10]` plus `[node+0x14]` into `0x004f2d80`. Unsupported kinds also fall back to the fixed modal arm. Current grounded callers are `simulation_frame_service_runtime_effect_queue_and_mark_custom_modal_dirty_if_needed` `0x00438710` and `simulation_frame_dispatch_active_runtime_effect_queue_record_or_open_fixed_fallback_modal_0x153` `0x00438840`, so this is now the real queued-record kind table rather than one loose arm list.","llvm-objdump + local disassembly + caller correlation + queue-family correlation + resolved-jump-table correlation + route-entry-lookup correlation + custom-modal correlation" -0x00438710,300,simulation_frame_service_runtime_effect_queue_and_mark_custom_modal_dirty_if_needed,simulation,thiscall,inferred,llvm-objdump + local disassembly + caller correlation + queue-family correlation,4,"Recurring service owner for the linked `0x20`-byte runtime-effect or queue-record family rooted at `[this+0x66a6]`, above the older append helper `scenario_state_append_runtime_effect_or_queue_record` `0x004337c0`. When a live custom modal already exists through `shell_has_live_custom_modal_dialog` `0x004c8680`, the helper first walks the queued-record list looking for any node whose promotion-latch dword `[node+0x0c]` equals `1`; when such a node exists and shell latch `0x0062be80` is already live it takes the short path through `shell_mark_custom_modal_dialog_dirty` `0x004c8670`. Otherwise it enters a recursion guard at `0x0062be3c`, samples the broader shell-block gate through `0x004348e0`, walks the queued-record list at `[this+0x66a6]`, and for each node either accepts it immediately when the shell gate is open or the node kind plus live shell state allows direct promotion, or falls back to node-specific handlers through `0x00437c00`. Accepted nodes are either unlinked and preserved as the new active queue pointer at `[this+0x66aa]`, or replace the previously active node there after releasing the earlier owned payloads through `0x0053afd0`; rejected nodes are released through the same payload helper before the walk continues. Current grounded callers are the frame-owned outcome follow-on branch at `0x0043963d` and the earlier world-side notification path at `0x00432cb0`, where this owner runs only after a stricter local gate. This is therefore the safest current read for the queue-record service and custom-modal dirty-mark owner above `[this+0x66a6]` and `[this+0x66aa]`, with the key short-path gate now tied to `[node+0x0c]` rather than the queue kind field itself.","llvm-objdump + local disassembly + caller correlation + queue-family correlation + custom-modal correlation + active-node-slot correlation + promotion-latch correlation" -0x0043963d,236,simulation_frame_service_outcome_followon_windows_overview_and_pause_toggle_after_transition,simulation,thiscall,inferred,llvm-objdump + local disassembly + callee correlation,3,"Post-outcome follow-on branch inside `simulation_frame_accumulate_and_step_world` `0x00439140`, reached only when the current outcome-sequence marker `[world+0x4a7b]` still matches the latched step-local marker. The branch first services the queued runtime-effect or queue-record family rooted at `[world+0x66a6]` through `simulation_frame_service_runtime_effect_queue_and_mark_custom_modal_dirty_if_needed` `0x00438710`, then conditionally opens `Overview.win` through `shell_open_or_focus_overview_window` `0x004f3a10` when no overview window is already live, the shell is not blocked by mode-`0x0c` plus input bit `0x400`, and the preview fixed-record collection at `0x006cea4c` still has one admissible entry through `0x00473e20/0x00473e70`. After that it runs a second follow-on gate for `LoadScreen.win`: when no live overview or ledger is open, shell latch `0x006d4000` is clear, and world flag `[this+0x4d]` is still nonzero, it clears that flag, rechecks the same overview-entry gate, and then opens or focuses `LoadScreen.win` page `0` through `shell_open_or_focus_load_screen_page` `0x004e4ee0`. The same branch also conditionally toggles world pause or resume through `world_toggle_pause_or_restore_game_speed` `0x00437a60` when no multiplayer session object is live at `0x006cd8d8`, before falling back into `world_view_service_shell_input_pan_and_hover` `0x0043db00` at the end of the frame. This is therefore the safest current owner for the post-transition outcome follow-on window and pause strip, not just another loose tail inside the frame cadence.","llvm-objdump + local disassembly + callee correlation + outcome-latch correlation + overview-correlation + loadscreen-correlation + pause-toggle-correlation" -0x00438840,76,simulation_frame_dispatch_active_runtime_effect_queue_record_or_open_fixed_fallback_modal_0x153,simulation,thiscall,inferred,llvm-objdump + caller correlation + queue-family correlation,3,"Small owner beside the queued runtime-effect record family. When the active queue slot `[this+0x66aa]` is nonnull it forwards that node into the same node-specific handler table at `0x00437c00` used by `simulation_frame_service_runtime_effect_queue_and_mark_custom_modal_dirty_if_needed` `0x00438710`. When no active node is staged it instead opens one fixed callback-driven custom modal rooted at localized id `0x153` through `0x5193f0 -> 0x4c98a0`, with the same all-zero option payload and default `-1.0f` scalar pair every time. Current grounded caller is the small shell-side branch at `0x004414d0`, which only reaches this owner when the live world object exists and the active shell presentation stack is otherwise idle. This is therefore the safest current read for the queued-record dispatch-or-fallback prompt owner above `[this+0x66aa]`, not a generic modal opener.","llvm-objdump + caller correlation + queue-family correlation + custom-modal correlation" -0x004414d0,33,shell_try_dispatch_active_runtime_effect_queue_record_prompt_when_shell_stack_idle,shell,cdecl,inferred,llvm-objdump + caller correlation + queue-family correlation,3,"Small shell-side gate above `simulation_frame_dispatch_active_runtime_effect_queue_record_or_open_fixed_fallback_modal_0x153` `0x00438840`. It first requires a live world object at `0x006cec78`, then requires the current shell presentation stack depth `[0x006d401c+0xc64]` to be nonpositive, and only then forwards the live world object into `0x00438840`. This is therefore the idle-shell dispatcher for the queued-record prompt family.","llvm-objdump + caller correlation + queue-family correlation + shell-stack-idle correlation" +0x00437c00,237,simulation_dispatch_runtime_effect_queue_node_by_kind,simulation,thiscall,inferred,llvm-objdump + local disassembly + caller correlation + queue-family correlation,4,"Kind-dispatch owner for one queued runtime-effect queue node. The helper switches on dword `[node+0x08]`, and the jump table is now resolved exactly. Kinds `1` and `2` are no-op success arms that return `1` immediately. Kinds `0` and `4` both open the fixed callback-driven custom modal through `shell_open_custom_modal_dialog_with_callbacks` `0x004c98a0`, using localized id `[node+0x04]` plus the standard all-zero payload and `-1.0f` scalar pair. Kind `3` forwards `[node+0x10]` plus `[node+0x14]` into `0x004dc540` and returns `0`. Kind `5` dispatches `[node+0x10]` into `0x004f29c0` with `dl=1`. Kind `6` first validates route-entry id `[node+0x10]` against collection `0x006cfcbc` and then forwards that id plus `[node+0x04]` into `0x00436820`, with failed validation falling back to the fixed modal arm. Kind `7` forwards `[node+0x10]` into `0x004c7520`. Kind `8` forwards `[node+0x10]` plus `[node+0x14]` into `0x004f2d80`. Unsupported kinds also fall back to the fixed modal arm. Current grounded callers are `simulation_frame_service_runtime_effect_queue` `0x00438710` and `simulation_frame_dispatch_active_runtime_effect_queue_node_or_open_fixed_fallback_modal` `0x00438840`, so this is now the real queue-node kind table rather than one loose arm list.","llvm-objdump + local disassembly + caller correlation + queue-family correlation + resolved-jump-table correlation + route-entry-lookup correlation + custom-modal correlation" +0x00438710,300,simulation_frame_service_runtime_effect_queue,simulation,thiscall,inferred,llvm-objdump + local disassembly + caller correlation + queue-family correlation,4,"Recurring service owner for the linked `0x20`-byte runtime-effect queue family rooted at `[this+0x66a6]`, above the older append helper `scenario_state_append_runtime_effect_queue_node` `0x004337c0`. When a live custom modal already exists through `shell_has_live_custom_modal_dialog` `0x004c8680`, the helper first walks the queued-node list looking for any node whose promotion-latch dword `[node+0x0c]` equals `1`; when such a node exists and shell latch `0x0062be80` is already live it takes the short path through `shell_mark_custom_modal_dialog_dirty` `0x004c8670`. Otherwise it enters a recursion guard at `0x0062be3c`, samples the broader shell-block gate through `0x004348e0`, walks the queued-node list at `[this+0x66a6]`, and for each node either accepts it immediately when the shell gate is open or the node kind plus live shell state allows direct promotion, or falls back to node-specific handlers through `0x00437c00`. Accepted nodes are either unlinked and preserved as the new active queue pointer at `[this+0x66aa]`, or replace the previously active node there after releasing the earlier owned payloads through `0x0053afd0`; rejected nodes are released through the same payload helper before the walk continues. Current grounded callers are the frame-owned outcome follow-on branch at `0x0043963d` and the earlier world-side notification path at `0x00432cb0`, where this owner runs only after a stricter local gate. This is therefore the safest current read for the queue service owner above `[this+0x66a6]` and `[this+0x66aa]`, with the key short-path gate now tied to `[node+0x0c]` rather than the queue kind field itself.","llvm-objdump + local disassembly + caller correlation + queue-family correlation + custom-modal correlation + active-node-slot correlation + promotion-latch correlation" +0x0043963d,236,simulation_frame_service_outcome_followon_windows_overview_and_pause_toggle_after_transition,simulation,thiscall,inferred,llvm-objdump + local disassembly + callee correlation,3,"Post-outcome follow-on branch inside `simulation_frame_accumulate_and_step_world` `0x00439140`, reached only when the current outcome-sequence marker `[world+0x4a7b]` still matches the latched step-local marker. The branch first services the queued runtime-effect queue family rooted at `[world+0x66a6]` through `simulation_frame_service_runtime_effect_queue` `0x00438710`, then conditionally opens `Overview.win` through `shell_open_or_focus_overview_window` `0x004f3a10` when no overview window is already live, the shell is not blocked by mode-`0x0c` plus input bit `0x400`, and the preview fixed-record collection at `0x006cea4c` still has one admissible entry through `0x00473e20/0x00473e70`. After that it runs a second follow-on gate for `LoadScreen.win`: when no live overview or ledger is open, shell latch `0x006d4000` is clear, and world flag `[this+0x4d]` is still nonzero, it clears that flag, rechecks the same overview-entry gate, and then opens or focuses `LoadScreen.win` page `0` through `shell_open_or_focus_load_screen_page` `0x004e4ee0`. The same branch also conditionally toggles world pause or resume through `world_toggle_pause_or_restore_game_speed` `0x00437a60` when no multiplayer session object is live at `0x006cd8d8`, before falling back into `world_view_service_shell_input_pan_and_hover` `0x0043db00` at the end of the frame. This is therefore the safest current owner for the post-transition outcome follow-on window and pause strip, not just another loose tail inside the frame cadence.","llvm-objdump + local disassembly + callee correlation + outcome-latch correlation + overview-correlation + loadscreen-correlation + pause-toggle-correlation" +0x00438840,76,simulation_frame_dispatch_active_runtime_effect_queue_node_or_open_fixed_fallback_modal,simulation,thiscall,inferred,llvm-objdump + caller correlation + queue-family correlation,3,"Small owner beside the queued runtime-effect queue family. When the active queue slot `[this+0x66aa]` is nonnull it forwards that node into the same node-specific handler table at `0x00437c00` used by `simulation_frame_service_runtime_effect_queue` `0x00438710`. When no active node is staged it instead opens one fixed callback-driven custom modal rooted at localized id `0x153` through `0x5193f0 -> 0x4c98a0`, with the same all-zero option payload and default `-1.0f` scalar pair every time. Current grounded caller is the small shell-side branch at `0x004414d0`, which only reaches this owner when the live world object exists and the active shell presentation stack is otherwise idle. This is therefore the safest current read for the active-queue-node dispatch-or-fallback prompt owner above `[this+0x66aa]`, not a generic modal opener.","llvm-objdump + caller correlation + queue-family correlation + custom-modal correlation" +0x004414d0,33,shell_try_dispatch_active_runtime_effect_queue_prompt_when_shell_stack_idle,shell,cdecl,inferred,llvm-objdump + caller correlation + queue-family correlation,3,"Small shell-side gate above `simulation_frame_dispatch_active_runtime_effect_queue_node_or_open_fixed_fallback_modal` `0x00438840`. It first requires a live world object at `0x006cec78`, then requires the current shell presentation stack depth `[0x006d401c+0xc64]` to be nonpositive, and only then forwards the live world object into `0x00438840`. This is therefore the idle-shell dispatcher for the active-queue prompt family.","llvm-objdump + caller correlation + queue-family correlation + shell-stack-idle correlation" 0x00413f80,681,aux_candidate_collection_restore_temp_record_bank_and_queue_keys_from_packed_state,map,thiscall,inferred,objdump + local disassembly + caller correlation + packed-state correlation,4,"Restore-side packed-state loader for the auxiliary or source record family rooted at `0x0062b2fc`. The helper first reads one `0x0c`-byte header from the caller stream, stores the queued-record count into `0x0062ba64`, allocates the queued-key array at `0x0062ba6c/0x0062ba70`, frees any prior temporary record bank at `0x0062b2f8`, allocates a fresh bank sized as `count * 0x74e`, and then walks every temporary record image in that bank. For each image it reads the queued key, the fixed scalar header at `[record+0x00/+0x04/+0x08/+0x0c/+0x4b8/+0x4bc]`, the counted inline dword run rooted at `[record+0x10]`, the fixed `0x10`-byte payload at `[record+0x498]`, the optional counted byte payload rooted at `[record+0x4c0]`, the counted dword run rooted at `[record+0x4c8]`, the paired one-byte streams rooted at `[record+0x65c]` and `[record+0x6c1]`, and the trailing dword at `[record+0x72a]`. On exit it seeds queue index `0x0062ba74 = 0` and stores the restored record count into `0x0062ba68`. Current grounded caller is the world-entry restore branch at `0x004443ed`, immediately before the optional live reimport through `0x0041a950`, so this is the safest current read for the packed-state restore of the temporary aux-candidate record bank plus queued keys rather than a generic bundle reader.","objdump + local disassembly + caller correlation + packed-state correlation + temp-record-bank correlation" 0x0041a950,55,aux_candidate_collection_release_live_entries_then_reimport_records_and_refresh_runtime_followons,map,thiscall,inferred,objdump + local disassembly + caller correlation + restore-path correlation,3,"Restore-side sibling over the auxiliary or source record pool rooted at `0x0062b2fc`. When the collection currently holds live entries, the helper walks every live id through `0x00518380` and dispatches entry vtable slot `+0x08` to release per-entry state, then tail-jumps directly back into `aux_candidate_collection_construct_stream_load_records_and_refresh_runtime_followons` `0x004196c0`. Current grounded caller is the world-entry restore branch at `0x0044440f`, which uses it only when the shell-side restore flags demand a live reimport after `aux_candidate_collection_restore_temp_record_bank_and_queue_keys_from_packed_state` `0x00413f80`, so this is the safest current read for the in-place aux-candidate reset-and-reimport sibling rather than a generic collection destructor.","objdump + local disassembly + caller correlation + restore-path correlation + entry-release correlation" 0x004931e0,186,route_entry_collection_run_optional_refresh_hooks_and_validate_world_cell_side_lists,map,thiscall,inferred,llvm-objdump + local disassembly + caller correlation,3,"Optional refresh owner over the live route-entry collection `0x006cfca8`. When shell gate `[0x006cec74+0x1cb]` is nonzero, the helper first walks every live route-entry record and dispatches the current per-entry hook `0x0048a780`, which is presently a no-op. It then scans the world-grid dimensions stored at `[0x0062c120+0x2155/+0x2159]`, resolves each cell-side route-entry side-list through `0x00492130`, and validates every linked route-entry id in those per-cell lists against the live collection through `0x00517d40`. Current grounded callers are the zero-depth optional refresh dispatcher `scenario_state_run_optional_collection_refresh_hooks_when_mutation_depth_zero` `0x00433b80` and the city-connection route-builder family at `0x00402ccf` and `0x0040461d`, so this is the safest current read for the route-entry collection's optional refresh-and-validation owner rather than a narrower in-place mutator.","llvm-objdump + local disassembly + caller correlation + route-entry-collection correlation + world-cell-side-list correlation" diff --git a/artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.dot b/artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.dot index 369a4d1..f131bfa 100644 --- a/artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.dot +++ b/artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.dot @@ -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"]; diff --git a/artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.md b/artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.md index b2bffd4..bfd6e5d 100644 --- a/artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.md +++ b/artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.md @@ -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` diff --git a/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-forward-subgraph.dot b/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-forward-subgraph.dot index 91038c7..0d7e499 100644 --- a/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-forward-subgraph.dot +++ b/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-forward-subgraph.dot @@ -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"]; diff --git a/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-forward-subgraph.md b/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-forward-subgraph.md index cc70fd5..e2c5f06 100644 --- a/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-forward-subgraph.md +++ b/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-forward-subgraph.md @@ -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` diff --git a/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-subgraph.dot b/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-subgraph.dot index ea2a5e3..feafa26 100644 --- a/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-subgraph.dot +++ b/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-subgraph.dot @@ -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"]; diff --git a/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-subgraph.md b/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-subgraph.md index f4ead75..7c298d3 100644 --- a/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-subgraph.md +++ b/artifacts/exports/rt3-1.06/runtime-effect-service-depth7-subgraph.md @@ -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` diff --git a/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-forward-subgraph.dot b/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-forward-subgraph.dot index 8dcc0a4..cfc82c0 100644 --- a/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-forward-subgraph.dot +++ b/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-forward-subgraph.dot @@ -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"]; diff --git a/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-forward-subgraph.md b/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-forward-subgraph.md index bec314f..72ca19d 100644 --- a/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-forward-subgraph.md +++ b/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-forward-subgraph.md @@ -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` diff --git a/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-subgraph.dot b/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-subgraph.dot index c6cdee6..4904bf4 100644 --- a/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-subgraph.dot +++ b/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-subgraph.dot @@ -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"]; diff --git a/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-subgraph.md b/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-subgraph.md index b49a246..ab5a5a0 100644 --- a/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-subgraph.md +++ b/artifacts/exports/rt3-1.06/setup-window-submodes-depth5-subgraph.md @@ -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` diff --git a/artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.dot b/artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.dot index f82e20a..869e6cb 100644 --- a/artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.dot +++ b/artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.dot @@ -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"]; diff --git a/artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.md b/artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.md index 55f59c3..aa42a57 100644 --- a/artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.md +++ b/artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.md @@ -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` diff --git a/crates/rrt-cli/src/app/command/finance.rs b/crates/rrt-cli/src/app/command/finance.rs new file mode 100644 index 0000000..71f651b --- /dev/null +++ b/crates/rrt-cli/src/app/command/finance.rs @@ -0,0 +1,20 @@ +use super::{Command, FinanceCommand, usage_error}; + +pub(super) fn parse_finance_command( + args: &[String], +) -> Result> { + 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(), + } +} diff --git a/crates/rrt-cli/src/app/command/mod.rs b/crates/rrt-cli/src/app/command/mod.rs new file mode 100644 index 0000000..e138d87 --- /dev/null +++ b/crates/rrt-cli/src/app/command/mod.rs @@ -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 | finance diff | runtime validate-fixture | runtime summarize-fixture | runtime export-fixture-state | runtime diff-state | runtime summarize-state | runtime snapshot-state | runtime inspect-smp | runtime inspect-candidate-table | runtime inspect-compact-event-dispatch-cluster | runtime inspect-compact-event-dispatch-cluster-counts | runtime inspect-map-title-hints | runtime summarize-save-load | runtime load-save-slice | runtime inspect-save-company-chairman | runtime inspect-save-placed-structure-triplets | runtime compare-region-fixed-row-runs | runtime inspect-periodic-company-service-trace | runtime inspect-region-service-trace | runtime inspect-infrastructure-asset-trace | runtime inspect-save-region-queued-notice-records | runtime inspect-placed-structure-dynamic-side-buffer | runtime inspect-unclassified-save-collections | runtime snapshot-save-state | runtime export-save-slice | runtime export-overlay-import | runtime inspect-pk4 | runtime inspect-cargo-types | runtime inspect-building-type-sources [building-bindings.json] | runtime inspect-cargo-skins | runtime inspect-cargo-economy-sources | runtime inspect-cargo-production-selector | runtime inspect-cargo-price-selector | runtime inspect-win | runtime extract-pk4-entry | runtime inspect-campaign-exe | runtime compare-classic-profile [saveN.gms...] | runtime compare-105-profile [saveN.gms...] | runtime compare-candidate-table [fileN...] | runtime compare-recipe-book-lines [fileN...] | runtime compare-setup-payload-core [fileN...] | runtime compare-setup-launch-payload [fileN...] | runtime compare-post-special-conditions-scalars [fileN...] | runtime scan-candidate-table-headers | runtime scan-candidate-table-named-runs | runtime scan-special-conditions | runtime scan-aligned-runtime-rule-band | runtime scan-post-special-conditions-scalars | runtime scan-post-special-conditions-tail | runtime scan-recipe-book-lines | runtime export-profile-block ]"; + +pub(super) fn parse_command() -> Result> { + let args: Vec = 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> { + 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() -> Result> { + 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::>(), + 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"), + })) + ); + } +} diff --git a/crates/rrt-cli/src/app/command/model.rs b/crates/rrt-cli/src/app/command/model.rs new file mode 100644 index 0000000..27a25fa --- /dev/null +++ b/crates/rrt-cli/src/app/command/model.rs @@ -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, + }, + 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, + }, + CompareRt3105Profile { + smp_paths: Vec, + }, + CompareCandidateTable { + smp_paths: Vec, + }, + CompareRecipeBookLines { + smp_paths: Vec, + }, + CompareSetupPayloadCore { + smp_paths: Vec, + }, + CompareSetupLaunchPayload { + smp_paths: Vec, + }, + ComparePostSpecialConditionsScalars { + smp_paths: Vec, + }, +} + +#[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 }, +} diff --git a/crates/rrt-cli/src/app/command/runtime/compare.rs b/crates/rrt-cli/src/app/command/runtime/compare.rs new file mode 100644 index 0000000..6ec19e5 --- /dev/null +++ b/crates/rrt-cli/src/app/command/runtime/compare.rs @@ -0,0 +1,66 @@ +use std::path::PathBuf; + +use super::super::{CompareCommand, usage_error}; + +pub(super) fn parse_compare_command( + args: &[String], +) -> Result> { + 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(), + } +} diff --git a/crates/rrt-cli/src/app/command/runtime/fixture_state.rs b/crates/rrt-cli/src/app/command/runtime/fixture_state.rs new file mode 100644 index 0000000..87396d6 --- /dev/null +++ b/crates/rrt-cli/src/app/command/runtime/fixture_state.rs @@ -0,0 +1,73 @@ +use super::super::{FixtureStateCommand, usage_error}; + +pub(super) fn parse_fixture_state_command( + args: &[String], +) -> Result> { + 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(), + } +} diff --git a/crates/rrt-cli/src/app/command/runtime/inspect.rs b/crates/rrt-cli/src/app/command/runtime/inspect.rs new file mode 100644 index 0000000..00e2641 --- /dev/null +++ b/crates/rrt-cli/src/app/command/runtime/inspect.rs @@ -0,0 +1,146 @@ +use super::super::{InspectCommand, usage_error}; + +pub(super) fn parse_inspect_command( + args: &[String], +) -> Result> { + 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(), + } +} diff --git a/crates/rrt-cli/src/app/command/runtime/mod.rs b/crates/rrt-cli/src/app/command/runtime/mod.rs new file mode 100644 index 0000000..6a53dbd --- /dev/null +++ b/crates/rrt-cli/src/app/command/runtime/mod.rs @@ -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> { + 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)) +} diff --git a/crates/rrt-cli/src/app/command/runtime/scan.rs b/crates/rrt-cli/src/app/command/runtime/scan.rs new file mode 100644 index 0000000..ae19b1d --- /dev/null +++ b/crates/rrt-cli/src/app/command/runtime/scan.rs @@ -0,0 +1,44 @@ +use super::super::{ScanCommand, usage_error}; + +pub(super) fn parse_scan_command( + args: &[String], +) -> Result> { + 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(), + } +} diff --git a/crates/rrt-cli/src/app/command/validate.rs b/crates/rrt-cli/src/app/command/validate.rs new file mode 100644 index 0000000..4f439ab --- /dev/null +++ b/crates/rrt-cli/src/app/command/validate.rs @@ -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> { + match args { + [] => Ok(Command::Validate { + repo_root: current_dir.to_path_buf(), + }), + [repo_root] => Ok(Command::Validate { + repo_root: repo_root.into(), + }), + _ => usage_error(), + } +} diff --git a/crates/rrt-cli/src/app/dispatch/finance.rs b/crates/rrt-cli/src/app/dispatch/finance.rs new file mode 100644 index 0000000..5af2084 --- /dev/null +++ b/crates/rrt-cli/src/app/dispatch/finance.rs @@ -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> { + match command { + FinanceCommand::Eval { snapshot_path } => run_finance_eval(&snapshot_path), + FinanceCommand::Diff { + left_path, + right_path, + } => run_finance_diff(&left_path, &right_path), + } +} diff --git a/crates/rrt-cli/src/app/dispatch/mod.rs b/crates/rrt-cli/src/app/dispatch/mod.rs new file mode 100644 index 0000000..60c0317 --- /dev/null +++ b/crates/rrt-cli/src/app/dispatch/mod.rs @@ -0,0 +1,81 @@ +mod finance; +mod runtime; +mod validate; + +use crate::app::command::{Command, parse_command}; + +pub(super) fn run() -> Result<(), Box> { + dispatch_command(parse_command()?) +} + +fn dispatch_command(command: Command) -> Result<(), Box> { + 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"); + } +} diff --git a/crates/rrt-cli/src/app/dispatch/runtime/compare.rs b/crates/rrt-cli/src/app/dispatch/runtime/compare.rs new file mode 100644 index 0000000..52fc632 --- /dev/null +++ b/crates/rrt-cli/src/app/dispatch/runtime/compare.rs @@ -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> { + 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) + } + } +} diff --git a/crates/rrt-cli/src/app/dispatch/runtime/fixture_state.rs b/crates/rrt-cli/src/app/dispatch/runtime/fixture_state.rs new file mode 100644 index 0000000..d0996c8 --- /dev/null +++ b/crates/rrt-cli/src/app/dispatch/runtime/fixture_state.rs @@ -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> { + 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), + } +} diff --git a/crates/rrt-cli/src/app/dispatch/runtime/inspect.rs b/crates/rrt-cli/src/app/dispatch/runtime/inspect.rs new file mode 100644 index 0000000..2baeade --- /dev/null +++ b/crates/rrt-cli/src/app/dispatch/runtime/inspect.rs @@ -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> { + 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), + } +} diff --git a/crates/rrt-cli/src/app/dispatch/runtime/mod.rs b/crates/rrt-cli/src/app/dispatch/runtime/mod.rs new file mode 100644 index 0000000..213be8b --- /dev/null +++ b/crates/rrt-cli/src/app/dispatch/runtime/mod.rs @@ -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> { + 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), + } +} diff --git a/crates/rrt-cli/src/app/dispatch/runtime/scan.rs b/crates/rrt-cli/src/app/dispatch/runtime/scan.rs new file mode 100644 index 0000000..dfa4a23 --- /dev/null +++ b/crates/rrt-cli/src/app/dispatch/runtime/scan.rs @@ -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> { + 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), + } +} diff --git a/crates/rrt-cli/src/app/dispatch/validate.rs b/crates/rrt-cli/src/app/dispatch/validate.rs new file mode 100644 index 0000000..1ac369d --- /dev/null +++ b/crates/rrt-cli/src/app/dispatch/validate.rs @@ -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> { + 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(()) +} diff --git a/crates/rrt-cli/src/app/finance.rs b/crates/rrt-cli/src/app/finance.rs new file mode 100644 index 0000000..c05d061 --- /dev/null +++ b/crates/rrt-cli/src/app/finance.rs @@ -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> { + 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> { + 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> { + let text = fs::read_to_string(path)?; + if let Ok(snapshot) = serde_json::from_str::(&text) { + return Ok(snapshot.evaluate()); + } + if let Ok(outcome) = serde_json::from_str::(&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> { + 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, +) { + 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(), + }), + _ => {} + } +} diff --git a/crates/rrt-cli/src/app/helpers/inspect.rs b/crates/rrt-cli/src/app/helpers/inspect.rs new file mode 100644 index 0000000..ac2c7f6 --- /dev/null +++ b/crates/rrt-cli/src/app/helpers/inspect.rs @@ -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> { + 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::::new(); + let mut dispatch_descriptor_occurrence_counts = BTreeMap::::new(); + let mut dispatch_descriptor_map_counts = BTreeMap::::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::::new(); + let mut add_building_descriptor_map_counts = BTreeMap::::new(); + let mut add_building_row_shape_occurrence_counts = BTreeMap::::new(); + let mut add_building_row_shape_map_counts = BTreeMap::::new(); + let mut add_building_signature_family_occurrence_counts = BTreeMap::::new(); + let mut add_building_signature_family_map_counts = BTreeMap::::new(); + let mut add_building_condition_tuple_occurrence_counts = BTreeMap::::new(); + let mut add_building_condition_tuple_map_counts = BTreeMap::::new(); + let mut add_building_signature_condition_cluster_occurrence_counts = + BTreeMap::::new(); + let mut add_building_signature_condition_cluster_map_counts = BTreeMap::::new(); + let mut signature_condition_cluster_descriptor_keys = + BTreeMap::>::new(); + let mut add_building_signature_condition_clusters = BTreeSet::::new(); + let mut dispatch_descriptor_occurrences = + BTreeMap::>::new(); + let mut unknown_descriptor_occurrences = + BTreeMap::>::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::::new(); + let mut map_add_building_descriptor_keys = BTreeSet::::new(); + let mut map_add_building_row_shapes = BTreeSet::::new(); + let mut map_add_building_signature_families = BTreeSet::::new(); + let mut map_add_building_condition_tuples = BTreeSet::::new(); + let mut map_add_building_signature_condition_clusters = BTreeSet::::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::>::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::>(); + 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::>(); + 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::>(); + 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::>()) + .unwrap_or_default(); + (cluster.clone(), keys) + }) + .collect::>(); + 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::>(); + (cluster.clone(), filtered) + }) + .collect::>(); + + 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> { + 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 { + 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, +) -> Result<(), Box> { + 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>, +) -> 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::>(); + 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::>(); + 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) + ) +} diff --git a/crates/rrt-cli/src/app/helpers/mod.rs b/crates/rrt-cli/src/app/helpers/mod.rs new file mode 100644 index 0000000..954b945 --- /dev/null +++ b/crates/rrt-cli/src/app/helpers/mod.rs @@ -0,0 +1,2 @@ +pub(super) mod inspect; +pub(super) mod state_io; diff --git a/crates/rrt-cli/src/app/helpers/state_io.rs b/crates/rrt-cli/src/app/helpers/state_io.rs new file mode 100644 index 0000000..9d2e15c --- /dev/null +++ b/crates/rrt-cli/src/app/helpers/state_io.rs @@ -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> { + 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> { + 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> { + 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> { + println!("{}", serde_json::to_string_pretty(report)?); + Ok(()) +} diff --git a/crates/rrt-cli/src/app/mod.rs b/crates/rrt-cli/src/app/mod.rs new file mode 100644 index 0000000..00c6fa9 --- /dev/null +++ b/crates/rrt-cli/src/app/mod.rs @@ -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> { + dispatch::run() +} diff --git a/crates/rrt-cli/src/app/reports/inspect.rs b/crates/rrt-cli/src/app/reports/inspect.rs new file mode 100644 index 0000000..f62b436 --- /dev/null +++ b/crates/rrt-cli/src/app/reports/inspect.rs @@ -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, +} + +#[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, + pub(crate) dispatch_descriptor_occurrence_counts: BTreeMap, + pub(crate) dispatch_descriptor_map_counts: BTreeMap, + pub(crate) dispatch_descriptor_occurrences: + BTreeMap>, + pub(crate) unknown_descriptor_ids: Vec, + pub(crate) unknown_descriptor_special_condition_label_matches: Vec, + pub(crate) unknown_descriptor_occurrences: + BTreeMap>, + 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, + pub(crate) add_building_descriptor_map_counts: BTreeMap, + pub(crate) add_building_row_shape_occurrence_counts: BTreeMap, + pub(crate) add_building_row_shape_map_counts: BTreeMap, + pub(crate) add_building_signature_family_occurrence_counts: BTreeMap, + pub(crate) add_building_signature_family_map_counts: BTreeMap, + pub(crate) add_building_condition_tuple_occurrence_counts: BTreeMap, + pub(crate) add_building_condition_tuple_map_counts: BTreeMap, + pub(crate) add_building_signature_condition_cluster_occurrence_counts: BTreeMap, + pub(crate) add_building_signature_condition_cluster_map_counts: BTreeMap, + pub(crate) add_building_signature_condition_cluster_descriptor_keys: + BTreeMap>, + pub(crate) add_building_signature_condition_cluster_non_add_building_descriptor_keys: + BTreeMap>, +} + +#[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, + pub(crate) dispatch_descriptor_occurrence_counts: BTreeMap, + pub(crate) dispatch_descriptor_map_counts: BTreeMap, + pub(crate) unknown_descriptor_ids: Vec, + pub(crate) unknown_descriptor_special_condition_label_matches: Vec, + 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, + pub(crate) add_building_descriptor_map_counts: BTreeMap, + pub(crate) add_building_row_shape_occurrence_counts: BTreeMap, + pub(crate) add_building_row_shape_map_counts: BTreeMap, + pub(crate) add_building_signature_family_occurrence_counts: BTreeMap, + pub(crate) add_building_signature_family_map_counts: BTreeMap, + pub(crate) add_building_condition_tuple_occurrence_counts: BTreeMap, + pub(crate) add_building_condition_tuple_map_counts: BTreeMap, + pub(crate) add_building_signature_condition_cluster_occurrence_counts: BTreeMap, + pub(crate) add_building_signature_condition_cluster_map_counts: BTreeMap, + pub(crate) add_building_signature_condition_cluster_descriptor_keys: + BTreeMap>, + pub(crate) add_building_signature_condition_cluster_non_add_building_descriptor_keys: + BTreeMap>, +} + +#[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, + pub(crate) signature_family: Option, + pub(crate) condition_tuples: Vec, + pub(crate) rows: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct RuntimeCompactEventDispatchClusterConditionTuple { + pub(crate) raw_condition_id: i32, + pub(crate) subtype: u8, + pub(crate) metric: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct RuntimeCompactEventDispatchClusterRow { + pub(crate) group_index: usize, + pub(crate) descriptor_id: u32, + pub(crate) descriptor_label: Option, + 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, +} diff --git a/crates/rrt-cli/src/app/reports/mod.rs b/crates/rrt-cli/src/app/reports/mod.rs new file mode 100644 index 0000000..436cc72 --- /dev/null +++ b/crates/rrt-cli/src/app/reports/mod.rs @@ -0,0 +1,2 @@ +pub(super) mod inspect; +pub(super) mod state; diff --git a/crates/rrt-cli/src/app/reports/state.rs b/crates/rrt-cli/src/app/reports/state.rs new file mode 100644 index 0000000..d716f6b --- /dev/null +++ b/crates/rrt-cli/src/app/reports/state.rs @@ -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, +} + +#[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, + pub(crate) expected_state_fragment_matches: bool, + pub(crate) expected_state_fragment_mismatches: Vec, +} + +#[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, +} + +#[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, +} diff --git a/crates/rrt-cli/src/app/runtime_compare/candidate_table.rs b/crates/rrt-cli/src/app/runtime_compare/candidate_table.rs new file mode 100644 index 0000000..12a3daa --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_compare/candidate_table.rs @@ -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, + pub(crate) footer_progress_word_0_hex: String, + pub(crate) footer_progress_word_1_hex: String, + pub(crate) availability_by_name: BTreeMap, +} + +#[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, + pub(crate) entries: Vec, +} + +#[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, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RuntimeCandidateTableComparisonReport { + pub(crate) file_count: usize, + pub(crate) matches: bool, + pub(crate) common_profile_family: Option, + pub(crate) common_semantic_family: Option, + pub(crate) samples: Vec, + pub(crate) difference_count: usize, + pub(crate) differences: Vec, +} + +pub(crate) fn compare_candidate_table( + smp_paths: &[PathBuf], +) -> Result<(), Box> { + let samples = smp_paths + .iter() + .map(|path| load_candidate_table_sample(path)) + .collect::, _>>()?; + 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> { + 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> { + 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> { + 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, Box> { + 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::>(); + 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 { + let mut numbered_entries = entries + .iter() + .filter_map(|entry| { + parse_numbered_candidate_name(&entry.text, prefix).map(|number| (entry, number)) + }) + .collect::>(); + 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 { + 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, + 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 { + 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 { + 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 +} diff --git a/crates/rrt-cli/src/app/runtime_compare/common.rs b/crates/rrt-cli/src/app/runtime_compare/common.rs new file mode 100644 index 0000000..242fbe3 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_compare/common.rs @@ -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, +} + +pub(crate) fn collect_json_multi_differences( + path: &str, + labeled_values: &[(String, Value)], + differences: &mut Vec, +) { + 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::>(); + 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::>(); + 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(), + }); +} diff --git a/crates/rrt-cli/src/app/runtime_compare/mod.rs b/crates/rrt-cli/src/app/runtime_compare/mod.rs new file mode 100644 index 0000000..f63c485 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_compare/mod.rs @@ -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, +}; diff --git a/crates/rrt-cli/src/app/runtime_compare/post_special.rs b/crates/rrt-cli/src/app/runtime_compare/post_special.rs new file mode 100644 index 0000000..9292e50 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_compare/post_special.rs @@ -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, + pub(crate) values_by_relative_offset_hex: BTreeMap, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RuntimePostSpecialConditionsScalarComparisonReport { + pub(crate) file_count: usize, + pub(crate) matches: bool, + pub(crate) common_profile_family: Option, + pub(crate) samples: Vec, + pub(crate) difference_count: usize, + pub(crate) differences: Vec, +} + +pub(crate) fn compare_post_special_conditions_scalars( + smp_paths: &[PathBuf], +) -> Result<(), Box> { + let samples = smp_paths + .iter() + .map(|path| load_post_special_conditions_scalar_sample(path)) + .collect::, _>>()?; + 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> { + 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, Box> { + 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::>(); + let mut differences = Vec::new(); + collect_json_multi_differences("$", &labeled_values, &mut differences); + Ok(differences) +} diff --git a/crates/rrt-cli/src/app/runtime_compare/profiles.rs b/crates/rrt-cli/src/app/runtime_compare/profiles.rs new file mode 100644 index 0000000..285b110 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_compare/profiles.rs @@ -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, + pub(crate) samples: Vec, + pub(crate) difference_count: usize, + pub(crate) differences: Vec, +} + +#[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, + pub(crate) samples: Vec, + pub(crate) difference_count: usize, + pub(crate) differences: Vec, +} + +pub(crate) fn compare_classic_profile( + smp_paths: &[PathBuf], +) -> Result<(), Box> { + let samples = smp_paths + .iter() + .map(|path| load_classic_profile_sample(path)) + .collect::, _>>()?; + 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> { + let samples = smp_paths + .iter() + .map(|path| load_rt3_105_profile_sample(path)) + .collect::, _>>()?; + 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> { + 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> { + 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, Box> { + 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::>(); + 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, Box> { + 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::>(); + let mut differences = Vec::new(); + collect_json_multi_differences("$", &labeled_values, &mut differences); + Ok(differences) +} diff --git a/crates/rrt-cli/src/app/runtime_compare/recipe_book.rs b/crates/rrt-cli/src/app/runtime_compare/recipe_book.rs new file mode 100644 index 0000000..c3b73b7 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_compare/recipe_book.rs @@ -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, + pub(crate) book_line_area_kind_by_index: BTreeMap, + pub(crate) max_annual_production_word_hex_by_book: BTreeMap, + pub(crate) line_kind_by_path: BTreeMap, + pub(crate) mode_word_hex_by_path: BTreeMap, + pub(crate) annual_amount_word_hex_by_path: BTreeMap, + pub(crate) supplied_cargo_token_word_hex_by_path: BTreeMap, + pub(crate) demanded_cargo_token_word_hex_by_path: BTreeMap, +} + +#[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, + pub(crate) samples: Vec, + pub(crate) difference_count: usize, + pub(crate) differences: Vec, + pub(crate) content_difference_count: usize, + pub(crate) content_differences: Vec, +} + +#[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, +} + +pub(crate) fn compare_recipe_book_lines( + smp_paths: &[PathBuf], +) -> Result<(), Box> { + let samples = smp_paths + .iter() + .map(|path| load_recipe_book_line_sample(path)) + .collect::, _>>()?; + 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> { + 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, Box> { + 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::>(); + 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, Box> { + 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::>(); + 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>, +) -> Vec { + let mut maps = maps.peekable(); + if maps.peek().is_none() { + return Vec::new(); + } + + let mut stable = maps + .next() + .map(|map| map.keys().cloned().collect::>()) + .unwrap_or_default(); + for map in maps { + let current = map.keys().cloned().collect::>(); + stable = stable.intersection(¤t).cloned().collect(); + } + stable.into_iter().collect() +} + +pub(crate) fn build_recipe_line_field_summaries<'a>( + maps: impl Iterator>, +) -> Vec { + let mut value_sets = BTreeMap::>::new(); + let mut counts = BTreeMap::::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() +} diff --git a/crates/rrt-cli/src/app/runtime_compare/region.rs b/crates/rrt-cli/src/app/runtime_compare/region.rs new file mode 100644 index 0000000..cfa685e --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_compare/region.rs @@ -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> { + 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(()) +} diff --git a/crates/rrt-cli/src/app/runtime_compare/setup_payload.rs b/crates/rrt-cli/src/app/runtime_compare/setup_payload.rs new file mode 100644 index 0000000..357a4f6 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_compare/setup_payload.rs @@ -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, + pub(crate) candidate_header_word_1_hex: Option, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RuntimeSetupPayloadCoreComparisonReport { + pub(crate) file_count: usize, + pub(crate) matches: bool, + pub(crate) samples: Vec, + pub(crate) difference_count: usize, + pub(crate) differences: Vec, +} + +#[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, + pub(crate) campaign_progress_page_index: Option, + 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, + pub(crate) nonzero_campaign_selector_values: BTreeMap, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RuntimeSetupLaunchPayloadComparisonReport { + pub(crate) file_count: usize, + pub(crate) matches: bool, + pub(crate) samples: Vec, + pub(crate) difference_count: usize, + pub(crate) differences: Vec, +} + +pub(crate) fn compare_setup_payload_core( + smp_paths: &[PathBuf], +) -> Result<(), Box> { + let samples = smp_paths + .iter() + .map(|path| load_setup_payload_core_sample(path)) + .collect::, _>>()?; + 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> { + let samples = smp_paths + .iter() + .map(|path| load_setup_launch_payload_sample(path)) + .collect::, _>>()?; + 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> { + 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> { + 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::>(); + let nonzero_campaign_selector_values = campaign_selector_values + .iter() + .filter_map(|(name, value)| (*value != 0).then_some((name.clone(), *value))) + .collect::>(); + + 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, Box> { + 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::>(); + 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, Box> { + 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::>(); + let mut differences = Vec::new(); + collect_json_multi_differences("$", &labeled_values, &mut differences); + Ok(differences) +} diff --git a/crates/rrt-cli/src/app/runtime_fixture_state/fixtures.rs b/crates/rrt-cli/src/app/runtime_fixture_state/fixtures.rs new file mode 100644 index 0000000..eb1d90a --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_fixture_state/fixtures.rs @@ -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> { + 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> { + 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> { + 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(()) +} diff --git a/crates/rrt-cli/src/app/runtime_fixture_state/mod.rs b/crates/rrt-cli/src/app/runtime_fixture_state/mod.rs new file mode 100644 index 0000000..aa49abe --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_fixture_state/mod.rs @@ -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}; diff --git a/crates/rrt-cli/src/app/runtime_fixture_state/save_import.rs b/crates/rrt-cli/src/app/runtime_fixture_state/save_import.rs new file mode 100644 index 0000000..3f376c0 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_fixture_state/save_import.rs @@ -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> { + 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> { + 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> { + let report = + export_runtime_overlay_import_document(snapshot_path, save_slice_path, output_path)?; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} diff --git a/crates/rrt-cli/src/app/runtime_fixture_state/save_load.rs b/crates/rrt-cli/src/app/runtime_fixture_state/save_load.rs new file mode 100644 index 0000000..c40c644 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_fixture_state/save_load.rs @@ -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> { + 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> { + 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(()) +} diff --git a/crates/rrt-cli/src/app/runtime_fixture_state/state.rs b/crates/rrt-cli/src/app/runtime_fixture_state/state.rs new file mode 100644 index 0000000..b965354 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_fixture_state/state.rs @@ -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> { + 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> { + 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> { + 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(()) +} diff --git a/crates/rrt-cli/src/app/runtime_inspect/assets.rs b/crates/rrt-cli/src/app/runtime_inspect/assets.rs new file mode 100644 index 0000000..56893b4 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_inspect/assets.rs @@ -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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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(()) +} diff --git a/crates/rrt-cli/src/app/runtime_inspect/maps.rs b/crates/rrt-cli/src/app/runtime_inspect/maps.rs new file mode 100644 index 0000000..c2e5e27 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_inspect/maps.rs @@ -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> { + 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::>(); + 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> { + 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> { + 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(()) +} diff --git a/crates/rrt-cli/src/app/runtime_inspect/mod.rs b/crates/rrt-cli/src/app/runtime_inspect/mod.rs new file mode 100644 index 0000000..7dacd79 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_inspect/mod.rs @@ -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, +}; diff --git a/crates/rrt-cli/src/app/runtime_inspect/smp.rs b/crates/rrt-cli/src/app/runtime_inspect/smp.rs new file mode 100644 index 0000000..6030e75 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_inspect/smp.rs @@ -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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + println!( + "{}", + serde_json::to_string_pretty(&inspect_unclassified_save_collection_headers_file( + smp_path + )?)? + ); + Ok(()) +} diff --git a/crates/rrt-cli/src/app/runtime_scan/aligned_band.rs b/crates/rrt-cli/src/app/runtime_scan/aligned_band.rs new file mode 100644 index 0000000..312da49 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_scan/aligned_band.rs @@ -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, + pub(crate) values_by_band_index: BTreeMap, +} + +#[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, + pub(crate) file_count_present: usize, + pub(crate) distinct_value_count: usize, + pub(crate) sample_value_hexes: Vec, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RuntimeAlignedRuntimeRuleBandFamilySummary { + pub(crate) profile_family: String, + pub(crate) source_kinds: Vec, + 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, + pub(crate) union_nonzero_band_indices: Vec, + pub(crate) offset_summaries: Vec, + pub(crate) sample_paths: Vec, +} + +#[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, +} + +pub(crate) fn scan_aligned_runtime_rule_band( + root_path: &Path, +) -> Result<(), Box> { + 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::>::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::>() + .into_iter() + .collect::>(); + let distinct_nonzero_index_set_count = samples + .iter() + .map(|sample| sample.nonzero_band_indices.clone()) + .collect::>() + .len(); + + let stable_band_indices = if samples.is_empty() { + BTreeSet::new() + } else { + let mut stable = samples[0] + .nonzero_band_indices + .iter() + .copied() + .collect::>(); + for sample in samples.iter().skip(1) { + let current = sample + .nonzero_band_indices + .iter() + .copied() + .collect::>(); + stable = stable.intersection(¤t).copied().collect(); + } + stable + }; + + let mut band_values = BTreeMap::>::new(); + let mut band_counts = BTreeMap::::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::>(); + + 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::>(); + + 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> { + 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, + }) +} diff --git a/crates/rrt-cli/src/app/runtime_scan/candidate_table.rs b/crates/rrt-cli/src/app/runtime_scan/candidate_table.rs new file mode 100644 index 0000000..24a1fff --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_scan/candidate_table.rs @@ -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, + pub(crate) source_kinds: Vec, + pub(crate) zero_trailer_count_min: usize, + pub(crate) zero_trailer_count_max: usize, + pub(crate) zero_trailer_count_values: Vec, + pub(crate) distinct_zero_name_set_count: usize, + pub(crate) sample_paths: Vec, +} + +#[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, +} + +#[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, +} + +#[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, + pub(crate) warehouse_runs: Vec, +} + +#[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, +} + +pub(crate) fn scan_candidate_table_headers( + root_path: &Path, +) -> Result<(), Box> { + 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>::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::>() + .into_iter() + .collect::>(); + let mut source_kinds = samples + .iter() + .map(|sample| sample.source_kind.clone()) + .collect::>() + .into_iter() + .collect::>(); + let mut zero_trailer_count_values = samples + .iter() + .map(|sample| sample.zero_trailer_entry_count) + .collect::>() + .into_iter() + .collect::>(); + let distinct_zero_name_set_count = samples + .iter() + .map(|sample| sample.zero_trailer_entry_names.clone()) + .collect::>() + .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::>(); + 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::>(); + + 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> { + 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, +) -> Result<(), Box> { + 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> { + 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> { + 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, + }) +} diff --git a/crates/rrt-cli/src/app/runtime_scan/common.rs b/crates/rrt-cli/src/app/runtime_scan/common.rs new file mode 100644 index 0000000..7578e0d --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_scan/common.rs @@ -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, +) -> Result<(), Box> { + 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 { + 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 { + 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 + } +} diff --git a/crates/rrt-cli/src/app/runtime_scan/mod.rs b/crates/rrt-cli/src/app/runtime_scan/mod.rs new file mode 100644 index 0000000..0c47e8c --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_scan/mod.rs @@ -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; diff --git a/crates/rrt-cli/src/app/runtime_scan/post_special.rs b/crates/rrt-cli/src/app/runtime_scan/post_special.rs new file mode 100644 index 0000000..331c541 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_scan/post_special.rs @@ -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, + pub(crate) values_by_relative_offset_hex: BTreeMap, +} + +#[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, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RuntimePostSpecialConditionsScalarFamilySummary { + pub(crate) profile_family: String, + pub(crate) source_kinds: Vec, + 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, + pub(crate) union_nonzero_relative_offset_hexes: Vec, + pub(crate) offset_summaries: Vec, + pub(crate) sample_paths: Vec, +} + +#[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, +} + +#[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, + pub(crate) values_by_relative_offset_hex: BTreeMap, +} + +#[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, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RuntimePostSpecialConditionsTailFamilySummary { + pub(crate) profile_family: String, + pub(crate) source_kinds: Vec, + 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, + pub(crate) union_nonzero_relative_offset_hexes: Vec, + pub(crate) offset_summaries: Vec, + pub(crate) sample_paths: Vec, +} + +#[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, +} + +pub(crate) fn scan_post_special_conditions_scalars( + root_path: &Path, +) -> Result<(), Box> { + 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> { + 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> { + 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> { + 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, +) -> Vec { + let mut grouped = BTreeMap::>::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::>() + .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::>() + .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, +) -> Vec { + let mut grouped = BTreeMap::>::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::>() + .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::>() + .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) -> BTreeSet { + 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::>()) + .unwrap_or_default(); + for offsets in offset_sets { + let current = offsets.iter().copied().collect::>(); + stable = stable.intersection(¤t).copied().collect(); + } + stable +} + +fn collect_offset_values<'a>( + maps: impl Iterator>, +) -> (BTreeMap>, BTreeMap) { + let mut offset_values = BTreeMap::>::new(); + let mut offset_counts = BTreeMap::::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>, BTreeMap), +) -> Vec { + 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>, BTreeMap), +) -> Vec { + 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() +} diff --git a/crates/rrt-cli/src/app/runtime_scan/recipe_book.rs b/crates/rrt-cli/src/app/runtime_scan/recipe_book.rs new file mode 100644 index 0000000..cd47dd9 --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_scan/recipe_book.rs @@ -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, + pub(crate) nonzero_supplied_token_paths: BTreeMap, + pub(crate) nonzero_demanded_token_paths: BTreeMap, +} + +#[derive(Debug, Serialize)] +pub(crate) struct RuntimeRecipeBookLineFamilySummary { + pub(crate) profile_family: String, + pub(crate) source_kinds: Vec, + 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, + pub(crate) stable_nonzero_supplied_token_paths: Vec, + pub(crate) stable_nonzero_demanded_token_paths: Vec, + pub(crate) mode_summaries: Vec, + pub(crate) supplied_token_summaries: Vec, + pub(crate) demanded_token_summaries: Vec, + pub(crate) sample_paths: Vec, +} + +#[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, +} + +pub(crate) fn scan_recipe_book_lines(root_path: &Path) -> Result<(), Box> { + 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::>::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::>() + .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::>(); + + 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> { + 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(), + }) +} diff --git a/crates/rrt-cli/src/app/runtime_scan/special_conditions.rs b/crates/rrt-cli/src/app/runtime_scan/special_conditions.rs new file mode 100644 index 0000000..51bf34a --- /dev/null +++ b/crates/rrt-cli/src/app/runtime_scan/special_conditions.rs @@ -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, +} + +#[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, +} + +#[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, + pub(crate) sample_files_with_any_enabled: Vec, +} + +pub(crate) fn scan_special_conditions(root_path: &Path) -> Result<(), Box> { + 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::>(); + let files_with_any_enabled_count = sample_files_with_any_enabled.len(); + + let mut grouped = BTreeMap::<(u8, String), Vec>::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::>(); + + 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> { + 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::>(); + + 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, + }) +} diff --git a/crates/rrt-cli/src/app/tests/compare.rs b/crates/rrt-cli/src/app/tests/compare.rs new file mode 100644 index 0000000..f8d2be9 --- /dev/null +++ b/crates/rrt-cli/src/app/tests/compare.rs @@ -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") + ); +} diff --git a/crates/rrt-cli/src/app/tests/mod.rs b/crates/rrt-cli/src/app/tests/mod.rs new file mode 100644 index 0000000..34124f5 --- /dev/null +++ b/crates/rrt-cli/src/app/tests/mod.rs @@ -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::*; diff --git a/crates/rrt-cli/src/app/tests/state/diff.rs b/crates/rrt-cli/src/app/tests/state/diff.rs new file mode 100644 index 0000000..e9afc0d --- /dev/null +++ b/crates/rrt-cli/src/app/tests/state/diff.rs @@ -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); +} diff --git a/crates/rrt-cli/src/app/tests/state/document_io.rs b/crates/rrt-cli/src/app/tests/state/document_io.rs new file mode 100644 index 0000000..bad4955 --- /dev/null +++ b/crates/rrt-cli/src/app/tests/state/document_io.rs @@ -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); +} diff --git a/crates/rrt-cli/src/app/tests/state/fixture_summary.rs b/crates/rrt-cli/src/app/tests/state/fixture_summary.rs new file mode 100644 index 0000000..8d7159b --- /dev/null +++ b/crates/rrt-cli/src/app/tests/state/fixture_summary.rs @@ -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"); +} diff --git a/crates/rrt-cli/src/app/tests/state/mod.rs b/crates/rrt-cli/src/app/tests/state/mod.rs new file mode 100644 index 0000000..b797563 --- /dev/null +++ b/crates/rrt-cli/src/app/tests/state/mod.rs @@ -0,0 +1,7 @@ +use super::*; + +mod diff; +mod document_io; +mod fixture_summary; +mod save_slice_overlay; +mod snapshot_io; diff --git a/crates/rrt-cli/src/app/tests/state/save_slice_overlay.rs b/crates/rrt-cli/src/app/tests/state/save_slice_overlay.rs new file mode 100644 index 0000000..3d6eb0f --- /dev/null +++ b/crates/rrt-cli/src/app/tests/state/save_slice_overlay.rs @@ -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" + })); +} diff --git a/crates/rrt-cli/src/app/tests/state/snapshot_io.rs b/crates/rrt-cli/src/app/tests/state/snapshot_io.rs new file mode 100644 index 0000000..a1bbbd3 --- /dev/null +++ b/crates/rrt-cli/src/app/tests/state/snapshot_io.rs @@ -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); +} diff --git a/crates/rrt-cli/src/app/tests/support/json.rs b/crates/rrt-cli/src/app/tests/support/json.rs new file mode 100644 index 0000000..099b5b2 --- /dev/null +++ b/crates/rrt-cli/src/app/tests/support/json.rs @@ -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:?}" + ); +} diff --git a/crates/rrt-cli/src/app/tests/support/mod.rs b/crates/rrt-cli/src/app/tests/support/mod.rs new file mode 100644 index 0000000..887bbb8 --- /dev/null +++ b/crates/rrt-cli/src/app/tests/support/mod.rs @@ -0,0 +1,5 @@ +mod json; +mod temp_files; + +pub(crate) use json::*; +pub(crate) use temp_files::*; diff --git a/crates/rrt-cli/src/app/tests/support/temp_files.rs b/crates/rrt-cli/src/app/tests/support/temp_files.rs new file mode 100644 index 0000000..d71980c --- /dev/null +++ b/crates/rrt-cli/src/app/tests/support/temp_files.rs @@ -0,0 +1,15 @@ +use std::fs; +use std::path::PathBuf; + +use serde::Serialize; + +pub(crate) fn write_temp_json(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 +} diff --git a/crates/rrt-cli/src/app/validate.rs b/crates/rrt-cli/src/app/validate.rs new file mode 100644 index 0000000..1fdc124 --- /dev/null +++ b/crates/rrt-cli/src/app/validate.rs @@ -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> { + 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> { + 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> { + 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> { + 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> { + 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())) +} diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index c23b282..d423935 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -1,7109 +1,10 @@ #![recursion_limit = "256"] -use std::collections::{BTreeMap, BTreeSet}; -use std::env; -use std::fs; -use std::io::Read; -use std::path::{Path, PathBuf}; - -use rrt_fixtures::{ - FixtureValidationReport, JsonDiffEntry, compare_expected_state_fragment, diff_json_values, - load_fixture_document, normalize_runtime_state, validate_fixture_document, -}; -use rrt_model::{ - BINARY_SUMMARY_PATH, CANONICAL_EXE_PATH, CONTROL_LOOP_ATLAS_PATH, FUNCTION_MAP_PATH, - REQUIRED_ATLAS_HEADINGS, REQUIRED_EXPORTS, - finance::{FinanceOutcome, FinanceSnapshot}, - load_binary_summary, load_function_map, -}; -use rrt_runtime::{ - BuildingTypeSourceReport, CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, - CargoEconomySourceReport, CargoSelectorReport, CargoSkinInspectionReport, - CargoTypeInspectionReport, OBSERVED_CAMPAIGN_SCENARIO_NAMES, - OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, Pk4ExtractionReport, Pk4InspectionReport, - RuntimeOverlayImportDocument, RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, - RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, RuntimeSnapshotSource, RuntimeSummary, - SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock, - SmpInspectionReport, SmpLoadedSaveSlice, SmpMapTitleHintProbe, SmpRt3105PackedProfileBlock, - SmpSaveLoadSummary, WinInspectionReport, compare_save_region_fixed_row_run_candidates, - execute_step_command, extract_pk4_entry_file, inspect_building_types_dir_with_bindings, - inspect_campaign_exe_file, inspect_cargo_economy_sources_with_bindings, inspect_cargo_skin_pk4, - inspect_cargo_types_dir, inspect_map_title_hint_file, inspect_pk4_file, - inspect_save_company_and_chairman_analysis_file, inspect_save_infrastructure_asset_trace_file, - inspect_save_periodic_company_service_trace_file, - inspect_save_placed_structure_dynamic_side_buffer_file, - inspect_save_region_queued_notice_records_file, inspect_save_region_service_trace_file, - inspect_smp_file, inspect_unclassified_save_collection_headers_file, inspect_win_file, - load_runtime_snapshot_document, load_runtime_state_import, load_save_slice_file, - project_save_slice_to_runtime_state_import, save_runtime_overlay_import_document, - save_runtime_save_slice_document, save_runtime_snapshot_document, - validate_runtime_snapshot_document, -}; -use serde::Serialize; -use serde_json::Value; -use sha2::{Digest, Sha256}; - -const SPECIAL_CONDITIONS_OFFSET: usize = 0x0d64; -const SPECIAL_CONDITION_COUNT: usize = 36; -const SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT: usize = 35; -const SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT: usize = 50; -const SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT: usize = 49; -const SMP_ALIGNED_RUNTIME_RULE_END_OFFSET: usize = - SPECIAL_CONDITIONS_OFFSET + SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT * 4; -const POST_SPECIAL_CONDITIONS_SCALAR_OFFSET: usize = 0x0df4; -const POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET: usize = 0x0f30; -const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET: usize = SMP_ALIGNED_RUNTIME_RULE_END_OFFSET; -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", -]; - -enum Command { - Validate { - repo_root: PathBuf, - }, - FinanceEval { - snapshot_path: PathBuf, - }, - FinanceDiff { - left_path: PathBuf, - right_path: PathBuf, - }, - RuntimeValidateFixture { - fixture_path: PathBuf, - }, - RuntimeSummarizeFixture { - fixture_path: PathBuf, - }, - RuntimeExportFixtureState { - fixture_path: PathBuf, - output_path: PathBuf, - }, - RuntimeDiffState { - left_path: PathBuf, - right_path: PathBuf, - }, - RuntimeSummarizeState { - snapshot_path: PathBuf, - }, - RuntimeImportState { - input_path: PathBuf, - output_path: PathBuf, - }, - RuntimeInspectSmp { - smp_path: PathBuf, - }, - RuntimeInspectCandidateTable { - smp_path: PathBuf, - }, - RuntimeInspectCompactEventDispatchCluster { - root_path: PathBuf, - }, - RuntimeInspectCompactEventDispatchClusterCounts { - root_path: PathBuf, - }, - RuntimeInspectMapTitleHints { - root_path: PathBuf, - }, - RuntimeSummarizeSaveLoad { - smp_path: PathBuf, - }, - RuntimeLoadSaveSlice { - smp_path: PathBuf, - }, - RuntimeInspectSaveCompanyChairman { - smp_path: PathBuf, - }, - RuntimeInspectSavePlacedStructureTriplets { - smp_path: PathBuf, - }, - RuntimeCompareRegionFixedRowRuns { - left_path: PathBuf, - right_path: PathBuf, - }, - RuntimeInspectPeriodicCompanyServiceTrace { - smp_path: PathBuf, - }, - RuntimeInspectRegionServiceTrace { - smp_path: PathBuf, - }, - RuntimeInspectInfrastructureAssetTrace { - smp_path: PathBuf, - }, - RuntimeInspectSaveRegionQueuedNoticeRecords { - smp_path: PathBuf, - }, - RuntimeInspectPlacedStructureDynamicSideBuffer { - smp_path: PathBuf, - }, - RuntimeInspectUnclassifiedSaveCollections { - smp_path: PathBuf, - }, - RuntimeImportSaveState { - smp_path: PathBuf, - output_path: PathBuf, - }, - RuntimeExportSaveSlice { - smp_path: PathBuf, - output_path: PathBuf, - }, - RuntimeExportOverlayImport { - snapshot_path: PathBuf, - save_slice_path: PathBuf, - output_path: PathBuf, - }, - RuntimeInspectPk4 { - pk4_path: PathBuf, - }, - RuntimeInspectCargoTypes { - cargo_types_dir: PathBuf, - }, - RuntimeInspectBuildingTypeSources { - building_types_dir: PathBuf, - bindings_path: Option, - }, - RuntimeInspectCargoSkins { - cargo_skin_pk4_path: PathBuf, - }, - RuntimeInspectCargoEconomySources { - cargo_types_dir: PathBuf, - cargo_skin_pk4_path: PathBuf, - }, - RuntimeInspectCargoProductionSelector { - cargo_types_dir: PathBuf, - cargo_skin_pk4_path: PathBuf, - }, - RuntimeInspectCargoPriceSelector { - cargo_types_dir: PathBuf, - cargo_skin_pk4_path: PathBuf, - }, - RuntimeInspectWin { - win_path: PathBuf, - }, - RuntimeExtractPk4Entry { - pk4_path: PathBuf, - entry_name: String, - output_path: PathBuf, - }, - RuntimeInspectCampaignExe { - exe_path: PathBuf, - }, - RuntimeCompareClassicProfile { - smp_paths: Vec, - }, - RuntimeCompareRt3105Profile { - smp_paths: Vec, - }, - RuntimeCompareCandidateTable { - smp_paths: Vec, - }, - RuntimeCompareRecipeBookLines { - smp_paths: Vec, - }, - RuntimeCompareSetupPayloadCore { - smp_paths: Vec, - }, - RuntimeCompareSetupLaunchPayload { - smp_paths: Vec, - }, - RuntimeComparePostSpecialConditionsScalars { - smp_paths: Vec, - }, - RuntimeScanCandidateTableHeaders { - root_path: PathBuf, - }, - RuntimeScanCandidateTableNamedRuns { - root_path: PathBuf, - }, - RuntimeScanSpecialConditions { - root_path: PathBuf, - }, - RuntimeScanAlignedRuntimeRuleBand { - root_path: PathBuf, - }, - RuntimeScanPostSpecialConditionsScalars { - root_path: PathBuf, - }, - RuntimeScanPostSpecialConditionsTail { - root_path: PathBuf, - }, - RuntimeScanRecipeBookLines { - root_path: PathBuf, - }, - RuntimeExportProfileBlock { - smp_path: PathBuf, - output_path: PathBuf, - }, -} - -#[derive(Debug, Serialize)] -struct FinanceDiffEntry { - path: String, - left: Value, - right: Value, -} - -#[derive(Debug, Serialize)] -struct FinanceDiffReport { - matches: bool, - difference_count: usize, - differences: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeFixtureSummaryReport { - fixture_id: String, - command_count: usize, - final_summary: RuntimeSummary, - expected_summary_matches: bool, - expected_summary_mismatches: Vec, - expected_state_fragment_matches: bool, - expected_state_fragment_mismatches: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeStateSummaryReport { - snapshot_id: String, - summary: RuntimeSummary, -} - -#[derive(Debug, Serialize)] -struct RuntimeStateDiffReport { - matches: bool, - difference_count: usize, - differences: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeSmpInspectionOutput { - path: String, - inspection: SmpInspectionReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeCompactEventDispatchClusterOutput { - root_path: String, - report: RuntimeCompactEventDispatchClusterReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeCompactEventDispatchClusterCountsOutput { - root_path: String, - report: RuntimeCompactEventDispatchClusterCountsReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeMapTitleHintDirectoryOutput { - root_path: String, - report: RuntimeMapTitleHintDirectoryReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeMapTitleHintDirectoryReport { - maps_scanned: usize, - maps_with_probe: usize, - maps_with_grounded_title_hits: usize, - maps_with_adjacent_title_pairs: usize, - maps_with_same_stem_adjacent_pairs: usize, - maps: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeMapTitleHintMapEntry { - path: String, - probe: SmpMapTitleHintProbe, -} - -#[derive(Debug, Serialize)] -struct RuntimeCompactEventDispatchClusterReport { - maps_scanned: usize, - maps_with_event_runtime_collection: usize, - maps_with_dispatch_strip_records: usize, - dispatch_strip_record_count: usize, - dispatch_strip_records_with_trigger_kind: usize, - dispatch_strip_records_missing_trigger_kind: usize, - dispatch_strip_payload_families: BTreeMap, - dispatch_descriptor_occurrence_counts: BTreeMap, - dispatch_descriptor_map_counts: BTreeMap, - dispatch_descriptor_occurrences: - BTreeMap>, - unknown_descriptor_ids: Vec, - unknown_descriptor_special_condition_label_matches: Vec, - unknown_descriptor_occurrences: - BTreeMap>, - add_building_dispatch_record_count: usize, - add_building_dispatch_records_with_trigger_kind: usize, - add_building_dispatch_records_missing_trigger_kind: usize, - add_building_descriptor_occurrence_counts: BTreeMap, - add_building_descriptor_map_counts: BTreeMap, - add_building_row_shape_occurrence_counts: BTreeMap, - add_building_row_shape_map_counts: BTreeMap, - add_building_signature_family_occurrence_counts: BTreeMap, - add_building_signature_family_map_counts: BTreeMap, - add_building_condition_tuple_occurrence_counts: BTreeMap, - add_building_condition_tuple_map_counts: BTreeMap, - add_building_signature_condition_cluster_occurrence_counts: BTreeMap, - add_building_signature_condition_cluster_map_counts: BTreeMap, - add_building_signature_condition_cluster_descriptor_keys: BTreeMap>, - add_building_signature_condition_cluster_non_add_building_descriptor_keys: - BTreeMap>, -} - -#[derive(Debug, Serialize)] -struct RuntimeCompactEventDispatchClusterCountsReport { - maps_scanned: usize, - maps_with_event_runtime_collection: usize, - maps_with_dispatch_strip_records: usize, - dispatch_strip_record_count: usize, - dispatch_strip_records_with_trigger_kind: usize, - dispatch_strip_records_missing_trigger_kind: usize, - dispatch_strip_payload_families: BTreeMap, - dispatch_descriptor_occurrence_counts: BTreeMap, - dispatch_descriptor_map_counts: BTreeMap, - unknown_descriptor_ids: Vec, - unknown_descriptor_special_condition_label_matches: Vec, - add_building_dispatch_record_count: usize, - add_building_dispatch_records_with_trigger_kind: usize, - add_building_dispatch_records_missing_trigger_kind: usize, - add_building_descriptor_occurrence_counts: BTreeMap, - add_building_descriptor_map_counts: BTreeMap, - add_building_row_shape_occurrence_counts: BTreeMap, - add_building_row_shape_map_counts: BTreeMap, - add_building_signature_family_occurrence_counts: BTreeMap, - add_building_signature_family_map_counts: BTreeMap, - add_building_condition_tuple_occurrence_counts: BTreeMap, - add_building_condition_tuple_map_counts: BTreeMap, - add_building_signature_condition_cluster_occurrence_counts: BTreeMap, - add_building_signature_condition_cluster_map_counts: BTreeMap, - add_building_signature_condition_cluster_descriptor_keys: BTreeMap>, - add_building_signature_condition_cluster_non_add_building_descriptor_keys: - BTreeMap>, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeCompactEventDispatchClusterOccurrence { - path: String, - record_index: usize, - live_entry_id: u32, - payload_family: String, - trigger_kind: Option, - signature_family: Option, - condition_tuples: Vec, - rows: Vec, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeCompactEventDispatchClusterConditionTuple { - raw_condition_id: i32, - subtype: u8, - metric: Option, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeCompactEventDispatchClusterRow { - group_index: usize, - descriptor_id: u32, - descriptor_label: Option, - opcode: u8, - raw_scalar_value: i32, -} - -#[derive(Debug, Serialize)] -struct RuntimeSaveLoadSummaryOutput { - path: String, - summary: SmpSaveLoadSummary, -} - -#[derive(Debug, Serialize)] -struct RuntimeLoadedSaveSliceOutput { - path: String, - save_slice: SmpLoadedSaveSlice, -} - -#[derive(Debug, Serialize)] -struct RuntimeSaveCompanyChairmanAnalysisOutput { - path: String, - analysis: rrt_runtime::SmpSaveCompanyChairmanAnalysisReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeRegionFixedRowRunComparisonOutput { - left_path: String, - right_path: String, - comparison: rrt_runtime::SmpSaveRegionFixedRowRunComparisonReport, -} - -#[derive(Debug, Serialize)] -struct RuntimePeriodicCompanyServiceTraceOutput { - path: String, - trace: rrt_runtime::SmpPeriodicCompanyServiceTraceReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeRegionServiceTraceOutput { - path: String, - trace: rrt_runtime::SmpRegionServiceTraceReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeInfrastructureAssetTraceOutput { - path: String, - trace: rrt_runtime::SmpInfrastructureAssetTraceReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeSaveSliceExportOutput { - path: String, - output_path: String, - save_slice_id: String, -} - -#[derive(Debug, Serialize)] -struct RuntimeOverlayImportExportOutput { - output_path: String, - import_id: String, - base_snapshot_path: String, - save_slice_path: String, -} - -#[derive(Debug, Serialize)] -struct RuntimePk4InspectionOutput { - path: String, - inspection: Pk4InspectionReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeCargoTypeInspectionOutput { - path: String, - inspection: CargoTypeInspectionReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeBuildingTypeInspectionOutput { - path: String, - inspection: BuildingTypeSourceReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeCargoSkinInspectionOutput { - path: String, - inspection: CargoSkinInspectionReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeCargoEconomyInspectionOutput { - cargo_types_dir: String, - cargo_skin_pk4_path: String, - inspection: CargoEconomySourceReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeCargoSelectorInspectionOutput { - cargo_types_dir: String, - cargo_skin_pk4_path: String, - selector: CargoSelectorReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeWinInspectionOutput { - path: String, - inspection: WinInspectionReport, -} - -#[derive(Debug, Serialize)] -struct RuntimePk4ExtractionOutput { - path: String, - output_path: String, - extraction: Pk4ExtractionReport, -} - -#[derive(Debug, Serialize)] -struct RuntimeCampaignExeInspectionOutput { - path: String, - inspection: CampaignExeInspectionReport, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeClassicProfileSample { - path: String, - profile_family: String, - progress_32dc_offset: usize, - progress_3714_offset: usize, - progress_3715_offset: usize, - packed_profile_offset: usize, - packed_profile_len: usize, - packed_profile_block: SmpClassicPackedProfileBlock, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeClassicProfileDifferenceValue { - path: String, - value: Value, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeClassicProfileDifference { - field_path: String, - values: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeClassicProfileComparisonReport { - file_count: usize, - matches: bool, - common_profile_family: Option, - samples: Vec, - difference_count: usize, - differences: Vec, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeRt3105ProfileSample { - path: String, - profile_family: String, - packed_profile_offset: usize, - packed_profile_len: usize, - packed_profile_block: SmpRt3105PackedProfileBlock, -} - -#[derive(Debug, Serialize)] -struct RuntimeRt3105ProfileComparisonReport { - file_count: usize, - matches: bool, - common_profile_family: Option, - samples: Vec, - difference_count: usize, - differences: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeCandidateTableSample { - path: String, - profile_family: String, - source_kind: String, - semantic_family: String, - header_word_0_hex: String, - header_word_1_hex: String, - header_word_2_hex: String, - observed_entry_count: usize, - zero_trailer_entry_count: usize, - nonzero_trailer_entry_count: usize, - zero_trailer_entry_names: Vec, - footer_progress_word_0_hex: String, - footer_progress_word_1_hex: String, - availability_by_name: BTreeMap, -} - -#[derive(Debug, Serialize)] -struct RuntimeCandidateTableEntrySample { - index: usize, - offset: usize, - text: String, - availability_dword: u32, - availability_dword_hex: String, - trailer_word: u32, - trailer_word_hex: String, -} - -#[derive(Debug, Serialize)] -struct RuntimeCandidateTableInspectionReport { - path: String, - profile_family: String, - source_kind: String, - semantic_family: String, - header_word_0_hex: String, - header_word_1_hex: String, - header_word_2_hex: String, - observed_entry_capacity: usize, - observed_entry_count: usize, - zero_trailer_entry_count: usize, - nonzero_trailer_entry_count: usize, - zero_trailer_entry_names: Vec, - entries: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeCandidateTableComparisonReport { - file_count: usize, - matches: bool, - common_profile_family: Option, - common_semantic_family: Option, - samples: Vec, - difference_count: usize, - differences: Vec, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeRecipeBookLineSample { - path: String, - profile_family: String, - source_kind: String, - book_count: usize, - book_stride_hex: String, - line_count: usize, - line_stride_hex: String, - book_head_kind_by_index: BTreeMap, - book_line_area_kind_by_index: BTreeMap, - max_annual_production_word_hex_by_book: BTreeMap, - line_kind_by_path: BTreeMap, - mode_word_hex_by_path: BTreeMap, - annual_amount_word_hex_by_path: BTreeMap, - supplied_cargo_token_word_hex_by_path: BTreeMap, - demanded_cargo_token_word_hex_by_path: BTreeMap, -} - -#[derive(Debug, Serialize)] -struct RuntimeRecipeBookLineComparisonReport { - file_count: usize, - matches: bool, - content_matches: bool, - common_profile_family: Option, - samples: Vec, - difference_count: usize, - differences: Vec, - content_difference_count: usize, - content_differences: Vec, -} - -#[derive(Debug, Clone)] -struct RuntimeRecipeBookLineScanSample { - path: String, - profile_family: String, - source_kind: String, - nonzero_mode_paths: BTreeMap, - nonzero_supplied_token_paths: BTreeMap, - nonzero_demanded_token_paths: BTreeMap, -} - -#[derive(Debug, Serialize)] -struct RuntimeRecipeBookLineFieldSummary { - line_path: String, - file_count_present: usize, - distinct_value_count: usize, - sample_value_hexes: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeRecipeBookLineFamilySummary { - profile_family: String, - source_kinds: Vec, - file_count: usize, - files_with_any_nonzero_modes_count: usize, - files_with_any_nonzero_supplied_tokens_count: usize, - files_with_any_nonzero_demanded_tokens_count: usize, - stable_nonzero_mode_paths: Vec, - stable_nonzero_supplied_token_paths: Vec, - stable_nonzero_demanded_token_paths: Vec, - mode_summaries: Vec, - supplied_token_summaries: Vec, - demanded_token_summaries: Vec, - sample_paths: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeRecipeBookLineScanReport { - root_path: String, - file_count: usize, - files_with_probe_count: usize, - files_with_any_nonzero_modes_count: usize, - files_with_any_nonzero_supplied_tokens_count: usize, - files_with_any_nonzero_demanded_tokens_count: usize, - skipped_file_count: usize, - family_summaries: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeSetupPayloadCoreSample { - path: String, - file_extension: String, - inferred_profile_family: String, - payload_word_0x14: u16, - payload_word_0x14_hex: String, - payload_byte_0x20: u8, - payload_byte_0x20_hex: String, - marker_bytes_0x2c9_0x2d0_hex: String, - row_category_byte_0x31a: u8, - row_category_byte_0x31a_hex: String, - row_visibility_byte_0x31b: u8, - row_visibility_byte_0x31b_hex: String, - row_visibility_byte_0x31c: u8, - row_visibility_byte_0x31c_hex: String, - row_count_word_0x3ae: u16, - row_count_word_0x3ae_hex: String, - payload_word_0x3b2: u16, - payload_word_0x3b2_hex: String, - payload_word_0x3ba: u16, - payload_word_0x3ba_hex: String, - candidate_header_word_0_hex: Option, - candidate_header_word_1_hex: Option, -} - -#[derive(Debug, Serialize)] -struct RuntimeSetupPayloadCoreComparisonReport { - file_count: usize, - matches: bool, - samples: Vec, - difference_count: usize, - differences: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeSetupLaunchPayloadSample { - path: String, - file_extension: String, - inferred_profile_family: String, - launch_flag_byte_0x22: u8, - launch_flag_byte_0x22_hex: String, - campaign_progress_in_known_range: bool, - campaign_progress_scenario_name: Option, - campaign_progress_page_index: Option, - launch_selector_byte_0x33: u8, - launch_selector_byte_0x33_hex: String, - launch_token_block_0x23_0x32_hex: String, - campaign_selector_values: BTreeMap, - nonzero_campaign_selector_values: BTreeMap, -} - -#[derive(Debug, Serialize)] -struct RuntimeSetupLaunchPayloadComparisonReport { - file_count: usize, - matches: bool, - samples: Vec, - difference_count: usize, - differences: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimePostSpecialConditionsScalarSample { - path: String, - profile_family: String, - source_kind: String, - nonzero_relative_offset_hexes: Vec, - values_by_relative_offset_hex: BTreeMap, -} - -#[derive(Debug, Serialize)] -struct RuntimePostSpecialConditionsScalarComparisonReport { - file_count: usize, - matches: bool, - common_profile_family: Option, - samples: Vec, - difference_count: usize, - differences: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeCandidateTableHeaderCluster { - header_word_0_hex: String, - header_word_1_hex: String, - file_count: usize, - profile_families: Vec, - source_kinds: Vec, - zero_trailer_count_min: usize, - zero_trailer_count_max: usize, - zero_trailer_count_values: Vec, - distinct_zero_name_set_count: usize, - sample_paths: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeCandidateTableHeaderScanReport { - root_path: String, - file_count: usize, - cluster_count: usize, - skipped_file_count: usize, - clusters: Vec, -} - -#[derive(Debug, Clone)] -struct RuntimeCandidateTableHeaderScanSample { - path: String, - profile_family: String, - source_kind: String, - header_word_0_hex: String, - header_word_1_hex: String, - zero_trailer_entry_count: usize, - zero_trailer_entry_names: Vec, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeCandidateTableNamedRun { - prefix: String, - start_index: usize, - end_index: usize, - count: usize, - first_name: String, - last_name: String, - start_offset: usize, - end_offset: usize, - distinct_trailer_hex_words: Vec, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeCandidateTableNamedRunScanSample { - path: String, - profile_family: String, - source_kind: String, - observed_entry_count: usize, - port_runs: Vec, - warehouse_runs: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeCandidateTableNamedRunScanReport { - root_path: String, - file_count: usize, - files_with_probe_count: usize, - files_with_any_numbered_port_runs_count: usize, - files_with_any_numbered_warehouse_runs_count: usize, - files_with_both_numbered_run_families_count: usize, - skipped_file_count: usize, - samples: Vec, -} - -#[derive(Debug, Clone, Serialize)] -struct RuntimeSpecialConditionsScanSample { - path: String, - profile_family: String, - source_kind: String, - enabled_visible_count: usize, - enabled_visible_labels: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeSpecialConditionsSlotSummary { - slot_index: u8, - label: String, - file_count_enabled: usize, - sample_paths: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeSpecialConditionsScanReport { - root_path: String, - file_count: usize, - files_with_probe_count: usize, - files_with_any_enabled_count: usize, - skipped_file_count: usize, - enabled_slot_summaries: Vec, - sample_files_with_any_enabled: Vec, -} - -#[derive(Debug, Clone)] -struct RuntimePostSpecialConditionsScalarScanSample { - path: String, - profile_family: String, - source_kind: String, - nonzero_relative_offsets: Vec, - values_by_relative_offset_hex: BTreeMap, -} - -#[derive(Debug, Serialize)] -struct RuntimePostSpecialConditionsScalarOffsetSummary { - relative_offset_hex: String, - file_count_present: usize, - distinct_value_count: usize, - sample_value_hexes: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimePostSpecialConditionsScalarFamilySummary { - profile_family: String, - source_kinds: Vec, - file_count: usize, - files_with_any_nonzero_count: usize, - distinct_nonzero_offset_set_count: usize, - stable_nonzero_relative_offset_hexes: Vec, - union_nonzero_relative_offset_hexes: Vec, - offset_summaries: Vec, - sample_paths: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimePostSpecialConditionsScalarScanReport { - root_path: String, - file_count: usize, - files_with_probe_count: usize, - files_with_any_nonzero_count: usize, - skipped_file_count: usize, - family_summaries: Vec, -} - -#[derive(Debug, Clone)] -struct RuntimePostSpecialConditionsTailScanSample { - path: String, - profile_family: String, - source_kind: String, - nonzero_relative_offsets: Vec, - values_by_relative_offset_hex: BTreeMap, -} - -#[derive(Debug, Serialize)] -struct RuntimePostSpecialConditionsTailOffsetSummary { - relative_offset_hex: String, - file_count_present: usize, - distinct_value_count: usize, - sample_value_hexes: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimePostSpecialConditionsTailFamilySummary { - profile_family: String, - source_kinds: Vec, - file_count: usize, - files_with_any_nonzero_count: usize, - distinct_nonzero_offset_set_count: usize, - stable_nonzero_relative_offset_hexes: Vec, - union_nonzero_relative_offset_hexes: Vec, - offset_summaries: Vec, - sample_paths: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimePostSpecialConditionsTailScanReport { - root_path: String, - file_count: usize, - files_with_probe_count: usize, - files_with_any_nonzero_count: usize, - skipped_file_count: usize, - family_summaries: Vec, -} - -#[derive(Debug, Clone)] -struct RuntimeAlignedRuntimeRuleBandScanSample { - path: String, - profile_family: String, - source_kind: String, - nonzero_band_indices: Vec, - values_by_band_index: BTreeMap, -} - -#[derive(Debug, Serialize)] -struct RuntimeAlignedRuntimeRuleBandOffsetSummary { - band_index: usize, - relative_offset_hex: String, - lane_kind: String, - known_label: Option, - file_count_present: usize, - distinct_value_count: usize, - sample_value_hexes: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeAlignedRuntimeRuleBandFamilySummary { - profile_family: String, - source_kinds: Vec, - file_count: usize, - files_with_any_nonzero_count: usize, - distinct_nonzero_index_set_count: usize, - stable_nonzero_band_indices: Vec, - union_nonzero_band_indices: Vec, - offset_summaries: Vec, - sample_paths: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeAlignedRuntimeRuleBandScanReport { - root_path: String, - file_count: usize, - files_with_probe_count: usize, - files_with_any_nonzero_count: usize, - skipped_file_count: usize, - family_summaries: Vec, -} - -#[derive(Debug, Serialize)] -struct RuntimeProfileBlockExportDocument { - source_path: String, - profile_kind: String, - profile_family: String, - payload: Value, -} - -#[derive(Debug, Serialize)] -struct RuntimeProfileBlockExportReport { - output_path: String, - profile_kind: String, - profile_family: String, -} +mod app; fn main() { - if let Err(err) = real_main() { + if let Err(err) = app::run() { eprintln!("error: {err}"); std::process::exit(1); } } - -fn real_main() -> Result<(), Box> { - match parse_command()? { - Command::Validate { repo_root } => { - 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"); - } - Command::FinanceEval { snapshot_path } => { - run_finance_eval(&snapshot_path)?; - } - Command::FinanceDiff { - left_path, - right_path, - } => { - run_finance_diff(&left_path, &right_path)?; - } - Command::RuntimeValidateFixture { fixture_path } => { - run_runtime_validate_fixture(&fixture_path)?; - } - Command::RuntimeSummarizeFixture { fixture_path } => { - run_runtime_summarize_fixture(&fixture_path)?; - } - Command::RuntimeExportFixtureState { - fixture_path, - output_path, - } => { - run_runtime_export_fixture_state(&fixture_path, &output_path)?; - } - Command::RuntimeDiffState { - left_path, - right_path, - } => { - run_runtime_diff_state(&left_path, &right_path)?; - } - Command::RuntimeSummarizeState { snapshot_path } => { - run_runtime_summarize_state(&snapshot_path)?; - } - Command::RuntimeImportState { - input_path, - output_path, - } => { - run_runtime_import_state(&input_path, &output_path)?; - } - Command::RuntimeInspectSmp { smp_path } => { - run_runtime_inspect_smp(&smp_path)?; - } - Command::RuntimeInspectCandidateTable { smp_path } => { - run_runtime_inspect_candidate_table(&smp_path)?; - } - Command::RuntimeInspectCompactEventDispatchCluster { root_path } => { - run_runtime_inspect_compact_event_dispatch_cluster(&root_path)?; - } - Command::RuntimeInspectCompactEventDispatchClusterCounts { root_path } => { - run_runtime_inspect_compact_event_dispatch_cluster_counts(&root_path)?; - } - Command::RuntimeInspectMapTitleHints { root_path } => { - run_runtime_inspect_map_title_hints(&root_path)?; - } - Command::RuntimeSummarizeSaveLoad { smp_path } => { - run_runtime_summarize_save_load(&smp_path)?; - } - Command::RuntimeLoadSaveSlice { smp_path } => { - run_runtime_load_save_slice(&smp_path)?; - } - Command::RuntimeInspectSaveCompanyChairman { smp_path } => { - run_runtime_inspect_save_company_chairman(&smp_path)?; - } - Command::RuntimeInspectSavePlacedStructureTriplets { smp_path } => { - run_runtime_inspect_save_placed_structure_triplets(&smp_path)?; - } - Command::RuntimeCompareRegionFixedRowRuns { - left_path, - right_path, - } => { - run_runtime_compare_region_fixed_row_runs(&left_path, &right_path)?; - } - Command::RuntimeInspectPeriodicCompanyServiceTrace { smp_path } => { - run_runtime_inspect_periodic_company_service_trace(&smp_path)?; - } - Command::RuntimeInspectRegionServiceTrace { smp_path } => { - run_runtime_inspect_region_service_trace(&smp_path)?; - } - Command::RuntimeInspectInfrastructureAssetTrace { smp_path } => { - run_runtime_inspect_infrastructure_asset_trace(&smp_path)?; - } - Command::RuntimeInspectSaveRegionQueuedNoticeRecords { smp_path } => { - run_runtime_inspect_save_region_queued_notice_records(&smp_path)?; - } - Command::RuntimeInspectPlacedStructureDynamicSideBuffer { smp_path } => { - run_runtime_inspect_placed_structure_dynamic_side_buffer(&smp_path)?; - } - Command::RuntimeInspectUnclassifiedSaveCollections { smp_path } => { - run_runtime_inspect_unclassified_save_collections(&smp_path)?; - } - Command::RuntimeImportSaveState { - smp_path, - output_path, - } => { - run_runtime_import_save_state(&smp_path, &output_path)?; - } - Command::RuntimeExportSaveSlice { - smp_path, - output_path, - } => { - run_runtime_export_save_slice(&smp_path, &output_path)?; - } - Command::RuntimeExportOverlayImport { - snapshot_path, - save_slice_path, - output_path, - } => { - run_runtime_export_overlay_import(&snapshot_path, &save_slice_path, &output_path)?; - } - Command::RuntimeInspectPk4 { pk4_path } => { - run_runtime_inspect_pk4(&pk4_path)?; - } - Command::RuntimeInspectCargoTypes { cargo_types_dir } => { - run_runtime_inspect_cargo_types(&cargo_types_dir)?; - } - Command::RuntimeInspectBuildingTypeSources { - building_types_dir, - bindings_path, - } => { - run_runtime_inspect_building_type_sources( - &building_types_dir, - bindings_path.as_deref(), - )?; - } - Command::RuntimeInspectCargoSkins { - cargo_skin_pk4_path, - } => { - run_runtime_inspect_cargo_skins(&cargo_skin_pk4_path)?; - } - Command::RuntimeInspectCargoEconomySources { - cargo_types_dir, - cargo_skin_pk4_path, - } => { - run_runtime_inspect_cargo_economy_sources(&cargo_types_dir, &cargo_skin_pk4_path)?; - } - Command::RuntimeInspectCargoProductionSelector { - cargo_types_dir, - cargo_skin_pk4_path, - } => { - run_runtime_inspect_cargo_production_selector(&cargo_types_dir, &cargo_skin_pk4_path)?; - } - Command::RuntimeInspectCargoPriceSelector { - cargo_types_dir, - cargo_skin_pk4_path, - } => { - run_runtime_inspect_cargo_price_selector(&cargo_types_dir, &cargo_skin_pk4_path)?; - } - Command::RuntimeInspectWin { win_path } => { - run_runtime_inspect_win(&win_path)?; - } - Command::RuntimeExtractPk4Entry { - pk4_path, - entry_name, - output_path, - } => { - run_runtime_extract_pk4_entry(&pk4_path, &entry_name, &output_path)?; - } - Command::RuntimeInspectCampaignExe { exe_path } => { - run_runtime_inspect_campaign_exe(&exe_path)?; - } - Command::RuntimeCompareClassicProfile { smp_paths } => { - run_runtime_compare_classic_profile(&smp_paths)?; - } - Command::RuntimeCompareRt3105Profile { smp_paths } => { - run_runtime_compare_rt3_105_profile(&smp_paths)?; - } - Command::RuntimeCompareCandidateTable { smp_paths } => { - run_runtime_compare_candidate_table(&smp_paths)?; - } - Command::RuntimeCompareRecipeBookLines { smp_paths } => { - run_runtime_compare_recipe_book_lines(&smp_paths)?; - } - Command::RuntimeCompareSetupPayloadCore { smp_paths } => { - run_runtime_compare_setup_payload_core(&smp_paths)?; - } - Command::RuntimeCompareSetupLaunchPayload { smp_paths } => { - run_runtime_compare_setup_launch_payload(&smp_paths)?; - } - Command::RuntimeComparePostSpecialConditionsScalars { smp_paths } => { - run_runtime_compare_post_special_conditions_scalars(&smp_paths)?; - } - Command::RuntimeScanCandidateTableHeaders { root_path } => { - run_runtime_scan_candidate_table_headers(&root_path)?; - } - Command::RuntimeScanCandidateTableNamedRuns { root_path } => { - run_runtime_scan_candidate_table_named_runs(&root_path)?; - } - Command::RuntimeScanSpecialConditions { root_path } => { - run_runtime_scan_special_conditions(&root_path)?; - } - Command::RuntimeScanAlignedRuntimeRuleBand { root_path } => { - run_runtime_scan_aligned_runtime_rule_band(&root_path)?; - } - Command::RuntimeScanPostSpecialConditionsScalars { root_path } => { - run_runtime_scan_post_special_conditions_scalars(&root_path)?; - } - Command::RuntimeScanPostSpecialConditionsTail { root_path } => { - run_runtime_scan_post_special_conditions_tail(&root_path)?; - } - Command::RuntimeScanRecipeBookLines { root_path } => { - run_runtime_scan_recipe_book_lines(&root_path)?; - } - Command::RuntimeExportProfileBlock { - smp_path, - output_path, - } => { - run_runtime_export_profile_block(&smp_path, &output_path)?; - } - } - - Ok(()) -} - -fn parse_command() -> Result> { - let args: Vec = env::args().skip(1).collect(); - match args.as_slice() { - [] => Ok(Command::Validate { - repo_root: env::current_dir()?, - }), - [command] if command == "validate" => Ok(Command::Validate { - repo_root: env::current_dir()?, - }), - [command, path] if command == "validate" => Ok(Command::Validate { - repo_root: PathBuf::from(path), - }), - [command, subcommand, path] if command == "finance" && subcommand == "eval" => { - Ok(Command::FinanceEval { - snapshot_path: PathBuf::from(path), - }) - } - [command, subcommand, left, right] if command == "finance" && subcommand == "diff" => { - Ok(Command::FinanceDiff { - left_path: PathBuf::from(left), - right_path: PathBuf::from(right), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "validate-fixture" => - { - Ok(Command::RuntimeValidateFixture { - fixture_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "summarize-fixture" => - { - Ok(Command::RuntimeSummarizeFixture { - fixture_path: PathBuf::from(path), - }) - } - [command, subcommand, fixture_path, output_path] - if command == "runtime" && subcommand == "export-fixture-state" => - { - Ok(Command::RuntimeExportFixtureState { - fixture_path: PathBuf::from(fixture_path), - output_path: PathBuf::from(output_path), - }) - } - [command, subcommand, left_path, right_path] - if command == "runtime" && subcommand == "diff-state" => - { - Ok(Command::RuntimeDiffState { - left_path: PathBuf::from(left_path), - right_path: PathBuf::from(right_path), - }) - } - [command, subcommand, path] if command == "runtime" && subcommand == "summarize-state" => { - Ok(Command::RuntimeSummarizeState { - snapshot_path: PathBuf::from(path), - }) - } - [command, subcommand, input_path, output_path] - if command == "runtime" && subcommand == "import-state" => - { - Ok(Command::RuntimeImportState { - input_path: PathBuf::from(input_path), - output_path: PathBuf::from(output_path), - }) - } - [command, subcommand, path] if command == "runtime" && subcommand == "inspect-smp" => { - Ok(Command::RuntimeInspectSmp { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-candidate-table" => - { - Ok(Command::RuntimeInspectCandidateTable { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, root_path] - if command == "runtime" - && subcommand == "inspect-compact-event-dispatch-cluster" => - { - Ok(Command::RuntimeInspectCompactEventDispatchCluster { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, root_path] - if command == "runtime" - && subcommand == "inspect-compact-event-dispatch-cluster-counts" => - { - Ok(Command::RuntimeInspectCompactEventDispatchClusterCounts { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, root_path] - if command == "runtime" && subcommand == "inspect-map-title-hints" => - { - Ok(Command::RuntimeInspectMapTitleHints { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "summarize-save-load" => - { - Ok(Command::RuntimeSummarizeSaveLoad { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "load-save-slice" => - { - Ok(Command::RuntimeLoadSaveSlice { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-save-company-chairman" => - { - Ok(Command::RuntimeInspectSaveCompanyChairman { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-save-placed-structure-triplets" => - { - Ok(Command::RuntimeInspectSavePlacedStructureTriplets { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, left_path, right_path] - if command == "runtime" && subcommand == "compare-region-fixed-row-runs" => - { - Ok(Command::RuntimeCompareRegionFixedRowRuns { - left_path: PathBuf::from(left_path), - right_path: PathBuf::from(right_path), - }) - } - [command, subcommand, path] - if command == "runtime" - && subcommand == "inspect-periodic-company-service-trace" => - { - Ok(Command::RuntimeInspectPeriodicCompanyServiceTrace { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-region-service-trace" => - { - Ok(Command::RuntimeInspectRegionServiceTrace { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-infrastructure-asset-trace" => - { - Ok(Command::RuntimeInspectInfrastructureAssetTrace { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" - && subcommand == "inspect-save-region-queued-notice-records" => - { - Ok(Command::RuntimeInspectSaveRegionQueuedNoticeRecords { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" - && subcommand == "inspect-placed-structure-dynamic-side-buffer" => - { - Ok(Command::RuntimeInspectPlacedStructureDynamicSideBuffer { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-unclassified-save-collections" => - { - Ok(Command::RuntimeInspectUnclassifiedSaveCollections { - smp_path: PathBuf::from(path), - }) - } - [command, subcommand, smp_path, output_path] - if command == "runtime" && subcommand == "import-save-state" => - { - Ok(Command::RuntimeImportSaveState { - smp_path: PathBuf::from(smp_path), - output_path: PathBuf::from(output_path), - }) - } - [command, subcommand, smp_path, output_path] - if command == "runtime" && subcommand == "export-save-slice" => - { - Ok(Command::RuntimeExportSaveSlice { - smp_path: PathBuf::from(smp_path), - output_path: PathBuf::from(output_path), - }) - } - [command, subcommand, snapshot_path, save_slice_path, output_path] - if command == "runtime" && subcommand == "export-overlay-import" => - { - Ok(Command::RuntimeExportOverlayImport { - snapshot_path: PathBuf::from(snapshot_path), - save_slice_path: PathBuf::from(save_slice_path), - output_path: PathBuf::from(output_path), - }) - } - [command, subcommand, path] if command == "runtime" && subcommand == "inspect-pk4" => { - Ok(Command::RuntimeInspectPk4 { - pk4_path: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-cargo-types" => - { - Ok(Command::RuntimeInspectCargoTypes { - cargo_types_dir: PathBuf::from(path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-building-type-sources" => - { - Ok(Command::RuntimeInspectBuildingTypeSources { - building_types_dir: PathBuf::from(path), - bindings_path: None, - }) - } - [command, subcommand, path, bindings_path] - if command == "runtime" && subcommand == "inspect-building-type-sources" => - { - Ok(Command::RuntimeInspectBuildingTypeSources { - building_types_dir: PathBuf::from(path), - bindings_path: Some(PathBuf::from(bindings_path)), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-cargo-skins" => - { - Ok(Command::RuntimeInspectCargoSkins { - cargo_skin_pk4_path: PathBuf::from(path), - }) - } - [command, subcommand, cargo_types_dir, cargo_skin_pk4_path] - if command == "runtime" && subcommand == "inspect-cargo-economy-sources" => - { - Ok(Command::RuntimeInspectCargoEconomySources { - cargo_types_dir: PathBuf::from(cargo_types_dir), - cargo_skin_pk4_path: PathBuf::from(cargo_skin_pk4_path), - }) - } - [command, subcommand, cargo_types_dir, cargo_skin_pk4_path] - if command == "runtime" && subcommand == "inspect-cargo-production-selector" => - { - Ok(Command::RuntimeInspectCargoProductionSelector { - cargo_types_dir: PathBuf::from(cargo_types_dir), - cargo_skin_pk4_path: PathBuf::from(cargo_skin_pk4_path), - }) - } - [command, subcommand, cargo_types_dir, cargo_skin_pk4_path] - if command == "runtime" && subcommand == "inspect-cargo-price-selector" => - { - Ok(Command::RuntimeInspectCargoPriceSelector { - cargo_types_dir: PathBuf::from(cargo_types_dir), - cargo_skin_pk4_path: PathBuf::from(cargo_skin_pk4_path), - }) - } - [command, subcommand, path] if command == "runtime" && subcommand == "inspect-win" => { - Ok(Command::RuntimeInspectWin { - win_path: PathBuf::from(path), - }) - } - [command, subcommand, pk4_path, entry_name, output_path] - if command == "runtime" && subcommand == "extract-pk4-entry" => - { - Ok(Command::RuntimeExtractPk4Entry { - pk4_path: PathBuf::from(pk4_path), - entry_name: entry_name.clone(), - output_path: PathBuf::from(output_path), - }) - } - [command, subcommand, path] - if command == "runtime" && subcommand == "inspect-campaign-exe" => - { - Ok(Command::RuntimeInspectCampaignExe { - exe_path: PathBuf::from(path), - }) - } - [command, subcommand, smp_paths @ ..] - if command == "runtime" - && subcommand == "compare-classic-profile" - && smp_paths.len() >= 2 => - { - Ok(Command::RuntimeCompareClassicProfile { - smp_paths: smp_paths.iter().map(PathBuf::from).collect(), - }) - } - [command, subcommand, smp_paths @ ..] - if command == "runtime" - && subcommand == "compare-105-profile" - && smp_paths.len() >= 2 => - { - Ok(Command::RuntimeCompareRt3105Profile { - smp_paths: smp_paths.iter().map(PathBuf::from).collect(), - }) - } - [command, subcommand, smp_paths @ ..] - if command == "runtime" - && subcommand == "compare-candidate-table" - && smp_paths.len() >= 2 => - { - Ok(Command::RuntimeCompareCandidateTable { - smp_paths: smp_paths.iter().map(PathBuf::from).collect(), - }) - } - [command, subcommand, smp_paths @ ..] - if command == "runtime" - && subcommand == "compare-recipe-book-lines" - && smp_paths.len() >= 2 => - { - Ok(Command::RuntimeCompareRecipeBookLines { - smp_paths: smp_paths.iter().map(PathBuf::from).collect(), - }) - } - [command, subcommand, smp_paths @ ..] - if command == "runtime" - && subcommand == "compare-setup-payload-core" - && smp_paths.len() >= 2 => - { - Ok(Command::RuntimeCompareSetupPayloadCore { - smp_paths: smp_paths.iter().map(PathBuf::from).collect(), - }) - } - [command, subcommand, smp_paths @ ..] - if command == "runtime" - && subcommand == "compare-setup-launch-payload" - && smp_paths.len() >= 2 => - { - Ok(Command::RuntimeCompareSetupLaunchPayload { - smp_paths: smp_paths.iter().map(PathBuf::from).collect(), - }) - } - [command, subcommand, smp_paths @ ..] - if command == "runtime" - && subcommand == "compare-post-special-conditions-scalars" - && smp_paths.len() >= 2 => - { - Ok(Command::RuntimeComparePostSpecialConditionsScalars { - smp_paths: smp_paths.iter().map(PathBuf::from).collect(), - }) - } - [command, subcommand, root_path] - if command == "runtime" && subcommand == "scan-candidate-table-headers" => - { - Ok(Command::RuntimeScanCandidateTableHeaders { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, root_path] - if command == "runtime" && subcommand == "scan-candidate-table-named-runs" => - { - Ok(Command::RuntimeScanCandidateTableNamedRuns { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, root_path] - if command == "runtime" && subcommand == "scan-special-conditions" => - { - Ok(Command::RuntimeScanSpecialConditions { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, root_path] - if command == "runtime" && subcommand == "scan-aligned-runtime-rule-band" => - { - Ok(Command::RuntimeScanAlignedRuntimeRuleBand { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, root_path] - if command == "runtime" && subcommand == "scan-post-special-conditions-scalars" => - { - Ok(Command::RuntimeScanPostSpecialConditionsScalars { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, root_path] - if command == "runtime" && subcommand == "scan-post-special-conditions-tail" => - { - Ok(Command::RuntimeScanPostSpecialConditionsTail { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, root_path] - if command == "runtime" && subcommand == "scan-recipe-book-lines" => - { - Ok(Command::RuntimeScanRecipeBookLines { - root_path: PathBuf::from(root_path), - }) - } - [command, subcommand, smp_path, output_path] - if command == "runtime" && subcommand == "export-profile-block" => - { - Ok(Command::RuntimeExportProfileBlock { - smp_path: PathBuf::from(smp_path), - output_path: PathBuf::from(output_path), - }) - } - _ => Err( - "usage: rrt-cli [validate [repo-root] | finance eval | finance diff | runtime validate-fixture | runtime summarize-fixture | runtime export-fixture-state | runtime diff-state | runtime summarize-state | runtime import-state | runtime inspect-smp | runtime inspect-candidate-table | runtime inspect-compact-event-dispatch-cluster | runtime inspect-compact-event-dispatch-cluster-counts | runtime inspect-map-title-hints | runtime summarize-save-load | runtime load-save-slice | runtime inspect-save-company-chairman | runtime inspect-save-placed-structure-triplets | runtime compare-region-fixed-row-runs | runtime inspect-periodic-company-service-trace | runtime inspect-region-service-trace | runtime inspect-infrastructure-asset-trace | runtime inspect-save-region-queued-notice-records | runtime inspect-placed-structure-dynamic-side-buffer | runtime inspect-unclassified-save-collections | runtime import-save-state | runtime export-save-slice | runtime export-overlay-import | runtime inspect-pk4 | runtime inspect-cargo-types | runtime inspect-building-type-sources [building-bindings.json] | runtime inspect-cargo-skins | runtime inspect-cargo-economy-sources | runtime inspect-cargo-production-selector | runtime inspect-cargo-price-selector | runtime inspect-win | runtime extract-pk4-entry | runtime inspect-campaign-exe | runtime compare-classic-profile [saveN.gms...] | runtime compare-105-profile [saveN.gms...] | runtime compare-candidate-table [fileN...] | runtime compare-recipe-book-lines [fileN...] | runtime compare-setup-payload-core [fileN...] | runtime compare-setup-launch-payload [fileN...] | runtime compare-post-special-conditions-scalars [fileN...] | runtime scan-candidate-table-headers | runtime scan-candidate-table-named-runs | runtime scan-special-conditions | runtime scan-aligned-runtime-rule-band | runtime scan-post-special-conditions-scalars | runtime scan-post-special-conditions-tail | runtime scan-recipe-book-lines | runtime export-profile-block ]" - .into(), - ), - } -} - -fn run_finance_eval(snapshot_path: &Path) -> Result<(), Box> { - let outcome = load_finance_outcome(snapshot_path)?; - println!("{}", serde_json::to_string_pretty(&outcome)?); - Ok(()) -} - -fn run_finance_diff(left_path: &Path, right_path: &Path) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_validate_fixture(fixture_path: &Path) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_summarize_fixture(fixture_path: &Path) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_export_fixture_state( - fixture_path: &Path, - output_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_summarize_state(snapshot_path: &Path) -> Result<(), Box> { - 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 import = load_runtime_state_import(snapshot_path)?; - let report = RuntimeStateSummaryReport { - snapshot_id: import.import_id, - summary: RuntimeSummary::from_state(&import.state), - }; - println!("{}", serde_json::to_string_pretty(&report)?); - Ok(()) -} - -fn run_runtime_diff_state( - left_path: &Path, - right_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn load_normalized_runtime_state(path: &Path) -> Result> { - 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 import = load_runtime_state_import(path)?; - normalize_runtime_state(&import.state) -} - -fn run_runtime_import_state( - input_path: &Path, - output_path: &Path, -) -> Result<(), Box> { - let import = load_runtime_state_import(input_path)?; - let snapshot = RuntimeSnapshotDocument { - format_version: SNAPSHOT_FORMAT_VERSION, - snapshot_id: format!("{}-snapshot", import.import_id), - source: RuntimeSnapshotSource { - source_fixture_id: None, - description: Some(match import.description { - Some(description) => format!( - "Imported runtime state from {} ({description})", - input_path.display() - ), - None => format!("Imported runtime state from {}", input_path.display()), - }), - }, - state: import.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(()) -} - -fn run_runtime_inspect_smp(smp_path: &Path) -> Result<(), Box> { - let report = RuntimeSmpInspectionOutput { - path: smp_path.display().to_string(), - inspection: inspect_smp_file(smp_path)?, - }; - println!("{}", serde_json::to_string_pretty(&report)?); - Ok(()) -} - -fn run_runtime_inspect_map_title_hints(root_path: &Path) -> Result<(), Box> { - 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::>(); - 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(()) -} - -fn run_runtime_inspect_compact_event_dispatch_cluster( - root_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_compact_event_dispatch_cluster_counts( - root_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn build_runtime_compact_event_dispatch_cluster_report( - root_path: &Path, -) -> Result> { - 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::::new(); - let mut dispatch_descriptor_occurrence_counts = BTreeMap::::new(); - let mut dispatch_descriptor_map_counts = BTreeMap::::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::::new(); - let mut add_building_descriptor_map_counts = BTreeMap::::new(); - let mut add_building_row_shape_occurrence_counts = BTreeMap::::new(); - let mut add_building_row_shape_map_counts = BTreeMap::::new(); - let mut add_building_signature_family_occurrence_counts = BTreeMap::::new(); - let mut add_building_signature_family_map_counts = BTreeMap::::new(); - let mut add_building_condition_tuple_occurrence_counts = BTreeMap::::new(); - let mut add_building_condition_tuple_map_counts = BTreeMap::::new(); - let mut add_building_signature_condition_cluster_occurrence_counts = - BTreeMap::::new(); - let mut add_building_signature_condition_cluster_map_counts = BTreeMap::::new(); - let mut signature_condition_cluster_descriptor_keys = - BTreeMap::>::new(); - let mut add_building_signature_condition_clusters = BTreeSet::::new(); - let mut dispatch_descriptor_occurrences = - BTreeMap::>::new(); - let mut unknown_descriptor_occurrences = - BTreeMap::>::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::::new(); - let mut map_add_building_descriptor_keys = BTreeSet::::new(); - let mut map_add_building_row_shapes = BTreeSet::::new(); - let mut map_add_building_signature_families = BTreeSet::::new(); - let mut map_add_building_condition_tuples = BTreeSet::::new(); - let mut map_add_building_signature_condition_clusters = BTreeSet::::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::>::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::>(); - 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::>(); - 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::>(); - 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::>()) - .unwrap_or_default(); - (cluster.clone(), keys) - }) - .collect::>(); - 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::>(); - (cluster.clone(), filtered) - }) - .collect::>(); - - 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, - }) -} - -fn compact_event_dispatch_add_building_descriptor_id(descriptor_id: u32) -> bool { - (503..=613).contains(&descriptor_id) -} - -fn run_runtime_summarize_save_load(smp_path: &Path) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_load_save_slice(smp_path: &Path) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_save_company_chairman( - smp_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_save_placed_structure_triplets( - smp_path: &Path, -) -> Result<(), Box> { - let analysis = inspect_save_company_and_chairman_analysis_file(smp_path)?; - println!( - "{}", - serde_json::to_string_pretty(&analysis.placed_structure_record_triplets)? - ); - Ok(()) -} - -fn run_runtime_compare_region_fixed_row_runs( - left_path: &Path, - right_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_periodic_company_service_trace( - smp_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_region_service_trace( - smp_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_infrastructure_asset_trace( - smp_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_save_region_queued_notice_records( - smp_path: &Path, -) -> Result<(), Box> { - println!( - "{}", - serde_json::to_string_pretty(&inspect_save_region_queued_notice_records_file(smp_path)?)? - ); - Ok(()) -} - -fn run_runtime_inspect_placed_structure_dynamic_side_buffer( - smp_path: &Path, -) -> Result<(), Box> { - println!( - "{}", - serde_json::to_string_pretty(&inspect_save_placed_structure_dynamic_side_buffer_file( - smp_path - )?)? - ); - Ok(()) -} - -fn run_runtime_inspect_unclassified_save_collections( - smp_path: &Path, -) -> Result<(), Box> { - println!( - "{}", - serde_json::to_string_pretty(&inspect_unclassified_save_collection_headers_file( - smp_path - )?)? - ); - Ok(()) -} - -fn run_runtime_import_save_state( - smp_path: &Path, - output_path: &Path, -) -> Result<(), Box> { - let save_slice = load_save_slice_file(smp_path)?; - let import = project_save_slice_to_runtime_state_import( - &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", import.import_id), - source: RuntimeSnapshotSource { - source_fixture_id: None, - description: import.description, - }, - state: import.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(()) -} - -fn run_runtime_export_save_slice( - smp_path: &Path, - output_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_export_overlay_import( - snapshot_path: &Path, - save_slice_path: &Path, - output_path: &Path, -) -> Result<(), Box> { - let report = - export_runtime_overlay_import_document(snapshot_path, save_slice_path, output_path)?; - println!("{}", serde_json::to_string_pretty(&report)?); - Ok(()) -} - -fn export_runtime_save_slice_document( - smp_path: &Path, - output_path: &Path, - save_slice: SmpLoadedSaveSlice, -) -> Result> { - 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, - }) -} - -fn export_runtime_overlay_import_document( - snapshot_path: &Path, - save_slice_path: &Path, - output_path: &Path, -) -> Result> { - 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, - }) -} - -fn run_runtime_inspect_pk4(pk4_path: &Path) -> Result<(), Box> { - let report = RuntimePk4InspectionOutput { - path: pk4_path.display().to_string(), - inspection: inspect_pk4_file(pk4_path)?, - }; - println!("{}", serde_json::to_string_pretty(&report)?); - Ok(()) -} - -fn run_runtime_inspect_cargo_types( - cargo_types_dir: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_building_type_sources( - building_types_dir: &Path, - bindings_path: Option<&Path>, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_cargo_skins( - cargo_skin_pk4_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_cargo_economy_sources( - cargo_types_dir: &Path, - cargo_skin_pk4_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_cargo_production_selector( - cargo_types_dir: &Path, - cargo_skin_pk4_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_cargo_price_selector( - cargo_types_dir: &Path, - cargo_skin_pk4_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_win(win_path: &Path) -> Result<(), Box> { - let report = RuntimeWinInspectionOutput { - path: win_path.display().to_string(), - inspection: inspect_win_file(win_path)?, - }; - println!("{}", serde_json::to_string_pretty(&report)?); - Ok(()) -} - -fn run_runtime_extract_pk4_entry( - pk4_path: &Path, - entry_name: &str, - output_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_inspect_campaign_exe(exe_path: &Path) -> Result<(), Box> { - let report = RuntimeCampaignExeInspectionOutput { - path: exe_path.display().to_string(), - inspection: inspect_campaign_exe_file(exe_path)?, - }; - println!("{}", serde_json::to_string_pretty(&report)?); - Ok(()) -} - -fn run_runtime_compare_classic_profile( - smp_paths: &[PathBuf], -) -> Result<(), Box> { - let samples = smp_paths - .iter() - .map(|path| load_classic_profile_sample(path)) - .collect::, _>>()?; - 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(()) -} - -fn run_runtime_compare_rt3_105_profile( - smp_paths: &[PathBuf], -) -> Result<(), Box> { - let samples = smp_paths - .iter() - .map(|path| load_rt3_105_profile_sample(path)) - .collect::, _>>()?; - 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(()) -} - -fn run_runtime_compare_candidate_table( - smp_paths: &[PathBuf], -) -> Result<(), Box> { - let samples = smp_paths - .iter() - .map(|path| load_candidate_table_sample(path)) - .collect::, _>>()?; - 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(()) -} - -fn run_runtime_inspect_candidate_table(smp_path: &Path) -> Result<(), Box> { - let report = load_candidate_table_inspection_report(smp_path)?; - println!("{}", serde_json::to_string_pretty(&report)?); - Ok(()) -} - -fn run_runtime_compare_recipe_book_lines( - smp_paths: &[PathBuf], -) -> Result<(), Box> { - let samples = smp_paths - .iter() - .map(|path| load_recipe_book_line_sample(path)) - .collect::, _>>()?; - 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(()) -} - -fn run_runtime_compare_setup_payload_core( - smp_paths: &[PathBuf], -) -> Result<(), Box> { - let samples = smp_paths - .iter() - .map(|path| load_setup_payload_core_sample(path)) - .collect::, _>>()?; - 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(()) -} - -fn run_runtime_compare_setup_launch_payload( - smp_paths: &[PathBuf], -) -> Result<(), Box> { - let samples = smp_paths - .iter() - .map(|path| load_setup_launch_payload_sample(path)) - .collect::, _>>()?; - 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(()) -} - -fn run_runtime_compare_post_special_conditions_scalars( - smp_paths: &[PathBuf], -) -> Result<(), Box> { - let samples = smp_paths - .iter() - .map(|path| load_post_special_conditions_scalar_sample(path)) - .collect::, _>>()?; - 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(()) -} - -fn run_runtime_scan_candidate_table_headers( - root_path: &Path, -) -> Result<(), Box> { - 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>::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::>() - .into_iter() - .collect::>(); - let mut source_kinds = samples - .iter() - .map(|sample| sample.source_kind.clone()) - .collect::>() - .into_iter() - .collect::>(); - let mut zero_trailer_count_values = samples - .iter() - .map(|sample| sample.zero_trailer_entry_count) - .collect::>() - .into_iter() - .collect::>(); - let distinct_zero_name_set_count = samples - .iter() - .map(|sample| sample.zero_trailer_entry_names.clone()) - .collect::>() - .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::>(); - 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::>(); - - 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(()) -} - -fn run_runtime_scan_candidate_table_named_runs( - root_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn run_runtime_scan_special_conditions(root_path: &Path) -> Result<(), Box> { - 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::>(); - let files_with_any_enabled_count = sample_files_with_any_enabled.len(); - - let mut grouped = BTreeMap::<(u8, String), Vec>::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::>(); - - 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(()) -} - -fn run_runtime_scan_aligned_runtime_rule_band( - root_path: &Path, -) -> Result<(), Box> { - 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::>::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::>() - .into_iter() - .collect::>(); - let distinct_nonzero_index_set_count = samples - .iter() - .map(|sample| sample.nonzero_band_indices.clone()) - .collect::>() - .len(); - - let stable_band_indices = if samples.is_empty() { - BTreeSet::new() - } else { - let mut stable = samples[0] - .nonzero_band_indices - .iter() - .copied() - .collect::>(); - for sample in samples.iter().skip(1) { - let current = sample - .nonzero_band_indices - .iter() - .copied() - .collect::>(); - stable = stable.intersection(¤t).copied().collect(); - } - stable - }; - - let mut band_values = BTreeMap::>::new(); - let mut band_counts = BTreeMap::::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::>(); - - 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::>(); - - 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(()) -} - -fn run_runtime_scan_post_special_conditions_scalars( - root_path: &Path, -) -> Result<(), Box> { - 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 mut grouped = BTreeMap::>::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_relative_offsets.is_empty()) - .count(); - let source_kinds = samples - .iter() - .map(|sample| sample.source_kind.clone()) - .collect::>() - .into_iter() - .collect::>(); - let distinct_nonzero_offset_set_count = samples - .iter() - .map(|sample| sample.nonzero_relative_offsets.clone()) - .collect::>() - .len(); - - let stable_offsets = if samples.is_empty() { - BTreeSet::new() - } else { - let mut stable = samples[0] - .nonzero_relative_offsets - .iter() - .copied() - .collect::>(); - for sample in samples.iter().skip(1) { - let current = sample - .nonzero_relative_offsets - .iter() - .copied() - .collect::>(); - stable = stable.intersection(¤t).copied().collect(); - } - stable - }; - - let mut offset_values = BTreeMap::>::new(); - let mut offset_counts = BTreeMap::::new(); - for sample in &samples { - for offset in &sample.nonzero_relative_offsets { - *offset_counts.entry(*offset).or_default() += 1; - } - for (offset_hex, value_hex) in &sample.values_by_relative_offset_hex { - if let Some(offset) = parse_hex_offset(offset_hex) { - offset_values - .entry(offset) - .or_default() - .insert(value_hex.clone()); - } - } - } - - let offset_summaries = 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::>(); - - RuntimePostSpecialConditionsScalarFamilySummary { - profile_family, - source_kinds, - file_count, - files_with_any_nonzero_count, - distinct_nonzero_offset_set_count, - stable_nonzero_relative_offset_hexes: stable_offsets - .into_iter() - .map(|offset| format!("0x{offset:x}")) - .collect(), - union_nonzero_relative_offset_hexes: offset_values - .keys() - .copied() - .map(|offset| format!("0x{offset:x}")) - .collect(), - offset_summaries, - sample_paths: samples - .iter() - .take(12) - .map(|sample| sample.path.clone()) - .collect(), - } - }) - .collect::>(); - - 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(()) -} - -fn run_runtime_scan_post_special_conditions_tail( - root_path: &Path, -) -> Result<(), Box> { - 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 mut grouped = BTreeMap::>::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_relative_offsets.is_empty()) - .count(); - let source_kinds = samples - .iter() - .map(|sample| sample.source_kind.clone()) - .collect::>() - .into_iter() - .collect::>(); - let distinct_nonzero_offset_set_count = samples - .iter() - .map(|sample| sample.nonzero_relative_offsets.clone()) - .collect::>() - .len(); - - let stable_offsets = if samples.is_empty() { - BTreeSet::new() - } else { - let mut stable = samples[0] - .nonzero_relative_offsets - .iter() - .copied() - .collect::>(); - for sample in samples.iter().skip(1) { - let current = sample - .nonzero_relative_offsets - .iter() - .copied() - .collect::>(); - stable = stable.intersection(¤t).copied().collect(); - } - stable - }; - - let mut offset_values = BTreeMap::>::new(); - let mut offset_counts = BTreeMap::::new(); - for sample in &samples { - for offset in &sample.nonzero_relative_offsets { - *offset_counts.entry(*offset).or_default() += 1; - } - for (offset_hex, value_hex) in &sample.values_by_relative_offset_hex { - if let Some(offset) = parse_hex_offset(offset_hex) { - offset_values - .entry(offset) - .or_default() - .insert(value_hex.clone()); - } - } - } - - let offset_summaries = 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::>(); - - RuntimePostSpecialConditionsTailFamilySummary { - profile_family, - source_kinds, - file_count, - files_with_any_nonzero_count, - distinct_nonzero_offset_set_count, - stable_nonzero_relative_offset_hexes: stable_offsets - .into_iter() - .map(|offset| format!("0x{offset:x}")) - .collect(), - union_nonzero_relative_offset_hexes: offset_values - .keys() - .copied() - .map(|offset| format!("0x{offset:x}")) - .collect(), - offset_summaries, - sample_paths: samples - .iter() - .take(12) - .map(|sample| sample.path.clone()) - .collect(), - } - }) - .collect::>(); - - 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(()) -} - -fn run_runtime_scan_recipe_book_lines(root_path: &Path) -> Result<(), Box> { - 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::>::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::>() - .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::>(); - - 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(()) -} - -fn run_runtime_export_profile_block( - smp_path: &Path, - output_path: &Path, -) -> Result<(), Box> { - 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(()) -} - -fn load_classic_profile_sample( - smp_path: &Path, -) -> Result> { - 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, - }) -} - -fn load_rt3_105_profile_sample( - smp_path: &Path, -) -> Result> { - 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, - }) -} - -fn load_candidate_table_sample( - smp_path: &Path, -) -> Result> { - 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(), - }) -} - -fn load_candidate_table_inspection_report( - smp_path: &Path, -) -> Result> { - 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, - }) -} - -fn load_recipe_book_line_sample( - smp_path: &Path, -) -> Result> { - 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, - }) -} - -fn load_recipe_book_line_scan_sample( - smp_path: &Path, -) -> Result> { - 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(), - }) -} - -fn load_setup_payload_core_sample( - smp_path: &Path, -) -> Result> { - 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}")), - }) -} - -fn load_setup_launch_payload_sample( - smp_path: &Path, -) -> Result> { - 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::>(); - let nonzero_campaign_selector_values = campaign_selector_values - .iter() - .filter_map(|(name, value)| (*value != 0).then_some((name.clone(), *value))) - .collect::>(); - - 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, - }) -} - -fn load_post_special_conditions_scalar_sample( - smp_path: &Path, -) -> Result> { - let sample = 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, - }) -} - -fn load_candidate_table_header_scan_sample( - smp_path: &Path, -) -> Result> { - 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, - }) -} - -fn load_candidate_table_named_run_scan_sample( - smp_path: &Path, -) -> Result> { - 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, - }) -} - -fn load_special_conditions_scan_sample( - smp_path: &Path, -) -> Result> { - 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::>(); - - 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, - }) -} - -fn load_post_special_conditions_scalar_scan_sample( - smp_path: &Path, -) -> Result> { - 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 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, - }) -} - -fn load_post_special_conditions_tail_scan_sample( - smp_path: &Path, -) -> Result> { - 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 post-special-conditions tail 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-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 - ..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 tail window", - smp_path.display() - ) - })?; - 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 load_aligned_runtime_rule_band_scan_sample( - smp_path: &Path, -) -> Result> { - let bytes = fs::read(smp_path)?; - if bytes.len() < SMP_ALIGNED_RUNTIME_RULE_END_OFFSET { - return Err(format!( - "{} is too small for the fixed aligned runtime-rule band", - 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-smp-aligned-runtime-rule-band", - "gms" => "save-smp-aligned-runtime-rule-band", - "gmx" => "sandbox-smp-aligned-runtime-rule-band", - _ => "smp-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 fixed 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, - }) -} - -fn collect_candidate_table_input_paths( - root_path: &Path, - out: &mut Vec, -) -> Result<(), Box> { - 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(()) -} - -fn collect_numbered_candidate_name_runs( - entries: &[RuntimeCandidateTableEntrySample], - prefix: &str, -) -> Vec { - let mut numbered_entries = entries - .iter() - .filter_map(|entry| { - parse_numbered_candidate_name(&entry.text, prefix).map(|number| (entry, number)) - }) - .collect::>(); - 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 -} - -fn parse_numbered_candidate_name(text: &str, prefix: &str) -> Option { - let digits = text.strip_prefix(prefix)?; - if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) { - return None; - } - digits.parse().ok() -} - -fn collect_special_conditions_input_paths( - root_path: &Path, - out: &mut Vec, -) -> Result<(), Box> { - 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(()) -} - -fn parse_special_condition_slot_index(label: &str) -> Option { - let suffix = label.strip_prefix("slot ")?; - let (slot_index, _) = suffix.split_once(':')?; - slot_index.parse().ok() -} - -fn compact_event_dispatch_strip_opcode(opcode: u8) -> bool { - matches!(opcode, 0x04..=0x08 | 0x0d | 0x10..=0x13 | 0x16) -} - -fn compact_event_signature_family_from_notes(notes: &[String]) -> Option { - notes.iter().find_map(|note| { - note.strip_prefix("compact signature family = ") - .map(ToString::to_string) - }) -} - -fn special_condition_label_for_compact_dispatch_descriptor( - descriptor_id: u32, -) -> Option<&'static str> { - let band_index = descriptor_id.checked_sub(535)? as usize; - SPECIAL_CONDITION_LABELS.get(band_index).copied() -} - -fn collect_compact_event_dispatch_cluster_input_paths( - root_path: &Path, - out: &mut Vec, -) -> Result<(), Box> { - 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(()) -} - -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()) -} - -fn compact_event_dispatch_row_shape_family( - grouped_rows: &BTreeMap>, -) -> 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::>(); - if parts.is_empty() { - return "[]".to_string(); - } - parts.sort(); - format!("[{}]", parts.join(",")) -} - -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::>(); - format!("[{}]", parts.join(",")) -} - -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) - ) -} - -fn parse_hex_offset(text: &str) -> Option { - text.strip_prefix("0x") - .and_then(|digits| usize::from_str_radix(digits, 16).ok()) -} - -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" - } -} - -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 - } -} - -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), - ) - ) -} - -fn classify_candidate_table_header_profile(extension: Option, 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(), - } -} - -fn read_u32_le(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 4)?; - Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) -} - -fn read_u16_le(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 2)?; - Some(u16::from_le_bytes([chunk[0], chunk[1]])) -} - -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 -} - -fn diff_classic_profile_samples( - samples: &[RuntimeClassicProfileSample], -) -> Result, Box> { - 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::>(); - let mut differences = Vec::new(); - collect_json_multi_differences("$", &labeled_values, &mut differences); - Ok(differences) -} - -fn diff_rt3_105_profile_samples( - samples: &[RuntimeRt3105ProfileSample], -) -> Result, Box> { - 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::>(); - let mut differences = Vec::new(); - collect_json_multi_differences("$", &labeled_values, &mut differences); - Ok(differences) -} - -fn diff_candidate_table_samples( - samples: &[RuntimeCandidateTableSample], -) -> Result, Box> { - 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::>(); - let mut differences = Vec::new(); - collect_json_multi_differences("$", &labeled_values, &mut differences); - Ok(differences) -} - -fn diff_recipe_book_line_samples( - samples: &[RuntimeRecipeBookLineSample], -) -> Result, Box> { - 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::>(); - let mut differences = Vec::new(); - collect_json_multi_differences("$", &labeled_values, &mut differences); - Ok(differences) -} - -fn diff_recipe_book_line_content_samples( - samples: &[RuntimeRecipeBookLineSample], -) -> Result, Box> { - 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::>(); - let mut differences = Vec::new(); - collect_json_multi_differences("$", &labeled_values, &mut differences); - Ok(differences) -} - -fn intersect_nonzero_recipe_line_paths<'a>( - maps: impl Iterator>, -) -> Vec { - let mut maps = maps.peekable(); - if maps.peek().is_none() { - return Vec::new(); - } - - let mut stable = maps - .next() - .map(|map| map.keys().cloned().collect::>()) - .unwrap_or_default(); - for map in maps { - let current = map.keys().cloned().collect::>(); - stable = stable.intersection(¤t).cloned().collect(); - } - stable.into_iter().collect() -} - -fn build_recipe_line_field_summaries<'a>( - maps: impl Iterator>, -) -> Vec { - let mut value_sets = BTreeMap::>::new(); - let mut counts = BTreeMap::::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() -} - -fn diff_setup_payload_core_samples( - samples: &[RuntimeSetupPayloadCoreSample], -) -> Result, Box> { - 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::>(); - let mut differences = Vec::new(); - collect_json_multi_differences("$", &labeled_values, &mut differences); - Ok(differences) -} - -fn diff_setup_launch_payload_samples( - samples: &[RuntimeSetupLaunchPayloadSample], -) -> Result, Box> { - 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::>(); - let mut differences = Vec::new(); - collect_json_multi_differences("$", &labeled_values, &mut differences); - Ok(differences) -} - -fn diff_post_special_conditions_scalar_samples( - samples: &[RuntimePostSpecialConditionsScalarSample], -) -> Result, Box> { - 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::>(); - let mut differences = Vec::new(); - collect_json_multi_differences("$", &labeled_values, &mut differences); - Ok(differences) -} - -fn build_profile_block_export_document( - smp_path: &Path, - inspection: &SmpInspectionReport, -) -> Result> { - 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()) -} - -fn collect_json_multi_differences( - path: &str, - labeled_values: &[(String, Value)], - differences: &mut Vec, -) { - 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::>(); - 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::>(); - 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(), - }); -} - -fn print_runtime_validation_report( - report: &FixtureValidationReport, -) -> Result<(), Box> { - println!("{}", serde_json::to_string_pretty(report)?); - Ok(()) -} - -fn load_finance_outcome(path: &Path) -> Result> { - let text = fs::read_to_string(path)?; - if let Ok(snapshot) = serde_json::from_str::(&text) { - return Ok(snapshot.evaluate()); - } - if let Ok(outcome) = serde_json::from_str::(&text) { - return Ok(outcome); - } - - Err(format!( - "unable to parse {} as FinanceSnapshot or FinanceOutcome", - path.display() - ) - .into()) -} - -fn diff_finance_outcomes( - left: &FinanceOutcome, - right: &FinanceOutcome, -) -> Result> { - 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, -) { - 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(), - }), - _ => {} - } -} - -fn validate_required_files(repo_root: &Path) -> Result<(), Box> { - 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(()) -} - -fn validate_binary_summary(repo_root: &Path) -> Result<(), Box> { - 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(()) -} - -fn validate_function_map(repo_root: &Path) -> Result<(), Box> { - 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(()) -} - -fn validate_control_loop_atlas(repo_root: &Path) -> Result<(), Box> { - 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> { - 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())) -} - -#[cfg(test)] -mod tests { - use super::*; - use rrt_model::finance::{ - AnnualFinanceDecision, AnnualFinanceEvaluation, CompanyFinanceState, DebtRestructureSummary, - }; - use rrt_runtime::{SmpPackedProfileWordLane, SmpRt3105PackedProfileBlock}; - - #[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); - } - - #[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); - assert!( - report - .differences - .iter() - .any(|entry| entry.path == "$.post_company.current_cash") - ); - assert!( - report - .differences - .iter() - .any(|entry| entry.path == "$.evaluation.debt_restructure.retired_principal") - ); - } - - #[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); - - run_runtime_summarize_fixture(&path).expect("fixture summary should succeed"); - - let _ = fs::remove_file(path); - } - - #[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() - )); - - run_runtime_export_fixture_state(&fixture_path, &snapshot_path) - .expect("fixture export should succeed"); - run_runtime_summarize_state(&snapshot_path).expect("snapshot summary should succeed"); - - let _ = fs::remove_file(fixture_path); - let _ = fs::remove_file(snapshot_path); - } - - #[test] - fn imports_runtime_state_dump_into_snapshot() { - let dump = serde_json::json!({ - "format_version": 1, - "dump_id": "runtime-dump-test", - "source": { - "description": "test raw runtime dump" - }, - "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-dump", &dump); - let output_path = std::env::temp_dir().join(format!( - "rrt-cli-runtime-import-{}.json", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system time should be after epoch") - .as_nanos() - )); - - run_runtime_import_state(&input_path, &output_path).expect("runtime import should succeed"); - run_runtime_summarize_state(&output_path).expect("imported snapshot should summarize"); - - let _ = fs::remove_file(input_path); - let _ = fs::remove_file(output_path); - } - - #[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); - - run_runtime_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); - - assert!( - differences - .iter() - .any(|entry| entry.path == "$.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 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"); - - run_runtime_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"); - - run_runtime_summarize_fixture(&fixture_path) - .expect("snapshot-backed imported packed-event fixture should summarize"); - } - - #[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", - ); - - run_runtime_summarize_fixture(&parity_fixture) - .expect("save-slice-backed parity fixture should summarize"); - run_runtime_summarize_fixture(&selective_fixture) - .expect("save-slice-backed selective-import fixture should summarize"); - run_runtime_summarize_fixture(&overlay_fixture) - .expect("overlay-backed selective-import fixture should summarize"); - run_runtime_summarize_fixture(&symbolic_overlay_fixture) - .expect("overlay-backed symbolic-target fixture should summarize"); - run_runtime_summarize_fixture(&negative_company_scope_overlay_fixture) - .expect("overlay-backed negative-sentinel company-scope fixture should summarize"); - run_runtime_summarize_fixture(&deactivate_overlay_fixture) - .expect("overlay-backed deactivate-company fixture should summarize"); - run_runtime_summarize_fixture(&track_capacity_overlay_fixture) - .expect("overlay-backed track-capacity fixture should summarize"); - run_runtime_summarize_fixture(&mixed_overlay_fixture) - .expect("overlay-backed mixed real-row fixture should summarize"); - run_runtime_summarize_fixture(&named_locomotive_fixture) - .expect("save-slice-backed named locomotive availability fixture should summarize"); - run_runtime_summarize_fixture(&missing_catalog_fixture).expect( - "save-slice-backed locomotive availability missing-catalog fixture should summarize", - ); - run_runtime_summarize_fixture(&save_locomotive_fixture).expect( - "save-slice-backed locomotive availability descriptor fixture should summarize", - ); - run_runtime_summarize_fixture(&overlay_locomotive_fixture) - .expect("overlay-backed locomotive availability fixture should summarize"); - run_runtime_summarize_fixture(&save_locomotive_cost_fixture) - .expect("save-slice-backed locomotive cost fixture should summarize"); - run_runtime_summarize_fixture(&overlay_locomotive_cost_fixture) - .expect("overlay-backed locomotive cost fixture should summarize"); - run_runtime_summarize_fixture(&scalar_band_parity_fixture) - .expect("save-slice-backed recovered scalar-band parity fixture should summarize"); - run_runtime_summarize_fixture(&world_scalar_executable_fixture) - .expect("save-slice-backed executable world-scalar fixture should summarize"); - run_runtime_summarize_fixture(&world_scalar_override_fixture) - .expect("save-slice-backed world-scalar override fixture should summarize"); - run_runtime_summarize_fixture(&runtime_variable_overlay_fixture) - .expect("overlay-backed runtime-variable fixture should summarize"); - run_runtime_summarize_fixture(&runtime_variable_condition_overlay_fixture) - .expect("overlay-backed runtime-variable condition fixture should summarize"); - run_runtime_summarize_fixture(&cargo_economics_fixture) - .expect("save-slice-backed cargo-economics fixture should summarize"); - run_runtime_summarize_fixture(&cargo_economics_parity_fixture) - .expect("save-slice-backed cargo-economics parity fixture should summarize"); - run_runtime_summarize_fixture(&add_building_shell_fixture) - .expect("save-slice-backed add-building shell fixture should summarize"); - run_runtime_summarize_fixture(&world_scalar_condition_fixture) - .expect("save-slice-backed executable world-scalar condition fixture should summarize"); - run_runtime_summarize_fixture(&world_scalar_condition_parity_fixture) - .expect("save-slice-backed parity world-scalar condition fixture should summarize"); - run_runtime_summarize_fixture(&cargo_catalog_fixture) - .expect("save-slice-backed cargo catalog fixture should summarize"); - run_runtime_summarize_fixture(&chairman_cash_overlay_fixture) - .expect("overlay-backed chairman-cash fixture should summarize"); - run_runtime_summarize_fixture(&chairman_cash_save_fixture) - .expect("save-slice-backed chairman-cash fixture should summarize"); - run_runtime_summarize_fixture(&chairman_condition_true_save_fixture) - .expect("save-slice-backed condition-true chairman fixture should summarize"); - run_runtime_summarize_fixture(&chairman_human_cash_save_fixture) - .expect("save-slice-backed human-chairman cash fixture should summarize"); - run_runtime_summarize_fixture(&deactivate_chairman_overlay_fixture) - .expect("overlay-backed deactivate-chairman fixture should summarize"); - run_runtime_summarize_fixture(&deactivate_chairman_save_fixture) - .expect("save-slice-backed deactivate-chairman fixture should summarize"); - run_runtime_summarize_fixture(&deactivate_chairman_ai_save_fixture) - .expect("save-slice-backed AI-chairman deactivate fixture should summarize"); - run_runtime_summarize_fixture(&deactivate_company_save_fixture) - .expect("save-slice-backed deactivate-company fixture should summarize"); - run_runtime_summarize_fixture(&track_capacity_save_fixture) - .expect("save-slice-backed track-capacity fixture should summarize"); - run_runtime_summarize_fixture(&negative_company_scope_save_fixture) - .expect("save-slice-backed negative-sentinel company-scope fixture should summarize"); - run_runtime_summarize_fixture(&missing_chairman_context_fixture) - .expect("save-slice-backed chairman missing-context fixture should summarize"); - run_runtime_summarize_fixture(&chairman_scope_parity_fixture) - .expect("save-slice-backed chairman scope parity fixture should summarize"); - run_runtime_summarize_fixture(&chairman_condition_overlay_fixture) - .expect("overlay-backed chairman condition fixture should summarize"); - run_runtime_summarize_fixture(&chairman_condition_save_fixture) - .expect("save-slice-backed chairman condition fixture should summarize"); - run_runtime_summarize_fixture(&company_governance_condition_overlay_fixture) - .expect("overlay-backed company governance condition fixture should summarize"); - run_runtime_summarize_fixture(&company_governance_condition_save_fixture) - .expect("save-slice-backed company governance condition fixture should summarize"); - run_runtime_summarize_fixture(&selection_only_context_overlay_fixture) - .expect("overlay-backed selection-only save context fixture should summarize"); - run_runtime_summarize_fixture(&credit_rating_descriptor_save_fixture) - .expect("save-slice-backed credit-rating descriptor fixture should summarize"); - run_runtime_summarize_fixture(&stock_prices_shell_save_fixture) - .expect("save-slice-backed shell-owned stock-prices fixture should summarize"); - run_runtime_summarize_fixture(&game_won_shell_save_fixture) - .expect("save-slice-backed shell-owned game-won fixture should summarize"); - run_runtime_summarize_fixture(&merger_premium_shell_save_fixture) - .expect("save-slice-backed shell-owned merger-premium fixture should summarize"); - run_runtime_summarize_fixture(&set_human_control_shell_save_fixture) - .expect("save-slice-backed shell-owned set-human-control fixture should summarize"); - run_runtime_summarize_fixture(&investor_confidence_condition_save_fixture) - .expect("save-slice-backed investor-confidence condition fixture should summarize"); - run_runtime_summarize_fixture(&management_attitude_condition_save_fixture) - .expect("save-slice-backed management-attitude condition fixture should summarize"); - } - - #[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 = rrt_runtime::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 = rrt_runtime::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); - } - - #[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); - - 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!( - differences - .iter() - .any(|entry| entry.path == "$.event_runtime_records[0]") - ); - - let _ = fs::remove_file(left_path); - let _ = fs::remove_file(right_path); - } - - #[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" - })); - } - - #[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") - ); - } - - fn write_temp_json(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 - } -} diff --git a/crates/rrt-fixtures/src/lib.rs b/crates/rrt-fixtures/src/lib.rs index 289914e..888dda4 100644 --- a/crates/rrt-fixtures/src/lib.rs +++ b/crates/rrt-fixtures/src/lib.rs @@ -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}; diff --git a/crates/rrt-fixtures/src/load.rs b/crates/rrt-fixtures/src/load.rs index 4efd0d8..689e04d 100644 --- a/crates/rrt-fixtures/src/load.rs +++ b/crates/rrt-fixtures/src/load.rs @@ -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,64 +462,62 @@ mod tests { placed_structure_collection: None, placed_structure_dynamic_side_buffer_summary: None, special_conditions_table: None, - event_runtime_collection: Some( - rrt_runtime::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - 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 { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(48), - decode_status: "parity_only".to_string(), - payload_family: "synthetic_harness".to_string(), - trigger_kind: Some(7), - active: Some(true), - marks_collection_dirty: Some(false), - one_shot: Some(false), - compact_control: None, - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - 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] }, - delta: 25, - }], - executable_import_ready: false, - notes: vec![], + 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(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + control_lane_notes: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(48), + decode_status: "parity_only".to_string(), + payload_family: "synthetic_harness".to_string(), + trigger_kind: Some(7), + active: Some(true), + marks_collection_dirty: Some(false), + one_shot: Some(false), + compact_control: None, + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: vec![], + decoded_conditions: Vec::new(), + 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); diff --git a/crates/rrt-fixtures/src/normalize.rs b/crates/rrt-fixtures/src/normalize.rs index 0c65b93..c178eb4 100644 --- a/crates/rrt-fixtures/src/normalize.rs +++ b/crates/rrt-fixtures/src/normalize.rs @@ -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> { Ok(serde_json::to_value(state)?) diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs deleted file mode 100644 index 964ab95..0000000 --- a/crates/rrt-fixtures/src/schema.rs +++ /dev/null @@ -1,1780 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use rrt_runtime::{RuntimeState, RuntimeSummary, StepCommand}; - -pub const FIXTURE_FORMAT_VERSION: u32 = 1; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct FixtureSource { - pub kind: String, - #[serde(default)] - pub description: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct ExpectedRuntimeSummary { - #[serde(default)] - pub calendar: Option, - #[serde(default)] - pub calendar_projection_source: Option, - #[serde(default)] - pub calendar_projection_is_placeholder: Option, - #[serde(default)] - pub world_flag_count: Option, - #[serde(default)] - pub world_restore_selected_year_profile_lane: Option, - #[serde(default)] - pub world_restore_campaign_scenario_enabled: Option, - #[serde(default)] - pub world_restore_sandbox_enabled: Option, - #[serde(default)] - pub world_restore_seed_tuple_written_from_raw_lane: Option, - #[serde(default)] - pub world_restore_absolute_counter_requires_shell_context: Option, - #[serde(default)] - pub world_restore_absolute_counter_reconstructible_from_save: Option, - #[serde(default)] - pub world_restore_packed_year_word_raw_u16: Option, - #[serde(default)] - pub world_restore_partial_year_progress_raw_u8: Option, - #[serde(default)] - pub world_restore_current_calendar_tuple_word_raw_u32: Option, - #[serde(default)] - pub world_restore_current_calendar_tuple_word_2_raw_u32: Option, - #[serde(default)] - pub world_restore_absolute_counter_raw_u32: Option, - #[serde(default)] - pub world_restore_absolute_counter_mirror_raw_u32: Option, - #[serde(default)] - pub world_restore_disable_cargo_economy_special_condition_slot: Option, - #[serde(default)] - pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: - Option, - #[serde(default)] - pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option, - #[serde(default)] - pub world_restore_disable_cargo_economy_special_condition_enabled: Option, - #[serde(default)] - pub world_restore_use_bio_accelerator_cars_enabled: Option, - #[serde(default)] - pub world_restore_use_wartime_cargos_enabled: Option, - #[serde(default)] - pub world_restore_disable_train_crashes_enabled: Option, - #[serde(default)] - pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option, - #[serde(default)] - pub world_restore_ai_ignore_territories_at_startup_enabled: Option, - #[serde(default)] - pub world_restore_limited_track_building_amount: Option, - #[serde(default)] - pub world_restore_economic_status_code: Option, - #[serde(default)] - pub world_restore_territory_access_cost: Option, - #[serde(default)] - pub world_restore_issue_37_value: Option, - #[serde(default)] - pub world_restore_issue_38_value: Option, - #[serde(default)] - pub world_restore_issue_39_value: Option, - #[serde(default)] - pub world_restore_issue_3a_value: Option, - #[serde(default)] - pub world_restore_issue_37_multiplier_raw_u32: Option, - #[serde(default)] - pub world_restore_issue_37_multiplier_value_f32_text: Option, - #[serde(default)] - pub world_restore_finance_neighborhood_count: Option, - #[serde(default)] - pub world_restore_finance_neighborhood_labels: Option>, - #[serde(default)] - pub world_restore_economic_tuning_mirror_raw_u32: Option, - #[serde(default)] - pub world_restore_economic_tuning_mirror_value_f32_text: Option, - #[serde(default)] - pub world_restore_economic_tuning_lane_count: Option, - #[serde(default)] - pub world_restore_economic_tuning_lane_value_f32_text: Option>, - #[serde(default)] - pub world_restore_absolute_counter_restore_kind: Option, - #[serde(default)] - pub world_restore_absolute_counter_adjustment_context: Option, - #[serde(default)] - pub metadata_count: Option, - #[serde(default)] - pub company_count: Option, - #[serde(default)] - pub active_company_count: Option, - #[serde(default)] - pub company_market_state_owner_count: Option, - #[serde(default)] - pub selected_company_outstanding_shares: Option, - #[serde(default)] - pub selected_company_bond_count: Option, - #[serde(default)] - pub selected_company_largest_live_bond_principal: Option, - #[serde(default)] - pub selected_company_highest_coupon_live_bond_principal: Option, - #[serde(default)] - pub selected_company_assigned_share_pool: Option, - #[serde(default)] - pub selected_company_unassigned_share_pool: Option, - #[serde(default)] - pub selected_company_cached_share_price: Option, - #[serde(default)] - pub selected_company_cached_share_price_value_f32_text: Option, - #[serde(default)] - pub selected_company_mutable_support_scalar_value_f32_text: Option, - #[serde(default)] - pub selected_company_stat_band_root_0cfb_count: Option, - #[serde(default)] - pub selected_company_stat_band_root_0d7f_count: Option, - #[serde(default)] - pub selected_company_stat_band_root_1c47_count: Option, - #[serde(default)] - pub selected_company_last_dividend_year: Option, - #[serde(default)] - pub selected_company_years_since_founding: Option, - #[serde(default)] - pub selected_company_years_since_last_bankruptcy: Option, - #[serde(default)] - pub selected_company_years_since_last_dividend: Option, - #[serde(default)] - pub selected_company_current_partial_year_weight_numerator: Option, - #[serde(default)] - pub selected_company_current_issue_absolute_counter: Option, - #[serde(default)] - pub selected_company_prior_issue_absolute_counter: Option, - #[serde(default)] - pub selected_company_current_issue_age_absolute_counter_delta: Option, - #[serde(default)] - pub selected_company_chairman_bonus_year: Option, - #[serde(default)] - pub selected_company_chairman_bonus_amount: Option, - #[serde(default)] - pub player_count: Option, - #[serde(default)] - pub chairman_profile_count: Option, - #[serde(default)] - pub active_chairman_profile_count: Option, - #[serde(default)] - pub selected_chairman_profile_id: Option, - #[serde(default)] - pub linked_chairman_company_count: Option, - #[serde(default)] - pub company_takeover_cooldown_count: Option, - #[serde(default)] - pub company_merger_cooldown_count: Option, - #[serde(default)] - pub train_count: Option, - #[serde(default)] - pub active_train_count: Option, - #[serde(default)] - pub retired_train_count: Option, - #[serde(default)] - pub locomotive_catalog_count: Option, - #[serde(default)] - pub cargo_catalog_count: Option, - #[serde(default)] - pub territory_count: Option, - #[serde(default)] - pub company_territory_track_count: Option, - #[serde(default)] - pub packed_event_collection_present: Option, - #[serde(default)] - pub packed_event_record_count: Option, - #[serde(default)] - pub packed_event_decoded_record_count: Option, - #[serde(default)] - pub packed_event_imported_runtime_record_count: Option, - #[serde(default)] - pub packed_event_parity_only_record_count: Option, - #[serde(default)] - pub packed_event_unsupported_record_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_company_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_selection_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_company_role_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_player_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_player_selection_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_player_role_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_chairman_context_count: Option, - #[serde(default)] - pub packed_event_blocked_chairman_target_scope_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_condition_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_player_condition_context_count: Option, - #[serde(default)] - pub packed_event_blocked_company_condition_scope_disabled_count: Option, - #[serde(default)] - pub packed_event_blocked_player_condition_scope_count: Option, - #[serde(default)] - pub packed_event_blocked_territory_condition_scope_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_territory_context_count: Option, - #[serde(default)] - pub packed_event_blocked_named_territory_binding_count: Option, - #[serde(default)] - pub packed_event_blocked_unmapped_ordinary_condition_count: Option, - #[serde(default)] - pub packed_event_blocked_unmapped_world_condition_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_compact_control_count: Option, - #[serde(default)] - pub packed_event_blocked_shell_owned_descriptor_count: Option, - #[serde(default)] - pub packed_event_blocked_evidence_blocked_descriptor_count: Option, - #[serde(default)] - pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: Option, - #[serde(default)] - pub packed_event_blocked_unmapped_real_descriptor_count: Option, - #[serde(default)] - pub packed_event_blocked_unmapped_world_descriptor_count: Option, - #[serde(default)] - pub packed_event_blocked_territory_access_variant_count: Option, - #[serde(default)] - pub packed_event_blocked_territory_access_scope_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_train_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_train_territory_context_count: Option, - #[serde(default)] - pub packed_event_blocked_missing_locomotive_catalog_context_count: Option, - #[serde(default)] - pub packed_event_blocked_confiscation_variant_count: Option, - #[serde(default)] - pub packed_event_blocked_retire_train_variant_count: Option, - #[serde(default)] - pub packed_event_blocked_retire_train_scope_count: Option, - #[serde(default)] - pub packed_event_blocked_structural_only_count: Option, - #[serde(default)] - pub event_runtime_record_count: Option, - #[serde(default)] - pub candidate_availability_count: Option, - #[serde(default)] - pub zero_candidate_availability_count: Option, - #[serde(default)] - pub named_locomotive_availability_count: Option, - #[serde(default)] - pub zero_named_locomotive_availability_count: Option, - #[serde(default)] - pub named_locomotive_cost_count: Option, - #[serde(default)] - pub cargo_production_override_count: Option, - #[serde(default)] - pub world_runtime_variable_count: Option, - #[serde(default)] - pub company_runtime_variable_owner_count: Option, - #[serde(default)] - pub player_runtime_variable_owner_count: Option, - #[serde(default)] - pub territory_runtime_variable_owner_count: Option, - #[serde(default)] - pub world_scalar_override_count: Option, - #[serde(default)] - pub special_condition_count: Option, - #[serde(default)] - pub enabled_special_condition_count: Option, - #[serde(default)] - pub save_profile_kind: Option, - #[serde(default)] - pub save_profile_family: Option, - #[serde(default)] - pub save_profile_map_path: Option, - #[serde(default)] - pub save_profile_display_name: Option, - #[serde(default)] - pub save_profile_selected_year_profile_lane: Option, - #[serde(default)] - pub save_profile_sandbox_enabled: Option, - #[serde(default)] - pub save_profile_campaign_scenario_enabled: Option, - #[serde(default)] - pub save_profile_staged_profile_copy_on_restore: Option, - #[serde(default)] - pub total_event_record_service_count: Option, - #[serde(default)] - pub periodic_boundary_call_count: Option, - #[serde(default)] - pub total_trigger_dispatch_count: Option, - #[serde(default)] - pub dirty_rerun_count: Option, - #[serde(default)] - pub total_company_cash: Option, -} - -impl ExpectedRuntimeSummary { - pub fn compare(&self, actual: &RuntimeSummary) -> Vec { - let mut mismatches = Vec::new(); - - if let Some(calendar) = self.calendar { - if actual.calendar != calendar { - mismatches.push(format!( - "calendar mismatch: expected {:?}, got {:?}", - calendar, actual.calendar - )); - } - } - if let Some(source) = &self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = - self.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) = - self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = &self.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) = self.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) = &self.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) = self.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) = &self.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) = self.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) = &self.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) = &self.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) = &self.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) = self.metadata_count { - if actual.metadata_count != count { - mismatches.push(format!( - "metadata_count mismatch: expected {count}, got {}", - actual.metadata_count - )); - } - } - if let Some(count) = self.company_count { - if actual.company_count != count { - mismatches.push(format!( - "company_count mismatch: expected {count}, got {}", - actual.company_count - )); - } - } - if let Some(count) = self.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) = self.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 - )); - } - } - if let Some(value) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = &self.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) = &self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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 - )); - } - } - if let Some(count) = self.player_count { - if actual.player_count != count { - mismatches.push(format!( - "player_count mismatch: expected {count}, got {}", - actual.player_count - )); - } - } - if let Some(count) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.train_count { - if actual.train_count != count { - mismatches.push(format!( - "train_count mismatch: expected {count}, got {}", - actual.train_count - )); - } - } - if let Some(count) = self.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) = self.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) = self.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) = self.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) = self.territory_count { - if actual.territory_count != count { - mismatches.push(format!( - "territory_count mismatch: expected {count}, got {}", - actual.territory_count - )); - } - } - if let Some(count) = self.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 - )); - } - } - if let Some(present) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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 - )); - } - } - if let Some(count) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = &self.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) = &self.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) = &self.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) = &self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.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) = self.total_company_cash { - if actual.total_company_cash != total { - mismatches.push(format!( - "total_company_cash mismatch: expected {total}, got {}", - actual.total_company_cash - )); - } - } - - mismatches - } -} - -#[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, - #[serde(default)] - pub expected_summary: ExpectedRuntimeSummary, - #[serde(default)] - pub expected_state_fragment: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum FixtureStateOrigin { - Inline, - SnapshotPath(String), - SaveSlicePath(String), - ImportPath(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, - #[serde(default)] - pub state_snapshot_path: Option, - #[serde(default)] - pub state_save_slice_path: Option, - #[serde(default)] - pub state_import_path: Option, - #[serde(default)] - pub commands: Vec, - #[serde(default)] - pub expected_summary: ExpectedRuntimeSummary, - #[serde(default)] - pub expected_state_fragment: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct FixtureValidationReport { - pub fixture_id: String, - pub valid: bool, - pub issue_count: usize, - pub issues: Vec, -} - -pub fn compare_expected_state_fragment(expected: &Value, actual: &Value) -> Vec { - 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, -) { - 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:?}" - )), - _ => {} - } -} - -pub fn validate_fixture_document(document: &FixtureDocument) -> FixtureValidationReport { - let mut issues = Vec::new(); - - if document.format_version != FIXTURE_FORMAT_VERSION { - issues.push(format!( - "unsupported format_version {} (expected {})", - document.format_version, FIXTURE_FORMAT_VERSION - )); - } - if document.fixture_id.trim().is_empty() { - issues.push("fixture_id must not be empty".to_string()); - } - if document.source.kind.trim().is_empty() { - issues.push("source.kind must not be empty".to_string()); - } - if document.commands.is_empty() { - issues.push("fixture must contain at least one command".to_string()); - } - if let Err(err) = document.state.validate() { - issues.push(format!("invalid runtime state: {err}")); - } - - for (index, command) in document.commands.iter().enumerate() { - if let Err(err) = command.validate() { - issues.push(format!("invalid command at index {index}: {err}")); - } - } - - FixtureValidationReport { - fixture_id: document.fixture_id.clone(), - valid: issues.is_empty(), - issue_count: issues.len(), - issues, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::load_fixture_document_from_str; - - 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:?}" - ); - } -} diff --git a/crates/rrt-fixtures/src/schema/document.rs b/crates/rrt-fixtures/src/schema/document.rs new file mode 100644 index 0000000..167a824 --- /dev/null +++ b/crates/rrt-fixtures/src/schema/document.rs @@ -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, +} + +#[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, + #[serde(default)] + pub expected_summary: ExpectedRuntimeSummary, + #[serde(default)] + pub expected_state_fragment: Option, +} + +#[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, + #[serde(default)] + pub state_snapshot_path: Option, + #[serde(default)] + pub state_save_slice_path: Option, + #[serde(default)] + pub state_input_path: Option, + #[serde(default)] + pub commands: Vec, + #[serde(default)] + pub expected_summary: ExpectedRuntimeSummary, + #[serde(default)] + pub expected_state_fragment: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FixtureValidationReport { + pub fixture_id: String, + pub valid: bool, + pub issue_count: usize, + pub issues: Vec, +} diff --git a/crates/rrt-fixtures/src/schema/mod.rs b/crates/rrt-fixtures/src/schema/mod.rs new file mode 100644 index 0000000..8c0269b --- /dev/null +++ b/crates/rrt-fixtures/src/schema/mod.rs @@ -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; diff --git a/crates/rrt-fixtures/src/schema/state_fragment.rs b/crates/rrt-fixtures/src/schema/state_fragment.rs new file mode 100644 index 0000000..48e3fef --- /dev/null +++ b/crates/rrt-fixtures/src/schema/state_fragment.rs @@ -0,0 +1,49 @@ +use serde_json::Value; + +pub fn compare_expected_state_fragment(expected: &Value, actual: &Value) -> Vec { + 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, +) { + 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:?}" + )), + _ => {} + } +} diff --git a/crates/rrt-fixtures/src/schema/summary.rs b/crates/rrt-fixtures/src/schema/summary.rs new file mode 100644 index 0000000..b17bac4 --- /dev/null +++ b/crates/rrt-fixtures/src/schema/summary.rs @@ -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, + #[serde(default)] + pub calendar_projection_source: Option, + #[serde(default)] + pub calendar_projection_is_placeholder: Option, + #[serde(default)] + pub world_flag_count: Option, + #[serde(default)] + pub world_restore_selected_year_profile_lane: Option, + #[serde(default)] + pub world_restore_campaign_scenario_enabled: Option, + #[serde(default)] + pub world_restore_sandbox_enabled: Option, + #[serde(default)] + pub world_restore_seed_tuple_written_from_raw_lane: Option, + #[serde(default)] + pub world_restore_absolute_counter_requires_shell_context: Option, + #[serde(default)] + pub world_restore_absolute_counter_reconstructible_from_save: Option, + #[serde(default)] + pub world_restore_packed_year_word_raw_u16: Option, + #[serde(default)] + pub world_restore_partial_year_progress_raw_u8: Option, + #[serde(default)] + pub world_restore_current_calendar_tuple_word_raw_u32: Option, + #[serde(default)] + pub world_restore_current_calendar_tuple_word_2_raw_u32: Option, + #[serde(default)] + pub world_restore_absolute_counter_raw_u32: Option, + #[serde(default)] + pub world_restore_absolute_counter_mirror_raw_u32: Option, + #[serde(default)] + pub world_restore_disable_cargo_economy_special_condition_slot: Option, + #[serde(default)] + pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: + Option, + #[serde(default)] + pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option, + #[serde(default)] + pub world_restore_disable_cargo_economy_special_condition_enabled: Option, + #[serde(default)] + pub world_restore_use_bio_accelerator_cars_enabled: Option, + #[serde(default)] + pub world_restore_use_wartime_cargos_enabled: Option, + #[serde(default)] + pub world_restore_disable_train_crashes_enabled: Option, + #[serde(default)] + pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option, + #[serde(default)] + pub world_restore_ai_ignore_territories_at_startup_enabled: Option, + #[serde(default)] + pub world_restore_limited_track_building_amount: Option, + #[serde(default)] + pub world_restore_economic_status_code: Option, + #[serde(default)] + pub world_restore_territory_access_cost: Option, + #[serde(default)] + pub world_restore_issue_37_value: Option, + #[serde(default)] + pub world_restore_issue_38_value: Option, + #[serde(default)] + pub world_restore_issue_39_value: Option, + #[serde(default)] + pub world_restore_issue_3a_value: Option, + #[serde(default)] + pub world_restore_issue_37_multiplier_raw_u32: Option, + #[serde(default)] + pub world_restore_issue_37_multiplier_value_f32_text: Option, + #[serde(default)] + pub world_restore_finance_neighborhood_count: Option, + #[serde(default)] + pub world_restore_finance_neighborhood_labels: Option>, + #[serde(default)] + pub world_restore_economic_tuning_mirror_raw_u32: Option, + #[serde(default)] + pub world_restore_economic_tuning_mirror_value_f32_text: Option, + #[serde(default)] + pub world_restore_economic_tuning_lane_count: Option, + #[serde(default)] + pub world_restore_economic_tuning_lane_value_f32_text: Option>, + #[serde(default)] + pub world_restore_absolute_counter_restore_kind: Option, + #[serde(default)] + pub world_restore_absolute_counter_adjustment_context: Option, + #[serde(default)] + pub metadata_count: Option, + #[serde(default)] + pub company_count: Option, + #[serde(default)] + pub active_company_count: Option, + #[serde(default)] + pub company_market_state_owner_count: Option, + #[serde(default)] + pub selected_company_outstanding_shares: Option, + #[serde(default)] + pub selected_company_bond_count: Option, + #[serde(default)] + pub selected_company_largest_live_bond_principal: Option, + #[serde(default)] + pub selected_company_highest_coupon_live_bond_principal: Option, + #[serde(default)] + pub selected_company_assigned_share_pool: Option, + #[serde(default)] + pub selected_company_unassigned_share_pool: Option, + #[serde(default)] + pub selected_company_cached_share_price: Option, + #[serde(default)] + pub selected_company_cached_share_price_value_f32_text: Option, + #[serde(default)] + pub selected_company_mutable_support_scalar_value_f32_text: Option, + #[serde(default)] + pub selected_company_stat_band_root_0cfb_count: Option, + #[serde(default)] + pub selected_company_stat_band_root_0d7f_count: Option, + #[serde(default)] + pub selected_company_stat_band_root_1c47_count: Option, + #[serde(default)] + pub selected_company_last_dividend_year: Option, + #[serde(default)] + pub selected_company_years_since_founding: Option, + #[serde(default)] + pub selected_company_years_since_last_bankruptcy: Option, + #[serde(default)] + pub selected_company_years_since_last_dividend: Option, + #[serde(default)] + pub selected_company_current_partial_year_weight_numerator: Option, + #[serde(default)] + pub selected_company_current_issue_absolute_counter: Option, + #[serde(default)] + pub selected_company_prior_issue_absolute_counter: Option, + #[serde(default)] + pub selected_company_current_issue_age_absolute_counter_delta: Option, + #[serde(default)] + pub selected_company_chairman_bonus_year: Option, + #[serde(default)] + pub selected_company_chairman_bonus_amount: Option, + #[serde(default)] + pub player_count: Option, + #[serde(default)] + pub chairman_profile_count: Option, + #[serde(default)] + pub active_chairman_profile_count: Option, + #[serde(default)] + pub selected_chairman_profile_id: Option, + #[serde(default)] + pub linked_chairman_company_count: Option, + #[serde(default)] + pub company_takeover_cooldown_count: Option, + #[serde(default)] + pub company_merger_cooldown_count: Option, + #[serde(default)] + pub train_count: Option, + #[serde(default)] + pub active_train_count: Option, + #[serde(default)] + pub retired_train_count: Option, + #[serde(default)] + pub locomotive_catalog_count: Option, + #[serde(default)] + pub cargo_catalog_count: Option, + #[serde(default)] + pub territory_count: Option, + #[serde(default)] + pub company_territory_track_count: Option, + #[serde(default)] + pub packed_event_collection_present: Option, + #[serde(default)] + pub packed_event_record_count: Option, + #[serde(default)] + pub packed_event_decoded_record_count: Option, + #[serde(default)] + pub packed_event_imported_runtime_record_count: Option, + #[serde(default)] + pub packed_event_parity_only_record_count: Option, + #[serde(default)] + pub packed_event_unsupported_record_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_company_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_selection_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_company_role_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_player_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_player_selection_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_player_role_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_chairman_context_count: Option, + #[serde(default)] + pub packed_event_blocked_chairman_target_scope_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_condition_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_player_condition_context_count: Option, + #[serde(default)] + pub packed_event_blocked_company_condition_scope_disabled_count: Option, + #[serde(default)] + pub packed_event_blocked_player_condition_scope_count: Option, + #[serde(default)] + pub packed_event_blocked_territory_condition_scope_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_territory_context_count: Option, + #[serde(default)] + pub packed_event_blocked_named_territory_binding_count: Option, + #[serde(default)] + pub packed_event_blocked_unmapped_ordinary_condition_count: Option, + #[serde(default)] + pub packed_event_blocked_unmapped_world_condition_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_compact_control_count: Option, + #[serde(default)] + pub packed_event_blocked_shell_owned_descriptor_count: Option, + #[serde(default)] + pub packed_event_blocked_evidence_blocked_descriptor_count: Option, + #[serde(default)] + pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: Option, + #[serde(default)] + pub packed_event_blocked_unmapped_real_descriptor_count: Option, + #[serde(default)] + pub packed_event_blocked_unmapped_world_descriptor_count: Option, + #[serde(default)] + pub packed_event_blocked_territory_access_variant_count: Option, + #[serde(default)] + pub packed_event_blocked_territory_access_scope_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_train_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_train_territory_context_count: Option, + #[serde(default)] + pub packed_event_blocked_missing_locomotive_catalog_context_count: Option, + #[serde(default)] + pub packed_event_blocked_confiscation_variant_count: Option, + #[serde(default)] + pub packed_event_blocked_retire_train_variant_count: Option, + #[serde(default)] + pub packed_event_blocked_retire_train_scope_count: Option, + #[serde(default)] + pub packed_event_blocked_structural_only_count: Option, + #[serde(default)] + pub event_runtime_record_count: Option, + #[serde(default)] + pub candidate_availability_count: Option, + #[serde(default)] + pub zero_candidate_availability_count: Option, + #[serde(default)] + pub named_locomotive_availability_count: Option, + #[serde(default)] + pub zero_named_locomotive_availability_count: Option, + #[serde(default)] + pub named_locomotive_cost_count: Option, + #[serde(default)] + pub cargo_production_override_count: Option, + #[serde(default)] + pub world_runtime_variable_count: Option, + #[serde(default)] + pub company_runtime_variable_owner_count: Option, + #[serde(default)] + pub player_runtime_variable_owner_count: Option, + #[serde(default)] + pub territory_runtime_variable_owner_count: Option, + #[serde(default)] + pub world_scalar_override_count: Option, + #[serde(default)] + pub special_condition_count: Option, + #[serde(default)] + pub enabled_special_condition_count: Option, + #[serde(default)] + pub save_profile_kind: Option, + #[serde(default)] + pub save_profile_family: Option, + #[serde(default)] + pub save_profile_map_path: Option, + #[serde(default)] + pub save_profile_display_name: Option, + #[serde(default)] + pub save_profile_selected_year_profile_lane: Option, + #[serde(default)] + pub save_profile_sandbox_enabled: Option, + #[serde(default)] + pub save_profile_campaign_scenario_enabled: Option, + #[serde(default)] + pub save_profile_staged_profile_copy_on_restore: Option, + #[serde(default)] + pub total_event_record_service_count: Option, + #[serde(default)] + pub periodic_boundary_call_count: Option, + #[serde(default)] + pub total_trigger_dispatch_count: Option, + #[serde(default)] + pub dirty_rerun_count: Option, + #[serde(default)] + pub total_company_cash: Option, +} diff --git a/crates/rrt-fixtures/src/schema/summary_compare/collections.rs b/crates/rrt-fixtures/src/schema/summary_compare/collections.rs new file mode 100644 index 0000000..8e72d9b --- /dev/null +++ b/crates/rrt-fixtures/src/schema/summary_compare/collections.rs @@ -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, +) { + 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, +) { + 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 + )); + } + } +} diff --git a/crates/rrt-fixtures/src/schema/summary_compare/mod.rs b/crates/rrt-fixtures/src/schema/summary_compare/mod.rs new file mode 100644 index 0000000..c720004 --- /dev/null +++ b/crates/rrt-fixtures/src/schema/summary_compare/mod.rs @@ -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 { + 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 + } +} diff --git a/crates/rrt-fixtures/src/schema/summary_compare/packed_events.rs b/crates/rrt-fixtures/src/schema/summary_compare/packed_events.rs new file mode 100644 index 0000000..16b782b --- /dev/null +++ b/crates/rrt-fixtures/src/schema/summary_compare/packed_events.rs @@ -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, +) { + 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 + )); + } + } +} diff --git a/crates/rrt-fixtures/src/schema/summary_compare/selected_company.rs b/crates/rrt-fixtures/src/schema/summary_compare/selected_company.rs new file mode 100644 index 0000000..59b5c83 --- /dev/null +++ b/crates/rrt-fixtures/src/schema/summary_compare/selected_company.rs @@ -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, +) { + 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 + )); + } + } +} diff --git a/crates/rrt-fixtures/src/schema/summary_compare/world.rs b/crates/rrt-fixtures/src/schema/summary_compare/world.rs new file mode 100644 index 0000000..cdc9a8c --- /dev/null +++ b/crates/rrt-fixtures/src/schema/summary_compare/world.rs @@ -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, +) { + 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 + )); + } + } +} diff --git a/crates/rrt-fixtures/src/schema/tests.rs b/crates/rrt-fixtures/src/schema/tests.rs new file mode 100644 index 0000000..e70829a --- /dev/null +++ b/crates/rrt-fixtures/src/schema/tests.rs @@ -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:?}" + ); +} diff --git a/crates/rrt-fixtures/src/schema/validate.rs b/crates/rrt-fixtures/src/schema/validate.rs new file mode 100644 index 0000000..d2983cb --- /dev/null +++ b/crates/rrt-fixtures/src/schema/validate.rs @@ -0,0 +1,37 @@ +use super::{FIXTURE_FORMAT_VERSION, FixtureDocument, FixtureValidationReport}; + +pub fn validate_fixture_document(document: &FixtureDocument) -> FixtureValidationReport { + let mut issues = Vec::new(); + + if document.format_version != FIXTURE_FORMAT_VERSION { + issues.push(format!( + "unsupported format_version {} (expected {})", + document.format_version, FIXTURE_FORMAT_VERSION + )); + } + if document.fixture_id.trim().is_empty() { + issues.push("fixture_id must not be empty".to_string()); + } + if document.source.kind.trim().is_empty() { + issues.push("source.kind must not be empty".to_string()); + } + if document.commands.is_empty() { + issues.push("fixture must contain at least one command".to_string()); + } + if let Err(err) = document.state.validate() { + issues.push(format!("invalid runtime state: {err}")); + } + + for (index, command) in document.commands.iter().enumerate() { + if let Err(err) = command.validate() { + issues.push(format!("invalid command at index {index}: {err}")); + } + } + + FixtureValidationReport { + fixture_id: document.fixture_id.clone(), + valid: issues.is_empty(), + issue_count: issues.len(), + issues, + } +} diff --git a/crates/rrt-fixtures/src/summary.rs b/crates/rrt-fixtures/src/summary.rs new file mode 100644 index 0000000..51545fe --- /dev/null +++ b/crates/rrt-fixtures/src/summary.rs @@ -0,0 +1 @@ +pub use crate::schema::{ExpectedRuntimeSummary, compare_expected_state_fragment}; diff --git a/crates/rrt-fixtures/src/validation.rs b/crates/rrt-fixtures/src/validation.rs new file mode 100644 index 0000000..7c141ef --- /dev/null +++ b/crates/rrt-fixtures/src/validation.rs @@ -0,0 +1,2 @@ +pub use crate::load::{load_fixture_document, load_fixture_document_from_str}; +pub use crate::schema::{FixtureValidationReport, validate_fixture_document}; diff --git a/crates/rrt-hook/src/capture.rs b/crates/rrt-hook/src/capture.rs new file mode 100644 index 0000000..7c25c11 --- /dev/null +++ b/crates/rrt-hook/src/capture.rs @@ -0,0 +1,156 @@ +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +use rrt_model::finance::{ + AnnualFinancePolicy, BondPosition, CompanyFinanceState, FinanceOutcome, FinanceSnapshot, +}; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FinanceLogPaths { + pub snapshot_path: PathBuf, + pub outcome_path: PathBuf, +} + +pub fn sample_finance_snapshot() -> FinanceSnapshot { + FinanceSnapshot { + policy: AnnualFinancePolicy { + dividends_allowed: false, + ..AnnualFinancePolicy::default() + }, + company: CompanyFinanceState { + current_cash: 100_000, + support_adjusted_share_price: 27.5, + book_value_per_share: 20.0, + outstanding_share_count: 60_000, + recent_net_profits: [40_000, 30_000, 20_000], + recent_revenue_totals: [250_000, 240_000, 230_000], + bonds: vec![ + BondPosition { + principal: 150_000, + coupon_rate: 0.12, + years_remaining: 12, + }, + BondPosition { + principal: 10_000, + coupon_rate: 0.10, + years_remaining: 10, + }, + ], + ..CompanyFinanceState::default() + }, + } +} + +pub fn write_finance_snapshot_bundle( + base_dir: &Path, + stem: &str, + snapshot: &FinanceSnapshot, +) -> io::Result { + fs::create_dir_all(base_dir)?; + + let snapshot_path = base_dir.join(format!("rrt_finance_{stem}_snapshot.json")); + let outcome_path = base_dir.join(format!("rrt_finance_{stem}_outcome.json")); + let outcome: FinanceOutcome = snapshot.evaluate(); + + let snapshot_json = serde_json::to_vec_pretty(snapshot) + .map_err(|err| io::Error::other(format!("serialize snapshot: {err}")))?; + let outcome_json = serde_json::to_vec_pretty(&outcome) + .map_err(|err| io::Error::other(format!("serialize outcome: {err}")))?; + + fs::write(&snapshot_path, snapshot_json)?; + fs::write(&outcome_path, outcome_json)?; + + Ok(FinanceLogPaths { + snapshot_path, + outcome_path, + }) +} + +pub fn write_finance_snapshot_only( + base_dir: &Path, + stem: &str, + snapshot: &FinanceSnapshot, +) -> io::Result { + fs::create_dir_all(base_dir)?; + + let snapshot_path = base_dir.join(format!("rrt_finance_{stem}_snapshot.json")); + let snapshot_json = serde_json::to_vec_pretty(snapshot) + .map_err(|err| io::Error::other(format!("serialize snapshot: {err}")))?; + fs::write(&snapshot_path, snapshot_json)?; + + Ok(snapshot_path) +} + +#[derive(Debug, Clone, Serialize)] +pub struct IndexedCollectionProbeRow { + pub entry_id: usize, + pub live: bool, + pub resolved_ptr: usize, + pub active_flag: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IndexedCollectionProbe { + pub collection_addr: usize, + pub flat_payload: bool, + pub stride: u32, + pub id_bound: i32, + pub payload_ptr: usize, + pub tombstone_ptr: usize, + pub first_rows: Vec, +} + +pub fn write_indexed_collection_probe( + base_dir: &Path, + stem: &str, + probe: &IndexedCollectionProbe, +) -> io::Result { + fs::create_dir_all(base_dir)?; + + let path = base_dir.join(format!("rrt_finance_{stem}_collection_probe.json")); + let json = serde_json::to_vec_pretty(probe) + .map_err(|err| io::Error::other(format!("serialize collection probe: {err}")))?; + fs::write(&path, json)?; + + Ok(path) +} + +#[derive(Debug, Clone, Serialize)] +pub struct CargoCollectionProbeRow { + pub entry_id: usize, + pub live: bool, + pub resolved_ptr: usize, + pub stem: Option, + pub route_style_byte: Option, + pub subtype_byte: Option, + pub class_marker: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CargoCollectionProbe { + pub collection_addr: usize, + pub flat_payload: bool, + pub stride: u32, + pub id_bound: i32, + pub payload_ptr: usize, + pub tombstone_ptr: usize, + pub live_entry_count: usize, + pub rows: Vec, +} + +pub fn write_cargo_collection_probe( + base_dir: &Path, + stem: &str, + probe: &CargoCollectionProbe, +) -> io::Result { + fs::create_dir_all(base_dir)?; + + let path = base_dir.join(format!("rrt_cargo_{stem}_collection_probe.json")); + let json = serde_json::to_vec_pretty(probe) + .map_err(|err| io::Error::other(format!("serialize cargo collection probe: {err}")))?; + fs::write(&path, json)?; + + Ok(path) +} diff --git a/crates/rrt-hook/src/lib.rs b/crates/rrt-hook/src/lib.rs index b3aa3dc..87230e7 100644 --- a/crates/rrt-hook/src/lib.rs +++ b/crates/rrt-hook/src/lib.rs @@ -1,3127 +1,15 @@ #![cfg_attr(not(windows), allow(dead_code))] -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; +mod capture; -use rrt_model::finance::{ - AnnualFinancePolicy, BondPosition, CompanyFinanceState, FinanceOutcome, FinanceSnapshot, +pub use capture::{ + CargoCollectionProbe, CargoCollectionProbeRow, FinanceLogPaths, IndexedCollectionProbe, + IndexedCollectionProbeRow, sample_finance_snapshot, write_cargo_collection_probe, + write_finance_snapshot_bundle, write_finance_snapshot_only, write_indexed_collection_probe, }; -use serde::Serialize; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FinanceLogPaths { - pub snapshot_path: PathBuf, - pub outcome_path: PathBuf, -} - -pub fn sample_finance_snapshot() -> FinanceSnapshot { - FinanceSnapshot { - policy: AnnualFinancePolicy { - dividends_allowed: false, - ..AnnualFinancePolicy::default() - }, - company: CompanyFinanceState { - current_cash: 100_000, - support_adjusted_share_price: 27.5, - book_value_per_share: 20.0, - outstanding_share_count: 60_000, - recent_net_profits: [40_000, 30_000, 20_000], - recent_revenue_totals: [250_000, 240_000, 230_000], - bonds: vec![ - BondPosition { - principal: 150_000, - coupon_rate: 0.12, - years_remaining: 12, - }, - BondPosition { - principal: 10_000, - coupon_rate: 0.10, - years_remaining: 10, - }, - ], - ..CompanyFinanceState::default() - }, - } -} - -pub fn write_finance_snapshot_bundle( - base_dir: &Path, - stem: &str, - snapshot: &FinanceSnapshot, -) -> io::Result { - fs::create_dir_all(base_dir)?; - - let snapshot_path = base_dir.join(format!("rrt_finance_{stem}_snapshot.json")); - let outcome_path = base_dir.join(format!("rrt_finance_{stem}_outcome.json")); - let outcome: FinanceOutcome = snapshot.evaluate(); - - let snapshot_json = serde_json::to_vec_pretty(snapshot) - .map_err(|err| io::Error::other(format!("serialize snapshot: {err}")))?; - let outcome_json = serde_json::to_vec_pretty(&outcome) - .map_err(|err| io::Error::other(format!("serialize outcome: {err}")))?; - - fs::write(&snapshot_path, snapshot_json)?; - fs::write(&outcome_path, outcome_json)?; - - Ok(FinanceLogPaths { - snapshot_path, - outcome_path, - }) -} - -pub fn write_finance_snapshot_only( - base_dir: &Path, - stem: &str, - snapshot: &FinanceSnapshot, -) -> io::Result { - fs::create_dir_all(base_dir)?; - - let snapshot_path = base_dir.join(format!("rrt_finance_{stem}_snapshot.json")); - let snapshot_json = serde_json::to_vec_pretty(snapshot) - .map_err(|err| io::Error::other(format!("serialize snapshot: {err}")))?; - fs::write(&snapshot_path, snapshot_json)?; - - Ok(snapshot_path) -} - -#[derive(Debug, Clone, Serialize)] -pub struct IndexedCollectionProbeRow { - pub entry_id: usize, - pub live: bool, - pub resolved_ptr: usize, - pub active_flag: Option, -} - -#[derive(Debug, Clone, Serialize)] -pub struct IndexedCollectionProbe { - pub collection_addr: usize, - pub flat_payload: bool, - pub stride: u32, - pub id_bound: i32, - pub payload_ptr: usize, - pub tombstone_ptr: usize, - pub first_rows: Vec, -} - -pub fn write_indexed_collection_probe( - base_dir: &Path, - stem: &str, - probe: &IndexedCollectionProbe, -) -> io::Result { - fs::create_dir_all(base_dir)?; - - let path = base_dir.join(format!("rrt_finance_{stem}_collection_probe.json")); - let json = serde_json::to_vec_pretty(probe) - .map_err(|err| io::Error::other(format!("serialize collection probe: {err}")))?; - fs::write(&path, json)?; - - Ok(path) -} - -#[derive(Debug, Clone, Serialize)] -pub struct CargoCollectionProbeRow { - pub entry_id: usize, - pub live: bool, - pub resolved_ptr: usize, - pub stem: Option, - pub route_style_byte: Option, - pub subtype_byte: Option, - pub class_marker: Option, -} - -#[derive(Debug, Clone, Serialize)] -pub struct CargoCollectionProbe { - pub collection_addr: usize, - pub flat_payload: bool, - pub stride: u32, - pub id_bound: i32, - pub payload_ptr: usize, - pub tombstone_ptr: usize, - pub live_entry_count: usize, - pub rows: Vec, -} - -pub fn write_cargo_collection_probe( - base_dir: &Path, - stem: &str, - probe: &CargoCollectionProbe, -) -> io::Result { - fs::create_dir_all(base_dir)?; - - let path = base_dir.join(format!("rrt_cargo_{stem}_collection_probe.json")); - let json = serde_json::to_vec_pretty(probe) - .map_err(|err| io::Error::other(format!("serialize cargo collection probe: {err}")))?; - fs::write(&path, json)?; - - Ok(path) -} #[cfg(windows)] -mod windows_hook { - use super::{ - CargoCollectionProbe, CargoCollectionProbeRow, IndexedCollectionProbe, - IndexedCollectionProbeRow, sample_finance_snapshot, write_cargo_collection_probe, - write_finance_snapshot_bundle, write_finance_snapshot_only, write_indexed_collection_probe, - }; - use core::ffi::{c_char, c_void}; - use core::mem; - use core::ptr; - use std::env; - use std::fmt::Write as _; - use std::path::PathBuf; - use std::sync::OnceLock; - use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; - use std::thread; - use std::time::Duration; - - use rrt_model::finance::{ - AnnualFinancePolicy, BondPosition, CompanyFinanceState, FinanceSnapshot, GrowthSetting, - }; - - const DLL_PROCESS_ATTACH: u32 = 1; - const E_FAIL: i32 = 0x8000_4005_u32 as i32; - const FILE_APPEND_DATA: u32 = 0x0000_0004; - const FILE_SHARE_READ: u32 = 0x0000_0001; - const OPEN_ALWAYS: u32 = 4; - const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080; - const INVALID_HANDLE_VALUE: isize = -1; - const FILE_END: u32 = 2; - - static LOG_PATH: &[u8] = b"rrt_hook_attach.log\0"; - static ATTACH_MESSAGE: &[u8] = b"rrt-hook: process attach\n"; - static FINANCE_CAPTURE_STARTED_MESSAGE: &[u8] = b"rrt-hook: finance capture thread started\n"; - static FINANCE_CAPTURE_SCAN_MESSAGE: &[u8] = b"rrt-hook: finance capture raw collection scan\n"; - static FINANCE_CAPTURE_PROBE_DUMP_WRITTEN_MESSAGE: &[u8] = - b"rrt-hook: finance collection probe written\n"; - static FINANCE_CAPTURE_COMPANY_RESOLVED_MESSAGE: &[u8] = - b"rrt-hook: finance capture company resolved\n"; - static FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE: &[u8] = - b"rrt-hook: finance probe snapshot written\n"; - static FINANCE_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: finance capture timed out\n"; - static CARGO_CAPTURE_STARTED_MESSAGE: &[u8] = b"rrt-hook: cargo capture thread started\n"; - static CARGO_CAPTURE_WRITTEN_MESSAGE: &[u8] = b"rrt-hook: cargo collection probe written\n"; - static CARGO_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: cargo capture timed out\n"; - static AUTO_LOAD_STARTED_MESSAGE: &[u8] = b"rrt-hook: auto load hook armed\n"; - static AUTO_LOAD_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell-state hook installed\n"; - static AUTO_LOAD_PROFILE_DISPATCH_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load startup-dispatch hook installed\n"; - static AUTO_LOAD_RUNTIME_RESET_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load runtime-reset hook installed\n"; - static AUTO_LOAD_ALLOCATOR_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load allocator hook installed\n"; - static AUTO_LOAD_LOAD_SCREEN_SCALAR_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen scalar hook installed\n"; - static AUTO_LOAD_LOAD_SCREEN_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen hook installed\n"; - static AUTO_LOAD_LOAD_SCREEN_MESSAGE_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen message hook installed\n"; - static AUTO_LOAD_RUNTIME_PRIME_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell runtime-prime hook installed\n"; - static AUTO_LOAD_FRAME_CYCLE_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell frame-cycle hook installed\n"; - static AUTO_LOAD_OBJECT_SERVICE_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object-service hook installed\n"; - static AUTO_LOAD_CHILD_SERVICE_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell child-service hook installed\n"; - static AUTO_LOAD_PUBLISH_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell publish hook installed\n"; - static AUTO_LOAD_UNPUBLISH_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell unpublish hook installed\n"; - static AUTO_LOAD_OBJECT_TEARDOWN_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object teardown hook installed\n"; - static AUTO_LOAD_OBJECT_RANGE_REMOVE_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object range-remove hook installed\n"; - static AUTO_LOAD_REMOVE_NODE_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell remove-node hook installed\n"; - static AUTO_LOAD_NODE_VCALL_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell node-vcall hook installed\n"; - static AUTO_LOAD_MODE2_TEARDOWN_HOOK_INSTALLED_MESSAGE: &[u8] = - b"rrt-hook: auto load mode2 teardown hook installed\n"; - static AUTO_LOAD_READY_MESSAGE: &[u8] = b"rrt-hook: auto load ready gate passed\n"; - static AUTO_LOAD_DEFERRED_MESSAGE: &[u8] = - b"rrt-hook: auto load restore deferred to later service turn\n"; - static AUTO_LOAD_CALLING_MESSAGE: &[u8] = b"rrt-hook: auto load restore calling\n"; - static AUTO_LOAD_STAGED_MESSAGE: &[u8] = - b"rrt-hook: auto load restore staged for later transition\n"; - static AUTO_LOAD_READY_COUNT_MESSAGE: &[u8] = b"rrt-hook: auto load ready count\n"; - static AUTO_LOAD_ARMED_TICK_MESSAGE: &[u8] = b"rrt-hook: auto load armed transition tick\n"; - static AUTO_LOAD_OWNER_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell transition entering\n"; - static AUTO_LOAD_OWNER_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell transition returned\n"; - static AUTO_LOAD_PROFILE_DISPATCH_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load startup dispatch entering\n"; - static AUTO_LOAD_PROFILE_DISPATCH_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load startup dispatch returned\n"; - static AUTO_LOAD_RUNTIME_RESET_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load runtime reset entering\n"; - static AUTO_LOAD_RUNTIME_RESET_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load runtime reset returned\n"; - static AUTO_LOAD_ALLOCATOR_ENTRY_MESSAGE: &[u8] = b"rrt-hook: auto load allocator entering\n"; - static AUTO_LOAD_ALLOCATOR_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load allocator returned\n"; - static AUTO_LOAD_LOAD_SCREEN_SCALAR_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen scalar entering\n"; - static AUTO_LOAD_LOAD_SCREEN_SCALAR_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen scalar returned\n"; - static AUTO_LOAD_LOAD_SCREEN_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen construct entering\n"; - static AUTO_LOAD_LOAD_SCREEN_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen construct returned\n"; - static AUTO_LOAD_LOAD_SCREEN_MESSAGE_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen message entering\n"; - static AUTO_LOAD_LOAD_SCREEN_MESSAGE_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load load-screen message returned\n"; - static AUTO_LOAD_RUNTIME_PRIME_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell runtime-prime entering\n"; - static AUTO_LOAD_RUNTIME_PRIME_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell runtime-prime returned\n"; - static AUTO_LOAD_FRAME_CYCLE_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell frame-cycle entering\n"; - static AUTO_LOAD_FRAME_CYCLE_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell frame-cycle returned\n"; - static AUTO_LOAD_OBJECT_SERVICE_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object-service entering\n"; - static AUTO_LOAD_OBJECT_SERVICE_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object-service returned\n"; - static AUTO_LOAD_CHILD_SERVICE_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell child-service entering\n"; - static AUTO_LOAD_CHILD_SERVICE_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell child-service returned\n"; - static AUTO_LOAD_PUBLISH_ENTRY_MESSAGE: &[u8] = b"rrt-hook: auto load shell publish entering\n"; - static AUTO_LOAD_PUBLISH_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell publish returned\n"; - static AUTO_LOAD_UNPUBLISH_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell unpublish entering\n"; - static AUTO_LOAD_UNPUBLISH_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell unpublish returned\n"; - static AUTO_LOAD_OBJECT_TEARDOWN_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object teardown entering\n"; - static AUTO_LOAD_OBJECT_TEARDOWN_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object teardown returned\n"; - static AUTO_LOAD_OBJECT_RANGE_REMOVE_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object range-remove entering\n"; - static AUTO_LOAD_OBJECT_RANGE_REMOVE_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell object range-remove returned\n"; - static AUTO_LOAD_REMOVE_NODE_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell remove-node entering\n"; - static AUTO_LOAD_REMOVE_NODE_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell remove-node returned\n"; - static AUTO_LOAD_NODE_VCALL_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load shell node-vcall entering\n"; - static AUTO_LOAD_NODE_VCALL_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load shell node-vcall returned\n"; - static AUTO_LOAD_MODE2_TEARDOWN_ENTRY_MESSAGE: &[u8] = - b"rrt-hook: auto load mode2 teardown entering\n"; - static AUTO_LOAD_MODE2_TEARDOWN_RETURNED_MESSAGE: &[u8] = - b"rrt-hook: auto load mode2 teardown returned\n"; - static AUTO_LOAD_TRIGGERED_MESSAGE: &[u8] = b"rrt-hook: auto load restore invoked\n"; - static AUTO_LOAD_SUCCESS_MESSAGE: &[u8] = b"rrt-hook: auto load request reported success\n"; - static AUTO_LOAD_FAILURE_MESSAGE: &[u8] = b"rrt-hook: auto load request reported failure\n"; - static DEBUG_MESSAGE: &[u8] = b"rrt-hook: DllMain process attach\0"; - static DIRECT_INPUT8_CREATE_NAME: &[u8] = b"DirectInput8Create\0"; - static mut REAL_DINPUT8_CREATE: Option = None; - static FINANCE_TEMPLATE_EMITTED: AtomicBool = AtomicBool::new(false); - static FINANCE_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false); - static FINANCE_COLLECTION_PROBE_WRITTEN: AtomicBool = AtomicBool::new(false); - static CARGO_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false); - static AUTO_LOAD_THREAD_STARTED: AtomicBool = AtomicBool::new(false); - static AUTO_LOAD_HOOK_INSTALLED: AtomicBool = AtomicBool::new(false); - static AUTO_LOAD_ATTEMPTED: AtomicBool = AtomicBool::new(false); - static AUTO_LOAD_IN_PROGRESS: AtomicBool = AtomicBool::new(false); - static AUTO_LOAD_DEFERRED: AtomicBool = AtomicBool::new(false); - static AUTO_LOAD_TRANSITION_ARMED: AtomicBool = AtomicBool::new(false); - static AUTO_LOAD_LAST_GATE_MASK: AtomicU32 = AtomicU32::new(u32::MAX); - static AUTO_LOAD_READY_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_ARMED_TICK_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE: AtomicBool = AtomicBool::new(false); - static AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_POST_TRANSITION_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_LOAD_SCREEN_MESSAGE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_SERVICE_ENTRY_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_SERVICE_RETURN_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_RUNTIME_PRIME_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_FRAME_CYCLE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_OBJECT_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_CHILD_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); - static AUTO_LOAD_SAVE_STEM: OnceLock = OnceLock::new(); - static mut SHELL_STATE_SERVICE_TRAMPOLINE: usize = 0; - static mut PROFILE_STARTUP_DISPATCH_TRAMPOLINE: usize = 0; - static mut RUNTIME_RESET_TRAMPOLINE: usize = 0; - static mut ALLOCATOR_TRAMPOLINE: usize = 0; - static mut LOAD_SCREEN_SCALAR_TRAMPOLINE: usize = 0; - static mut LOAD_SCREEN_CONSTRUCT_TRAMPOLINE: usize = 0; - static mut LOAD_SCREEN_MESSAGE_TRAMPOLINE: usize = 0; - static mut RUNTIME_PRIME_TRAMPOLINE: usize = 0; - static mut FRAME_CYCLE_TRAMPOLINE: usize = 0; - static mut OBJECT_SERVICE_TRAMPOLINE: usize = 0; - static mut CHILD_SERVICE_TRAMPOLINE: usize = 0; - static mut SHELL_PUBLISH_TRAMPOLINE: usize = 0; - static mut SHELL_UNPUBLISH_TRAMPOLINE: usize = 0; - static mut SHELL_OBJECT_TEARDOWN_TRAMPOLINE: usize = 0; - static mut SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE: usize = 0; - static mut SHELL_REMOVE_NODE_TRAMPOLINE: usize = 0; - static mut SHELL_NODE_VCALL_TRAMPOLINE: usize = 0; - static mut MODE2_TEARDOWN_TRAMPOLINE: usize = 0; - - const COMPANY_COLLECTION_ADDR: usize = 0x0062be10; - const CARGO_COLLECTION_ADDR: usize = 0x0062ba8c; - const SHELL_CONTROLLER_PTR_ADDR: usize = 0x006d4024; - const SHELL_STATE_PTR_ADDR: usize = 0x006cec74; - const ACTIVE_MODE_PTR_ADDR: usize = 0x006cec78; - const SHELL_STATE_SERVICE_ADDR: usize = 0x00482160; - const SHELL_TRANSITION_MODE_ADDR: usize = 0x00482ec0; - const PROFILE_STARTUP_DISPATCH_ADDR: usize = 0x00438890; - const RUNTIME_RESET_ADDR: usize = 0x004336d0; - const STARTUP_RUNTIME_ALLOC_THUNK_ADDR: usize = 0x0053b070; - const LOAD_SCREEN_SET_SCALAR_ADDR: usize = 0x004ea710; - const LOAD_SCREEN_CONSTRUCT_ADDR: usize = 0x004ea620; - const LOAD_SCREEN_HANDLE_MESSAGE_ADDR: usize = 0x004e3a80; - const SHELL_RUNTIME_PRIME_ADDR: usize = 0x00538b60; - const SHELL_FRAME_CYCLE_ADDR: usize = 0x00520620; - const SHELL_OBJECT_SERVICE_ADDR: usize = 0x0053fda0; - const SHELL_CHILD_SERVICE_ADDR: usize = 0x005595d0; - const SHELL_PUBLISH_WINDOW_ADDR: usize = 0x00538e50; - const SHELL_UNPUBLISH_WINDOW_ADDR: usize = 0x005389c0; - const SHELL_OBJECT_TEARDOWN_ADDR: usize = 0x005400c0; - const SHELL_OBJECT_RANGE_REMOVE_ADDR: usize = 0x0053fe00; - const SHELL_REMOVE_NODE_ADDR: usize = 0x0053f860; - const SHELL_NODE_VCALL_ADDR: usize = 0x00540910; - const MODE2_TEARDOWN_ADDR: usize = 0x00502720; - const SHELL_STATE_ACTIVE_MODE_OFFSET: usize = 0x08; - const SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET: usize = 0x0c; - const RUNTIME_PROFILE_PTR_ADDR: usize = 0x006cec7c; - const RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET: usize = 0x01; - const RUNTIME_PROFILE_MANUAL_LOAD_PATH_OFFSET: usize = 0x11; - const RUNTIME_PROFILE_PENDING_LOAD_BYTE_OFFSET: usize = 0x97; - const SHELL_MODE_STARTUP_LOAD_DISPATCH: u32 = 1; - const STARTUP_SELECTOR_SCENARIO_LOAD: u8 = 3; - const STARTUP_RUNTIME_OBJECT_SIZE: u32 = 0x00046c40; - const INDEXED_COLLECTION_FLAT_FLAG_OFFSET: usize = 0x04; - const INDEXED_COLLECTION_STRIDE_OFFSET: usize = 0x08; - const INDEXED_COLLECTION_ID_BOUND_OFFSET: usize = 0x14; - const INDEXED_COLLECTION_PAYLOAD_OFFSET: usize = 0x30; - const INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET: usize = 0x34; - const CARGO_STEM_OFFSET: usize = 0x04; - const CARGO_SUBTYPE_OFFSET: usize = 0x32; - const CARGO_ROUTE_STYLE_OFFSET: usize = 0x46; - const CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET: usize = 0x9a; - const COMPANY_ACTIVE_OFFSET: usize = 0x3f; - const COMPANY_OUTSTANDING_SHARES_OFFSET: usize = 0x47; - const COMPANY_COMPANY_VALUE_OFFSET: usize = 0x57; - const COMPANY_BOND_COUNT_OFFSET: usize = 0x5b; - const COMPANY_BOND_TABLE_OFFSET: usize = 0x5f; - const COMPANY_FOUNDING_YEAR_OFFSET: usize = 0x157; - const COMPANY_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163; - const COMPANY_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18; - const COMPANY_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56; - - const SCENARIO_CURRENT_YEAR_OFFSET: usize = 0x0d; - const SCENARIO_BUILDING_DENSITY_GROWTH_OFFSET: usize = 0x4c7c; - const SCENARIO_BANKRUPTCY_TOGGLE_OFFSET: usize = 0x4a8f; - const SCENARIO_BOND_TOGGLE_OFFSET: usize = 0x4a8b; - const SCENARIO_STOCK_TOGGLE_OFFSET: usize = 0x4a87; - const SCENARIO_DIVIDEND_TOGGLE_OFFSET: usize = 0x4a93; - - const MAX_CAPTURE_POLL_ATTEMPTS: usize = 120; - const CAPTURE_POLL_INTERVAL: Duration = Duration::from_secs(1); - const AUTO_LOAD_READY_POLLS: u32 = 1; - const AUTO_LOAD_DEFER_POLLS: u32 = 0; - const MEM_COMMIT: u32 = 0x1000; - const MEM_RESERVE: u32 = 0x2000; - const PAGE_EXECUTE_READWRITE: u32 = 0x40; - unsafe extern "system" { - fn CreateFileA( - lp_file_name: *const c_char, - desired_access: u32, - share_mode: u32, - security_attributes: *mut c_void, - creation_disposition: u32, - flags_and_attributes: u32, - template_file: *mut c_void, - ) -> isize; - fn SetFilePointer( - file: isize, - distance: i32, - distance_high: *mut i32, - move_method: u32, - ) -> u32; - fn WriteFile( - file: isize, - buffer: *const c_void, - bytes_to_write: u32, - bytes_written: *mut u32, - overlapped: *mut c_void, - ) -> i32; - fn CloseHandle(handle: isize) -> i32; - fn DisableThreadLibraryCalls(module: *mut c_void) -> i32; - fn FlushInstructionCache( - process: *mut c_void, - base_address: *const c_void, - size: usize, - ) -> i32; - fn GetCurrentProcess() -> *mut c_void; - fn GetSystemDirectoryA(buffer: *mut u8, size: u32) -> u32; - fn GetProcAddress(module: isize, name: *const c_char) -> *mut c_void; - fn LoadLibraryA(name: *const c_char) -> isize; - fn OutputDebugStringA(output: *const c_char); - fn VirtualAlloc( - address: *mut c_void, - size: usize, - allocation_type: u32, - protect: u32, - ) -> *mut c_void; - fn VirtualProtect( - address: *mut c_void, - size: usize, - new_protect: u32, - old_protect: *mut u32, - ) -> i32; - } - - #[repr(C)] - pub struct Guid { - data1: u32, - data2: u16, - data3: u16, - data4: [u8; 8], - } - - type DirectInput8CreateFn = unsafe extern "system" fn( - instance: *mut c_void, - version: u32, - riid: *const Guid, - out: *mut *mut c_void, - outer: *mut c_void, - ) -> i32; - type ShellStateServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32; - type ShellTransitionModeFn = unsafe extern "thiscall" fn(*mut u8, u32, u32) -> i32; - type ProfileStartupDispatchFn = unsafe extern "thiscall" fn(*mut u8, u32, u32) -> i32; - type RuntimeResetFn = unsafe extern "thiscall" fn(*mut u8) -> *mut u8; - type StartupRuntimeAllocThunkFn = unsafe extern "cdecl" fn(u32) -> *mut u8; - type LoadScreenSetScalarFn = unsafe extern "thiscall" fn(*mut u8, u32) -> u32; - type LoadScreenConstructFn = unsafe extern "thiscall" fn(*mut u8) -> *mut u8; - type LoadScreenHandleMessageFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32; - type ShellRuntimePrimeFn = unsafe extern "thiscall" fn(*mut u8) -> i32; - type ShellFrameCycleFn = unsafe extern "thiscall" fn(*mut u8) -> i32; - type ShellObjectServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32; - type ShellChildServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32; - type ShellPublishWindowFn = unsafe extern "thiscall" fn(*mut u8, *mut u8, u32) -> i32; - type ShellUnpublishWindowFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32; - type ShellObjectTeardownFn = unsafe extern "thiscall" fn(*mut u8) -> i32; - type ShellObjectRangeRemoveFn = unsafe extern "thiscall" fn(*mut u8, u32, u32, u32) -> i32; - type ShellRemoveNodeFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32; - type ShellNodeVcallFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32; - type Mode2TeardownFn = unsafe extern "thiscall" fn(*mut u8) -> i32; - #[unsafe(no_mangle)] - pub extern "system" fn DllMain( - module: *mut c_void, - reason: u32, - _reserved: *mut c_void, - ) -> i32 { - if reason == DLL_PROCESS_ATTACH { - unsafe { - let _ = DisableThreadLibraryCalls(module); - OutputDebugStringA(DEBUG_MESSAGE.as_ptr().cast()); - append_attach_log(); - } - } - 1 - } - - #[unsafe(no_mangle)] - pub extern "system" fn DirectInput8Create( - instance: *mut c_void, - version: u32, - riid: *const Guid, - out: *mut *mut c_void, - outer: *mut c_void, - ) -> i32 { - maybe_emit_finance_template_bundle(); - maybe_start_finance_capture_thread(); - maybe_start_cargo_capture_thread(); - maybe_install_auto_load_hook(); - - let direct_input8_create = unsafe { load_direct_input8_create() }; - match direct_input8_create { - Some(callback) => unsafe { callback(instance, version, riid, out, outer) }, - None => E_FAIL, - } - } - - unsafe fn append_attach_log() { - append_log_message(ATTACH_MESSAGE); - } - - fn append_log_message(message: &[u8]) { - let handle = unsafe { - CreateFileA( - LOG_PATH.as_ptr().cast(), - FILE_APPEND_DATA, - FILE_SHARE_READ, - ptr::null_mut(), - OPEN_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - ptr::null_mut(), - ) - }; - if handle == INVALID_HANDLE_VALUE { - return; - } - - let _ = unsafe { SetFilePointer(handle, 0, ptr::null_mut(), FILE_END) }; - let mut bytes_written = 0_u32; - let _ = unsafe { - WriteFile( - handle, - message.as_ptr().cast(), - message.len() as u32, - &mut bytes_written, - ptr::null_mut(), - ) - }; - let _ = unsafe { CloseHandle(handle) }; - } - - fn append_log_line(line: &str) { - append_log_message(line.as_bytes()); - } - - fn maybe_emit_finance_template_bundle() { - if env::var_os("RRT_WRITE_FINANCE_TEMPLATE").is_none() { - return; - } - if FINANCE_TEMPLATE_EMITTED.swap(true, Ordering::AcqRel) { - return; - } - - let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - let _ = - write_finance_snapshot_bundle(&base_dir, "attach_template", &sample_finance_snapshot()); - } - - fn maybe_start_finance_capture_thread() { - if env::var_os("RRT_WRITE_FINANCE_CAPTURE").is_none() { - return; - } - if FINANCE_CAPTURE_STARTED.swap(true, Ordering::AcqRel) { - return; - } - - append_log_message(FINANCE_CAPTURE_STARTED_MESSAGE); - let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - let _ = thread::Builder::new() - .name("rrt-finance-capture".to_string()) - .spawn(move || { - for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS { - if !FINANCE_COLLECTION_PROBE_WRITTEN.load(Ordering::Acquire) { - if let Some(probe) = unsafe { capture_company_collection_probe() } { - if write_indexed_collection_probe(&base_dir, "attach_probe", &probe) - .is_ok() - { - FINANCE_COLLECTION_PROBE_WRITTEN.store(true, Ordering::Release); - append_log_message(FINANCE_CAPTURE_PROBE_DUMP_WRITTEN_MESSAGE); - } - } - } - if let Some(snapshot) = unsafe { try_capture_probe_snapshot() } { - append_log_message(FINANCE_CAPTURE_COMPANY_RESOLVED_MESSAGE); - if write_finance_snapshot_only(&base_dir, "attach_probe", &snapshot).is_ok() - { - append_log_message(FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE); - return; - } - } - thread::sleep(CAPTURE_POLL_INTERVAL); - } - - append_log_message(FINANCE_CAPTURE_TIMEOUT_MESSAGE); - }); - } - - fn maybe_start_cargo_capture_thread() { - if env::var_os("RRT_WRITE_CARGO_CAPTURE").is_none() { - return; - } - if CARGO_CAPTURE_STARTED.swap(true, Ordering::AcqRel) { - return; - } - - append_log_message(CARGO_CAPTURE_STARTED_MESSAGE); - let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - let _ = thread::Builder::new() - .name("rrt-cargo-capture".to_string()) - .spawn(move || { - let mut last_probe: Option = None; - for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS { - if let Some(probe) = unsafe { capture_cargo_collection_probe() } { - last_probe = Some(probe.clone()); - if probe.live_entry_count > 0 - && write_cargo_collection_probe(&base_dir, "attach_probe", &probe) - .is_ok() - { - append_log_message(CARGO_CAPTURE_WRITTEN_MESSAGE); - return; - } - } - thread::sleep(CAPTURE_POLL_INTERVAL); - } - - if let Some(probe) = last_probe { - let _ = write_cargo_collection_probe(&base_dir, "attach_probe_timeout", &probe); - } - append_log_message(CARGO_CAPTURE_TIMEOUT_MESSAGE); - }); - } - - fn maybe_install_auto_load_hook() { - let save_stem = match env::var("RRT_AUTO_LOAD_SAVE") { - Ok(value) if !value.trim().is_empty() => value, - _ => return, - }; - let _ = AUTO_LOAD_SAVE_STEM.set(save_stem); - if AUTO_LOAD_HOOK_INSTALLED.swap(true, Ordering::AcqRel) { - return; - } - - append_log_message(AUTO_LOAD_STARTED_MESSAGE); - AUTO_LOAD_THREAD_STARTED.store(true, Ordering::Release); - if unsafe { install_shell_state_service_hook() } { - append_log_message(AUTO_LOAD_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_profile_startup_dispatch_hook() } { - append_log_message(AUTO_LOAD_PROFILE_DISPATCH_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_runtime_reset_hook() } { - append_log_message(AUTO_LOAD_RUNTIME_RESET_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_allocator_hook() } { - append_log_message(AUTO_LOAD_ALLOCATOR_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_load_screen_scalar_hook() } { - append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_load_screen_construct_hook() } { - append_log_message(AUTO_LOAD_LOAD_SCREEN_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_load_screen_message_hook() } { - append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_runtime_prime_hook() } { - append_log_message(AUTO_LOAD_RUNTIME_PRIME_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_frame_cycle_hook() } { - append_log_message(AUTO_LOAD_FRAME_CYCLE_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_object_service_hook() } { - append_log_message(AUTO_LOAD_OBJECT_SERVICE_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_child_service_hook() } { - append_log_message(AUTO_LOAD_CHILD_SERVICE_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_shell_publish_hook() } { - append_log_message(AUTO_LOAD_PUBLISH_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_shell_unpublish_hook() } { - append_log_message(AUTO_LOAD_UNPUBLISH_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_shell_object_teardown_hook() } { - append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_shell_object_range_remove_hook() } { - append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_shell_remove_node_hook() } { - append_log_message(AUTO_LOAD_REMOVE_NODE_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_shell_node_vcall_hook() } { - append_log_message(AUTO_LOAD_NODE_VCALL_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - if unsafe { install_mode2_teardown_hook() } { - append_log_message(AUTO_LOAD_MODE2_TEARDOWN_HOOK_INSTALLED_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - } - - fn run_auto_load_worker() { - append_log_message(AUTO_LOAD_CALLING_MESSAGE); - let staged = unsafe { invoke_manual_load_branch() }; - if staged { - append_log_message(AUTO_LOAD_TRIGGERED_MESSAGE); - append_log_message(AUTO_LOAD_SUCCESS_MESSAGE); - } else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - } - AUTO_LOAD_IN_PROGRESS.store(false, Ordering::Release); - } - - unsafe fn invoke_manual_load_branch() -> bool { - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - if shell_state.is_null() { - return false; - } - - let shell_transition_mode: ShellTransitionModeFn = - unsafe { mem::transmute(SHELL_TRANSITION_MODE_ADDR) }; - - AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.store(0, Ordering::Release); - AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.store(true, Ordering::Release); - append_log_message(AUTO_LOAD_OWNER_ENTRY_MESSAGE); - let _ = unsafe { shell_transition_mode(shell_state, SHELL_MODE_STARTUP_LOAD_DISPATCH, 0) }; - AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.store(false, Ordering::Release); - append_log_message(AUTO_LOAD_OWNER_RETURNED_MESSAGE); - log_post_transition_state(); - - true - } - - unsafe fn stage_manual_load_request(save_stem: &str) -> bool { - if save_stem.is_empty() || save_stem.as_bytes().contains(&0) { - return false; - } - - let runtime_profile = unsafe { read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8) }; - if runtime_profile.is_null() { - return false; - } - - let path_seed = unsafe { runtime_profile.add(RUNTIME_PROFILE_MANUAL_LOAD_PATH_OFFSET) }; - if unsafe { write_c_string(path_seed, 260, save_stem.as_bytes()) }.is_none() { - return false; - } - - unsafe { - ptr::write_unaligned( - runtime_profile - .add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET) - .cast::(), - STARTUP_SELECTOR_SCENARIO_LOAD, - ) - }; - unsafe { - ptr::write_unaligned( - runtime_profile - .add(RUNTIME_PROFILE_PENDING_LOAD_BYTE_OFFSET) - .cast::(), - 0, - ) - }; - log_auto_load_launch_state(runtime_profile); - true - } - - fn log_auto_load_launch_state(runtime_profile: *mut u8) { - let startup_selector = - unsafe { read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) }; - let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - let mode_id = if shell_state.is_null() { - 0 - } else { - unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize } - }; - let mut line = String::from("rrt-hook: auto load launch state "); - let _ = write!( - &mut line, - "selector=0x{startup_selector:02x} mode_id=0x{mode_id:08x} global_active_mode=0x{global_active_mode:08x} target_mode=0x{SHELL_MODE_STARTUP_LOAD_DISPATCH:08x}\n", - ); - append_log_line(&line); - } - - fn log_post_transition_state() { - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - let active_mode_global = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let field_active_mode_object = if shell_state.is_null() { - 0 - } else { - unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } - }; - let runtime_profile = unsafe { read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8) }; - let startup_selector = if runtime_profile.is_null() { - 0 - } else { - unsafe { - read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize - } - }; - let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) }; - let load_screen_scalar = if load_screen_singleton.is_null() { - 0 - } else { - unsafe { read_u32(load_screen_singleton.add(0x78)) } - }; - let mut line = String::from("rrt-hook: auto load post-transition state "); - let _ = write!( - &mut line, - "shell_state=0x{:08x} active_mode_global=0x{active_mode_global:08x} field_active_mode_object=0x{field_active_mode_object:08x} load_screen_singleton=0x{:08x} load_screen_scalar=0x{load_screen_scalar:08x} startup_selector=0x{startup_selector:08x}\n", - shell_state as usize, load_screen_singleton as usize, - ); - append_log_line(&line); - } - - fn log_post_transition_service_state() { - if !AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) { - return; - } - let count = AUTO_LOAD_POST_TRANSITION_SERVICE_LOG_COUNT.fetch_add(1, Ordering::AcqRel); - if count >= 8 { - return; - } - - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - let active_mode_global = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let field_active_mode_object = if shell_state.is_null() { - 0 - } else { - unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } - }; - let runtime_profile = unsafe { read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8) }; - let startup_selector = if runtime_profile.is_null() { - 0 - } else { - unsafe { - read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize - } - }; - let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) }; - let load_screen_scalar = if load_screen_singleton.is_null() { - 0 - } else { - unsafe { read_u32(load_screen_singleton.add(0x78)) } - }; - let mut line = String::from("rrt-hook: auto load post-transition service state "); - let _ = write!( - &mut line, - "count=0x{:08x} shell_state=0x{:08x} active_mode_global=0x{active_mode_global:08x} field_active_mode_object=0x{field_active_mode_object:08x} load_screen_singleton=0x{:08x} load_screen_scalar=0x{load_screen_scalar:08x} startup_selector=0x{startup_selector:08x}\n", - count + 1, - shell_state as usize, - load_screen_singleton as usize, - ); - append_log_line(&line); - } - - unsafe fn write_c_string(destination: *mut u8, capacity: usize, bytes: &[u8]) -> Option<()> { - if bytes.len() + 1 > capacity { - return None; - } - - unsafe { ptr::write_bytes(destination, 0, capacity) }; - unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), destination, bytes.len()) }; - Some(()) - } - - unsafe fn try_capture_probe_snapshot() -> Option { - append_log_message(FINANCE_CAPTURE_SCAN_MESSAGE); - let company = unsafe { resolve_first_active_company()? }; - Some(unsafe { capture_probe_snapshot_from_company(company) }) - } - - unsafe fn runtime_saved_world_restore_gate_mask() -> u32 { - let mut mask = 0_u32; - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - if !shell_state.is_null() { - mask |= 0x1; - } - let shell_controller = unsafe { read_ptr(SHELL_CONTROLLER_PTR_ADDR as *const u8) }; - if !shell_controller.is_null() { - mask |= 0x2; - } - let active_mode = unsafe { resolve_active_mode_ptr() }; - if !active_mode.is_null() { - mask |= 0x4; - } - mask - } - - unsafe fn current_mode_id() -> u32 { - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - if shell_state.is_null() { - return 0; - } - unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) } - } - - fn auto_load_ready_polls() -> u32 { - env::var("RRT_AUTO_LOAD_READY_POLLS") - .ok() - .and_then(|value| value.parse::().ok()) - .filter(|value| *value > 0) - .unwrap_or(AUTO_LOAD_READY_POLLS) - } - - fn auto_load_defer_polls() -> u32 { - env::var("RRT_AUTO_LOAD_DEFER_POLLS") - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(AUTO_LOAD_DEFER_POLLS) - } - - unsafe extern "fastcall" fn shell_state_service_detour(this: *mut u8, _edx: usize) -> i32 { - log_shell_state_service_entry(this); - let trampoline: ShellStateServiceFn = - unsafe { mem::transmute(SHELL_STATE_SERVICE_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - log_shell_state_service_return(this, result); - log_post_transition_service_state(); - maybe_service_auto_load_on_main_thread(); - result - } - - unsafe extern "fastcall" fn profile_startup_dispatch_detour( - this: *mut u8, - _edx: usize, - arg1: u32, - arg2: u32, - ) -> i32 { - log_profile_startup_dispatch_entry(this, arg1, arg2); - append_log_message(AUTO_LOAD_PROFILE_DISPATCH_ENTRY_MESSAGE); - let trampoline: ProfileStartupDispatchFn = - unsafe { mem::transmute(PROFILE_STARTUP_DISPATCH_TRAMPOLINE) }; - let result = unsafe { trampoline(this, arg1, arg2) }; - append_log_message(AUTO_LOAD_PROFILE_DISPATCH_RETURNED_MESSAGE); - log_profile_startup_dispatch_return(this, arg1, arg2, result); - result - } - - unsafe extern "fastcall" fn runtime_reset_detour(this: *mut u8, _edx: usize) -> *mut u8 { - append_log_message(AUTO_LOAD_RUNTIME_RESET_ENTRY_MESSAGE); - log_runtime_reset_entry(this); - let trampoline: RuntimeResetFn = unsafe { mem::transmute(RUNTIME_RESET_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - append_log_message(AUTO_LOAD_RUNTIME_RESET_RETURNED_MESSAGE); - log_runtime_reset_return(this, result); - result - } - - unsafe extern "cdecl" fn allocator_detour(size: u32) -> *mut u8 { - let trace = should_trace_allocator(size); - let count = if trace { - AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 - } else { - 0 - }; - if trace { - append_log_message(AUTO_LOAD_ALLOCATOR_ENTRY_MESSAGE); - log_allocator_entry(size, count); - } - let original: StartupRuntimeAllocThunkFn = unsafe { mem::transmute(0x005a125dusize) }; - let result = unsafe { original(size) }; - if trace { - append_log_message(AUTO_LOAD_ALLOCATOR_RETURNED_MESSAGE); - log_allocator_return(size, count, result); - } - result - } - - unsafe extern "fastcall" fn load_screen_scalar_detour( - this: *mut u8, - _edx: usize, - value_bits: u32, - ) -> u32 { - append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_ENTRY_MESSAGE); - log_load_screen_scalar_entry(this, value_bits); - let trampoline: LoadScreenSetScalarFn = - unsafe { mem::transmute(LOAD_SCREEN_SCALAR_TRAMPOLINE) }; - let result = unsafe { trampoline(this, value_bits) }; - append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_RETURNED_MESSAGE); - log_load_screen_scalar_return(this, value_bits, result); - result - } - - unsafe extern "fastcall" fn load_screen_construct_detour( - this: *mut u8, - _edx: usize, - ) -> *mut u8 { - append_log_message(AUTO_LOAD_LOAD_SCREEN_ENTRY_MESSAGE); - log_load_screen_construct_entry(this); - let trampoline: LoadScreenConstructFn = - unsafe { mem::transmute(LOAD_SCREEN_CONSTRUCT_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - append_log_message(AUTO_LOAD_LOAD_SCREEN_RETURNED_MESSAGE); - log_load_screen_construct_return(this, result); - result - } - - unsafe extern "fastcall" fn load_screen_message_detour( - this: *mut u8, - _edx: usize, - message: *mut u8, - ) -> i32 { - let trace = should_trace_load_screen_message(this); - let count = if trace { - AUTO_LOAD_LOAD_SCREEN_MESSAGE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 - } else { - 0 - }; - if trace { - append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_ENTRY_MESSAGE); - log_load_screen_message_entry(this, message, count); - } - let trampoline: LoadScreenHandleMessageFn = - unsafe { mem::transmute(LOAD_SCREEN_MESSAGE_TRAMPOLINE) }; - let result = unsafe { trampoline(this, message) }; - if trace { - append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_RETURNED_MESSAGE); - log_load_screen_message_return(this, message, count, result); - } - result - } - - unsafe extern "fastcall" fn runtime_prime_detour(this: *mut u8, _edx: usize) -> i32 { - let trace = should_trace_runtime_prime(); - let count = if trace { - AUTO_LOAD_RUNTIME_PRIME_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 - } else { - 0 - }; - if trace { - append_log_message(AUTO_LOAD_RUNTIME_PRIME_ENTRY_MESSAGE); - log_runtime_prime_entry(this, count); - } - let trampoline: ShellRuntimePrimeFn = unsafe { mem::transmute(RUNTIME_PRIME_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - if trace { - append_log_message(AUTO_LOAD_RUNTIME_PRIME_RETURNED_MESSAGE); - log_runtime_prime_return(this, count, result); - } - result - } - - unsafe extern "fastcall" fn frame_cycle_detour(this: *mut u8, _edx: usize) -> i32 { - let trace = should_trace_frame_cycle(); - let count = if trace { - AUTO_LOAD_FRAME_CYCLE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 - } else { - 0 - }; - if trace { - append_log_message(AUTO_LOAD_FRAME_CYCLE_ENTRY_MESSAGE); - log_frame_cycle_entry(this, count); - } - let trampoline: ShellFrameCycleFn = unsafe { mem::transmute(FRAME_CYCLE_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - if trace { - append_log_message(AUTO_LOAD_FRAME_CYCLE_RETURNED_MESSAGE); - log_frame_cycle_return(this, count, result); - } - result - } - - unsafe extern "fastcall" fn object_service_detour(this: *mut u8, _edx: usize) -> i32 { - let trace = should_trace_object_service(); - let count = if trace { - AUTO_LOAD_OBJECT_SERVICE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 - } else { - 0 - }; - if trace { - append_log_message(AUTO_LOAD_OBJECT_SERVICE_ENTRY_MESSAGE); - log_object_service_entry(this, count); - } - let trampoline: ShellObjectServiceFn = unsafe { mem::transmute(OBJECT_SERVICE_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - if trace { - append_log_message(AUTO_LOAD_OBJECT_SERVICE_RETURNED_MESSAGE); - log_object_service_return(this, count, result); - } - result - } - - unsafe extern "fastcall" fn child_service_detour(this: *mut u8, _edx: usize) -> i32 { - let trace = should_trace_child_service(); - let count = if trace { - AUTO_LOAD_CHILD_SERVICE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 - } else { - 0 - }; - if trace { - append_log_message(AUTO_LOAD_CHILD_SERVICE_ENTRY_MESSAGE); - log_child_service_entry(this, count); - } - let trampoline: ShellChildServiceFn = unsafe { mem::transmute(CHILD_SERVICE_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - if trace { - append_log_message(AUTO_LOAD_CHILD_SERVICE_RETURNED_MESSAGE); - log_child_service_return(this, count, result); - } - result - } - - unsafe extern "fastcall" fn shell_publish_detour( - this: *mut u8, - _edx: usize, - object: *mut u8, - flag: u32, - ) -> i32 { - append_log_message(AUTO_LOAD_PUBLISH_ENTRY_MESSAGE); - log_shell_publish_entry(this, object, flag); - let trampoline: ShellPublishWindowFn = unsafe { mem::transmute(SHELL_PUBLISH_TRAMPOLINE) }; - let result = unsafe { trampoline(this, object, flag) }; - append_log_message(AUTO_LOAD_PUBLISH_RETURNED_MESSAGE); - log_shell_publish_return(this, object, flag, result); - result - } - - unsafe extern "fastcall" fn shell_unpublish_detour( - this: *mut u8, - _edx: usize, - object: *mut u8, - ) -> i32 { - append_log_message(AUTO_LOAD_UNPUBLISH_ENTRY_MESSAGE); - log_shell_unpublish_entry(this, object); - let trampoline: ShellUnpublishWindowFn = - unsafe { mem::transmute(SHELL_UNPUBLISH_TRAMPOLINE) }; - let result = unsafe { trampoline(this, object) }; - append_log_message(AUTO_LOAD_UNPUBLISH_RETURNED_MESSAGE); - log_shell_unpublish_return(this, object, result); - result - } - - unsafe extern "fastcall" fn shell_object_range_remove_detour( - this: *mut u8, - _edx: usize, - arg1: u32, - arg2: u32, - arg3: u32, - ) -> i32 { - append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_ENTRY_MESSAGE); - log_shell_object_range_remove_entry(this, arg1, arg2, arg3); - let trampoline: ShellObjectRangeRemoveFn = - unsafe { mem::transmute(SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE) }; - let result = unsafe { trampoline(this, arg1, arg2, arg3) }; - append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_RETURNED_MESSAGE); - log_shell_object_range_remove_return(this, arg1, arg2, arg3, result); - result - } - - unsafe extern "fastcall" fn shell_object_teardown_detour(this: *mut u8, _edx: usize) -> i32 { - append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_ENTRY_MESSAGE); - log_shell_object_teardown_entry(this); - let trampoline: ShellObjectTeardownFn = - unsafe { mem::transmute(SHELL_OBJECT_TEARDOWN_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_RETURNED_MESSAGE); - log_shell_object_teardown_return(this, result); - result - } - - unsafe extern "fastcall" fn shell_node_vcall_detour( - this: *mut u8, - _edx: usize, - record: *mut u8, - ) -> i32 { - append_log_message(AUTO_LOAD_NODE_VCALL_ENTRY_MESSAGE); - log_shell_node_vcall_entry(this, record); - let trampoline: ShellNodeVcallFn = unsafe { mem::transmute(SHELL_NODE_VCALL_TRAMPOLINE) }; - let result = unsafe { trampoline(this, record) }; - append_log_message(AUTO_LOAD_NODE_VCALL_RETURNED_MESSAGE); - log_shell_node_vcall_return(this, record, result); - result - } - - unsafe extern "fastcall" fn shell_remove_node_detour( - this: *mut u8, - _edx: usize, - node: *mut u8, - ) -> i32 { - append_log_message(AUTO_LOAD_REMOVE_NODE_ENTRY_MESSAGE); - log_shell_remove_node_entry(this, node); - let trampoline: ShellRemoveNodeFn = unsafe { mem::transmute(SHELL_REMOVE_NODE_TRAMPOLINE) }; - let result = unsafe { trampoline(this, node) }; - append_log_message(AUTO_LOAD_REMOVE_NODE_RETURNED_MESSAGE); - log_shell_remove_node_return(this, node, result); - result - } - - unsafe extern "fastcall" fn mode2_teardown_detour(this: *mut u8, _edx: usize) -> i32 { - append_log_message(AUTO_LOAD_MODE2_TEARDOWN_ENTRY_MESSAGE); - log_mode2_teardown_entry(this); - let trampoline: Mode2TeardownFn = unsafe { mem::transmute(MODE2_TEARDOWN_TRAMPOLINE) }; - let result = unsafe { trampoline(this) }; - append_log_message(AUTO_LOAD_MODE2_TEARDOWN_RETURNED_MESSAGE); - log_mode2_teardown_return(this, result); - result - } - - fn maybe_service_auto_load_on_main_thread() { - if !AUTO_LOAD_HOOK_INSTALLED.load(Ordering::Acquire) - || AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) - || AUTO_LOAD_IN_PROGRESS.load(Ordering::Acquire) - { - return; - } - - let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() }; - let last_gate_mask = AUTO_LOAD_LAST_GATE_MASK.swap(gate_mask, Ordering::AcqRel); - if gate_mask != last_gate_mask { - log_auto_load_gate_mask(gate_mask); - } - - let mode_id = unsafe { current_mode_id() }; - let ready = gate_mask == 0x7 && mode_id == 2; - let ready_count = if ready { - AUTO_LOAD_READY_COUNT.fetch_add(1, Ordering::AcqRel) + 1 - } else { - AUTO_LOAD_READY_COUNT.store(0, Ordering::Release); - AUTO_LOAD_DEFERRED.store(false, Ordering::Release); - 0 - }; - if ready { - append_log_message(AUTO_LOAD_READY_COUNT_MESSAGE); - log_auto_load_ready_count(ready_count, gate_mask, mode_id); - } - - let ready_polls = auto_load_ready_polls(); - if ready_count < ready_polls { - return; - } - - if !AUTO_LOAD_DEFERRED.load(Ordering::Acquire) { - AUTO_LOAD_DEFERRED.store(true, Ordering::Release); - append_log_message(AUTO_LOAD_READY_MESSAGE); - append_log_message(AUTO_LOAD_DEFERRED_MESSAGE); - return; - } - - if ready_count < ready_polls.saturating_add(auto_load_defer_polls()) { - return; - } - - if AUTO_LOAD_ATTEMPTED.swap(true, Ordering::AcqRel) { - return; - } - - if !AUTO_LOAD_TRANSITION_ARMED.load(Ordering::Acquire) { - let Some(save_stem) = AUTO_LOAD_SAVE_STEM.get() else { - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release); - return; - }; - if unsafe { stage_manual_load_request(save_stem) } { - AUTO_LOAD_TRANSITION_ARMED.store(true, Ordering::Release); - AUTO_LOAD_ARMED_TICK_COUNT.store(0, Ordering::Release); - append_log_message(AUTO_LOAD_STAGED_MESSAGE); - log_auto_load_armed_tick(); - AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release); - AUTO_LOAD_IN_PROGRESS.store(true, Ordering::Release); - append_log_message(AUTO_LOAD_ARMED_TICK_MESSAGE); - append_log_message(AUTO_LOAD_READY_MESSAGE); - run_auto_load_worker(); - return; - } - append_log_message(AUTO_LOAD_FAILURE_MESSAGE); - AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release); - return; - } - - AUTO_LOAD_IN_PROGRESS.store(true, Ordering::Release); - append_log_message(AUTO_LOAD_READY_MESSAGE); - run_auto_load_worker(); - } - - fn log_auto_load_gate_mask(mask: u32) { - let mut line = String::from("rrt-hook: auto load gate mask "); - let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - let mode_id = if shell_state.is_null() { - 0 - } else { - unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize } - }; - let field_active_mode_object = if shell_state.is_null() { - 0 - } else { - unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } - }; - let startup_selector = unsafe { - let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8); - if runtime_profile.is_null() { - 0 - } else { - read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize - } - }; - let _ = write!( - &mut line, - "0x{mask:01x} shell_state={} shell_controller={} active_mode={} global_active_mode=0x{global_active_mode:08x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x} field_active_mode_object=0x{field_active_mode_object:08x}\n", - (mask & 0x1) != 0, - (mask & 0x2) != 0, - (mask & 0x4) != 0, - ); - append_log_line(&line); - } - - fn log_auto_load_armed_tick() { - let tick_count = AUTO_LOAD_ARMED_TICK_COUNT.fetch_add(1, Ordering::AcqRel) + 1; - let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() }; - let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - let mode_id = if shell_state.is_null() { - 0 - } else { - unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize } - }; - let startup_selector = unsafe { - let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8); - if runtime_profile.is_null() { - 0 - } else { - read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize - } - }; - let mut line = String::from("rrt-hook: auto load armed tick "); - let _ = write!( - &mut line, - "count=0x{tick_count:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x} global_active_mode=0x{global_active_mode:08x}\n", - ); - append_log_line(&line); - } - - fn log_auto_load_ready_count(count: u32, gate_mask: u32, mode_id: u32) { - let startup_selector = unsafe { - let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8); - if runtime_profile.is_null() { - 0 - } else { - read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize - } - }; - let mut line = String::from("rrt-hook: auto load ready count "); - let _ = write!( - &mut line, - "count=0x{count:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x}\n", - ); - append_log_line(&line); - } - - fn log_shell_state_service_entry(this: *mut u8) { - let count = AUTO_LOAD_SERVICE_ENTRY_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1; - if count > 8 { - return; - } - let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() }; - let mode_id = unsafe { current_mode_id() }; - let active_mode_global = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let field_active_mode_object = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } - }; - let field_a0 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0xa0)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell-state service entry "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} active_mode_global=0x{active_mode_global:08x} field_active_mode_object=0x{field_active_mode_object:08x} field_a0=0x{field_a0:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_shell_state_service_return(this: *mut u8, result: i32) { - let count = AUTO_LOAD_SERVICE_RETURN_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1; - if count > 8 { - return; - } - let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() }; - let mode_id = unsafe { current_mode_id() }; - let active_mode_global = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let field_active_mode_object = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } - }; - let field_a0 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0xa0)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell-state service return "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} active_mode_global=0x{active_mode_global:08x} field_active_mode_object=0x{field_active_mode_object:08x} field_a0=0x{field_a0:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_profile_startup_dispatch_entry(this: *mut u8, arg1: u32, arg2: u32) { - let startup_selector = unsafe { - let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8); - if runtime_profile.is_null() { - 0 - } else { - read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize - } - }; - let mut line = String::from("rrt-hook: auto load startup dispatch entry "); - let _ = write!( - &mut line, - "this=0x{:08x} arg1=0x{arg1:08x} arg2=0x{arg2:08x} selector=0x{startup_selector:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_profile_startup_dispatch_return(this: *mut u8, arg1: u32, arg2: u32, result: i32) { - let active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load startup dispatch return "); - let _ = write!( - &mut line, - "this=0x{:08x} arg1=0x{arg1:08x} arg2=0x{arg2:08x} result=0x{result:08x} active_mode=0x{active_mode:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_runtime_reset_entry(this: *mut u8) { - let field_4cae = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x4cae)) as usize } - }; - let field_4cb2 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x4cb2)) as usize } - }; - let field_66b2 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x66b2)) as usize } - }; - let field_66b6 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x66b6)) as usize } - }; - let mut line = String::from("rrt-hook: auto load runtime reset entry "); - let _ = write!( - &mut line, - "this=0x{:08x} field_4cae=0x{field_4cae:08x} field_4cb2=0x{field_4cb2:08x} field_66b2=0x{field_66b2:08x} field_66b6=0x{field_66b6:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_runtime_reset_return(this: *mut u8, result: *mut u8) { - let field_4cae = if result.is_null() { - 0 - } else { - unsafe { read_u32(result.add(0x4cae)) as usize } - }; - let field_4cb2 = if result.is_null() { - 0 - } else { - unsafe { read_u32(result.add(0x4cb2)) as usize } - }; - let field_66b2 = if result.is_null() { - 0 - } else { - unsafe { read_u32(result.add(0x66b2)) as usize } - }; - let field_66b6 = if result.is_null() { - 0 - } else { - unsafe { read_u32(result.add(0x66b6)) as usize } - }; - let mut line = String::from("rrt-hook: auto load runtime reset return "); - let _ = write!( - &mut line, - "this=0x{:08x} result=0x{:08x} field_4cae=0x{field_4cae:08x} field_4cb2=0x{field_4cb2:08x} field_66b2=0x{field_66b2:08x} field_66b6=0x{field_66b6:08x}\n", - this as usize, result as usize, - ); - append_log_line(&line); - } - - fn should_trace_allocator(size: u32) -> bool { - if size == STARTUP_RUNTIME_OBJECT_SIZE { - return true; - } - AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.load(Ordering::Acquire) - && AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.load(Ordering::Acquire) < 16 - } - - fn log_allocator_entry(size: u32, count: u32) { - let mut line = String::from("rrt-hook: auto load allocator entry "); - let transition_window = AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.load(Ordering::Acquire); - let _ = write!( - &mut line, - "count=0x{count:08x} size=0x{size:08x} transition_window={transition_window}\n", - ); - append_log_line(&line); - } - - fn log_allocator_return(size: u32, count: u32, result: *mut u8) { - let mut line = String::from("rrt-hook: auto load allocator return "); - let _ = write!( - &mut line, - "count=0x{count:08x} size=0x{size:08x} result=0x{:08x}\n", - result as usize, - ); - append_log_line(&line); - } - - fn log_load_screen_scalar_entry(this: *mut u8, value_bits: u32) { - let field_78 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x78)) } - }; - let mut line = String::from("rrt-hook: auto load load-screen scalar entry "); - let _ = write!( - &mut line, - "this=0x{:08x} value_bits=0x{value_bits:08x} field_78=0x{field_78:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_load_screen_scalar_return(this: *mut u8, value_bits: u32, result: u32) { - let field_78 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x78)) } - }; - let mut line = String::from("rrt-hook: auto load load-screen scalar return "); - let _ = write!( - &mut line, - "this=0x{:08x} value_bits=0x{value_bits:08x} result=0x{result:08x} field_78=0x{field_78:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_load_screen_construct_entry(this: *mut u8) { - let mut line = String::from("rrt-hook: auto load load-screen construct entry "); - let _ = write!(&mut line, "this=0x{:08x}\n", this as usize); - append_log_line(&line); - } - - fn log_load_screen_construct_return(this: *mut u8, result: *mut u8) { - let singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load load-screen construct return "); - let _ = write!( - &mut line, - "this=0x{:08x} result=0x{:08x} singleton=0x{singleton:08x}\n", - this as usize, result as usize, - ); - append_log_line(&line); - if AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.load(Ordering::Acquire) { - AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.store(0, Ordering::Release); - } - } - - fn should_trace_load_screen_message(this: *mut u8) -> bool { - if !AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) { - return false; - } - let singleton = unsafe { read_ptr(0x006d10b0 as *const u8) }; - if singleton.is_null() || this != singleton { - return false; - } - AUTO_LOAD_LOAD_SCREEN_MESSAGE_LOG_COUNT.load(Ordering::Acquire) < 16 - } - - fn log_load_screen_message_entry(this: *mut u8, message: *mut u8, count: u32) { - let message_id = if message.is_null() { - 0 - } else { - unsafe { read_u32(message) } - }; - let message_arg8 = if message.is_null() { - 0 - } else { - unsafe { read_u32(message.add(0x08)) } - }; - let page = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x78)) } - }; - let page_substate = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x7c)) } - }; - let page_kind = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x80)) } - }; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load load-screen message entry "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} message=0x{:08x} message_id=0x{message_id:08x} message_arg8=0x{message_arg8:08x} page=0x{page:08x} page_substate=0x{page_substate:08x} page_kind=0x{page_kind:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, message as usize, - ); - append_log_line(&line); - } - - fn log_load_screen_message_return(this: *mut u8, message: *mut u8, count: u32, result: i32) { - let page = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x78)) } - }; - let page_substate = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x7c)) } - }; - let page_kind = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x80)) } - }; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load load-screen message return "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} message=0x{:08x} result=0x{result:08x} page=0x{page:08x} page_substate=0x{page_substate:08x} page_kind=0x{page_kind:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - message as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn should_trace_runtime_prime() -> bool { - AUTO_LOAD_RUNTIME_PRIME_LOG_COUNT.load(Ordering::Acquire) < 12 - } - - fn log_runtime_prime_entry(this: *mut u8, count: u32) { - let list_head = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x04)) as usize } - }; - let field_0c68 = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x0c68)) as usize } - }; - let field_001c = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x1c)) as usize } - }; - let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell runtime-prime entry "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} list_head=0x{list_head:08x} field_0c68=0x{field_0c68:02x} field_1c=0x{field_001c:08x} load_screen_singleton=0x{load_screen_singleton:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_runtime_prime_return(this: *mut u8, count: u32, result: i32) { - let list_head = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x04)) as usize } - }; - let field_001c = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x1c)) as usize } - }; - let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell runtime-prime return "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} list_head=0x{list_head:08x} field_1c=0x{field_001c:08x} load_screen_singleton=0x{load_screen_singleton:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn should_trace_frame_cycle() -> bool { - AUTO_LOAD_FRAME_CYCLE_LOG_COUNT.load(Ordering::Acquire) < 12 - } - - fn log_frame_cycle_entry(this: *mut u8, count: u32) { - let field_18 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x18)) as usize } - }; - let field_1c = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x1c)) as usize } - }; - let field_20 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x20)) as usize } - }; - let field_28 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x28)) as usize } - }; - let flag_55 = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x55)) as usize } - }; - let flag_56 = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x56)) as usize } - }; - let field_58 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x58)) as usize } - }; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell frame-cycle entry "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} field_18=0x{field_18:08x} field_1c=0x{field_1c:08x} field_20=0x{field_20:08x} field_28=0x{field_28:08x} flag_55=0x{flag_55:02x} flag_56=0x{flag_56:02x} field_58=0x{field_58:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_frame_cycle_return(this: *mut u8, count: u32, result: i32) { - let flag_55 = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x55)) as usize } - }; - let flag_56 = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x56)) as usize } - }; - let field_58 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x58)) as usize } - }; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell frame-cycle return "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} flag_55=0x{flag_55:02x} flag_56=0x{flag_56:02x} field_58=0x{field_58:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn should_trace_object_service() -> bool { - AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) - && AUTO_LOAD_OBJECT_SERVICE_LOG_COUNT.load(Ordering::Acquire) < 16 - } - - fn log_object_service_entry(this: *mut u8, count: u32) { - let vtable = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this) as usize } - }; - let field_1d = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x1d)) as usize } - }; - let field_5c = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x5c)) as usize } - }; - let child_head = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x70)) as usize } - }; - let child_tail = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x74)) as usize } - }; - let first_child_vtable = if child_head == 0 { - 0 - } else { - unsafe { read_ptr(child_head as *const u8) as usize } - }; - let first_child_call18 = if first_child_vtable == 0 { - 0 - } else { - unsafe { read_ptr((first_child_vtable + 0x18) as *const u8) as usize } - }; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell object-service entry "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} vtable=0x{vtable:08x} field_1d=0x{field_1d:02x} field_5c=0x{field_5c:08x} child_head=0x{child_head:08x} child_tail=0x{child_tail:08x} child_call18=0x{first_child_call18:08x} load_screen_singleton=0x{load_screen_singleton:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_object_service_return(this: *mut u8, count: u32, result: i32) { - let field_1d = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x1d)) as usize } - }; - let child_head = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x70)) as usize } - }; - let child_tail = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x74)) as usize } - }; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell object-service return "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} field_1d=0x{field_1d:02x} child_head=0x{child_head:08x} child_tail=0x{child_tail:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn should_trace_child_service() -> bool { - AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) - && AUTO_LOAD_CHILD_SERVICE_LOG_COUNT.load(Ordering::Acquire) < 16 - } - - fn log_child_service_entry(this: *mut u8, count: u32) { - let vtable = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this) as usize } - }; - let field_4b = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x4b)) as usize } - }; - let flag_68 = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x68)) as usize } - }; - let flag_6a = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x6a)) as usize } - }; - let field_86 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x86)) as usize } - }; - let field_b0 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0xb0)) as usize } - }; - let field_b8 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0xb8)) as usize } - }; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell child-service entry "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} vtable=0x{vtable:08x} field_4b=0x{field_4b:08x} flag_68=0x{flag_68:02x} flag_6a=0x{flag_6a:02x} field_86=0x{field_86:08x} field_b0=0x{field_b0:08x} field_b8=0x{field_b8:08x} load_screen_singleton=0x{load_screen_singleton:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_child_service_return(this: *mut u8, count: u32, result: i32) { - let field_b0 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0xb0)) as usize } - }; - let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell child-service return "); - let _ = write!( - &mut line, - "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} field_b0=0x{field_b0:08x} startup_runtime=0x{startup_runtime:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_shell_publish_entry(this: *mut u8, object: *mut u8, flag: u32) { - let mut line = String::from("rrt-hook: auto load shell publish entry "); - let _ = write!( - &mut line, - "this=0x{:08x} object=0x{:08x} flag=0x{flag:08x}\n", - this as usize, object as usize, - ); - append_log_line(&line); - } - - fn log_shell_publish_return(this: *mut u8, object: *mut u8, flag: u32, result: i32) { - let active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; - let mut line = String::from("rrt-hook: auto load shell publish return "); - let _ = write!( - &mut line, - "this=0x{:08x} object=0x{:08x} flag=0x{flag:08x} result=0x{result:08x} active_mode=0x{active_mode:08x}\n", - this as usize, - object as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_shell_unpublish_entry(this: *mut u8, object: *mut u8) { - let bundle_head = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this) as usize } - }; - let bundle_tail = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x04)) as usize } - }; - let object_vtable = if object.is_null() { - 0 - } else { - unsafe { read_ptr(object) as usize } - }; - let object_field_04 = if object.is_null() { - 0 - } else { - unsafe { read_ptr(object.add(0x04)) as usize } - }; - let object_field_08 = if object.is_null() { - 0 - } else { - unsafe { read_ptr(object.add(0x08)) as usize } - }; - let object_field_0c = if object.is_null() { - 0 - } else { - unsafe { read_ptr(object.add(0x0c)) as usize } - }; - let object_prev = if object.is_null() { - 0 - } else { - unsafe { read_ptr(object.add(0x54)) as usize } - }; - let object_next = if object.is_null() { - 0 - } else { - unsafe { read_ptr(object.add(0x58)) as usize } - }; - let object_list_74 = if object.is_null() { - 0 - } else { - unsafe { read_ptr(object.add(0x74)) as usize } - }; - let list_field_00 = if object_list_74 == 0 { - 0 - } else { - unsafe { read_ptr(object_list_74 as *const u8) as usize } - }; - let list_field_04 = if object_list_74 == 0 { - 0 - } else { - unsafe { read_u16((object_list_74 as *const u8).add(0x04)) as usize } - }; - let list_field_4b = if object_list_74 == 0 { - 0 - } else { - unsafe { read_u32((object_list_74 as *const u8).add(0x4b)) as usize } - }; - let list_field_8a = if object_list_74 == 0 { - 0 - } else { - unsafe { read_ptr((object_list_74 as *const u8).add(0x8a)) as usize } - }; - let list_vcall_04 = if list_field_00 == 0 { - 0 - } else { - unsafe { read_ptr((list_field_00 as *const u8).add(0x04)) as usize } - }; - let list2_field_00 = if list_field_8a == 0 { - 0 - } else { - unsafe { read_ptr(list_field_8a as *const u8) as usize } - }; - let list2_field_04 = if list_field_8a == 0 { - 0 - } else { - unsafe { read_u16((list_field_8a as *const u8).add(0x04)) as usize } - }; - let list2_field_4b = if list_field_8a == 0 { - 0 - } else { - unsafe { read_u32((list_field_8a as *const u8).add(0x4b)) as usize } - }; - let list2_field_8a = if list_field_8a == 0 { - 0 - } else { - unsafe { read_ptr((list_field_8a as *const u8).add(0x8a)) as usize } - }; - let list2_vcall_04 = if list2_field_00 == 0 { - 0 - } else { - unsafe { read_ptr((list2_field_00 as *const u8).add(0x04)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell unpublish entry "); - let _ = write!( - &mut line, - "this=0x{:08x} object=0x{:08x} bundle_head=0x{bundle_head:08x} bundle_tail=0x{bundle_tail:08x} object_vtable=0x{object_vtable:08x} field_04=0x{object_field_04:08x} field_08=0x{object_field_08:08x} field_0c=0x{object_field_0c:08x} prev=0x{object_prev:08x} next=0x{object_next:08x} list_74=0x{object_list_74:08x} list_00=0x{list_field_00:08x} list_call4=0x{list_vcall_04:08x} list_04=0x{list_field_04:04x} list_4b=0x{list_field_4b:08x} list_8a=0x{list_field_8a:08x} list2_00=0x{list2_field_00:08x} list2_call4=0x{list2_vcall_04:08x} list2_04=0x{list2_field_04:04x} list2_4b=0x{list2_field_4b:08x} list2_8a=0x{list2_field_8a:08x}\n", - this as usize, object as usize, - ); - append_log_line(&line); - } - - fn log_shell_unpublish_return(this: *mut u8, object: *mut u8, result: i32) { - let mut line = String::from("rrt-hook: auto load shell unpublish return "); - let _ = write!( - &mut line, - "this=0x{:08x} object=0x{:08x} result=0x{result:08x}\n", - this as usize, - object as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_shell_object_teardown_entry(this: *mut u8) { - let object_vtable = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this) as usize } - }; - let field_04 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x04)) as usize } - }; - let field_08 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x08)) as usize } - }; - let field_0c = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x0c)) as usize } - }; - let head_70 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x70)) as usize } - }; - let head_74 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x74)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell object teardown entry "); - let _ = write!( - &mut line, - "this=0x{:08x} vtable=0x{object_vtable:08x} field_04=0x{field_04:08x} field_08=0x{field_08:08x} field_0c=0x{field_0c:08x} head_70=0x{head_70:08x} head_74=0x{head_74:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_shell_object_teardown_return(this: *mut u8, result: i32) { - let head_70 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x70)) as usize } - }; - let head_74 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x74)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell object teardown return "); - let _ = write!( - &mut line, - "this=0x{:08x} result=0x{result:08x} head_70=0x{head_70:08x} head_74=0x{head_74:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_shell_object_range_remove_entry(this: *mut u8, arg1: u32, arg2: u32, arg3: u32) { - let head_74 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x74)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell object range-remove entry "); - let _ = write!( - &mut line, - "this=0x{:08x} arg1=0x{arg1:08x} arg2=0x{arg2:08x} arg3=0x{arg3:08x} head_74=0x{head_74:08x}\n", - this as usize, - ); - append_log_line(&line); - } - - fn log_shell_object_range_remove_return( - this: *mut u8, - arg1: u32, - arg2: u32, - arg3: u32, - result: i32, - ) { - let head_74 = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x74)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell object range-remove return "); - let _ = write!( - &mut line, - "this=0x{:08x} arg1=0x{arg1:08x} arg2=0x{arg2:08x} arg3=0x{arg3:08x} result=0x{result:08x} head_74=0x{head_74:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_shell_node_vcall_entry(this: *mut u8, record: *mut u8) { - let node_vtable = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this) as usize } - }; - let node_type = if this.is_null() { - 0 - } else { - unsafe { read_u32(this) as usize } - }; - let node_field_08 = if this.is_null() { - 0 - } else { - unsafe { read_u32(this.add(0x08)) as usize } - }; - let node_field_20 = if this.is_null() { - 0 - } else { - unsafe { read_u8(this.add(0x20)) as usize } - }; - let record_kind = if record.is_null() { - 0 - } else { - unsafe { read_u32(record) as usize } - }; - let record_x = if record.is_null() { - 0 - } else { - unsafe { read_u32(record.add(0x24)) as usize } - }; - let record_y = if record.is_null() { - 0 - } else { - unsafe { read_u32(record.add(0x28)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell node-vcall entry "); - let _ = write!( - &mut line, - "this=0x{:08x} node_vtable=0x{node_vtable:08x} node_type=0x{node_type:08x} node_08=0x{node_field_08:08x} node_20=0x{node_field_20:02x} record=0x{:08x} record_kind=0x{record_kind:08x} record_x=0x{record_x:08x} record_y=0x{record_y:08x}\n", - this as usize, record as usize, - ); - append_log_line(&line); - } - - fn log_shell_node_vcall_return(this: *mut u8, record: *mut u8, result: i32) { - let mut line = String::from("rrt-hook: auto load shell node-vcall return "); - let _ = write!( - &mut line, - "this=0x{:08x} record=0x{:08x} result=0x{result:08x}\n", - this as usize, - record as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_shell_remove_node_entry(this: *mut u8, node: *mut u8) { - let owner_head = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x70)) as usize } - }; - let owner_tail = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x74)) as usize } - }; - let node_vtable = if node.is_null() { - 0 - } else { - unsafe { read_ptr(node) as usize } - }; - let node_kind = if node.is_null() { - 0 - } else { - unsafe { read_u16(node.add(0x04)) as usize } - }; - let node_owner = if node.is_null() { - 0 - } else { - unsafe { read_u32(node.add(0x4b)) as usize } - }; - let node_prev = if node.is_null() { - 0 - } else { - unsafe { read_ptr(node.add(0x8a)) as usize } - }; - let node_next = if node.is_null() { - 0 - } else { - unsafe { read_ptr(node.add(0x8e)) as usize } - }; - let node_call0 = if node_vtable == 0 { - 0 - } else { - unsafe { read_ptr(node_vtable as *const u8) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell remove-node entry "); - let _ = write!( - &mut line, - "this=0x{:08x} owner_head=0x{owner_head:08x} owner_tail=0x{owner_tail:08x} node=0x{:08x} node_vtable=0x{node_vtable:08x} node_call0=0x{node_call0:08x} node_kind=0x{node_kind:04x} node_owner=0x{node_owner:08x} node_prev=0x{node_prev:08x} node_next=0x{node_next:08x}\n", - this as usize, node as usize, - ); - append_log_line(&line); - } - - fn log_shell_remove_node_return(this: *mut u8, node: *mut u8, result: i32) { - let owner_head = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x70)) as usize } - }; - let owner_tail = if this.is_null() { - 0 - } else { - unsafe { read_ptr(this.add(0x74)) as usize } - }; - let mut line = String::from("rrt-hook: auto load shell remove-node return "); - let _ = write!( - &mut line, - "this=0x{:08x} owner_head=0x{owner_head:08x} owner_tail=0x{owner_tail:08x} node=0x{:08x} result=0x{result:08x}\n", - this as usize, - node as usize, - result = result as u32, - ); - append_log_line(&line); - } - - fn log_mode2_teardown_entry(this: *mut u8) { - let mut line = String::from("rrt-hook: auto load mode2 teardown entry "); - let _ = write!(&mut line, "this=0x{:08x}\n", this as usize); - append_log_line(&line); - } - - fn log_mode2_teardown_return(this: *mut u8, result: i32) { - let mut line = String::from("rrt-hook: auto load mode2 teardown return "); - let _ = write!( - &mut line, - "this=0x{:08x} result=0x{result:08x}\n", - this as usize, - result = result as u32, - ); - append_log_line(&line); - } - - unsafe fn resolve_active_mode_ptr() -> *mut u8 { - let global_active_mode = unsafe { resolve_global_active_mode_ptr() }; - if !global_active_mode.is_null() { - return global_active_mode; - } - - let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; - if shell_state.is_null() { - return ptr::null_mut(); - } - - unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) } - } - - unsafe fn resolve_global_active_mode_ptr() -> *mut u8 { - unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } - } - - unsafe fn install_shell_state_service_hook() -> bool { - const STOLEN_LEN: usize = 6; - let target = SHELL_STATE_SERVICE_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - shell_state_service_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { SHELL_STATE_SERVICE_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_profile_startup_dispatch_hook() -> bool { - const STOLEN_LEN: usize = 16; - let target = PROFILE_STARTUP_DISPATCH_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - profile_startup_dispatch_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { PROFILE_STARTUP_DISPATCH_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_runtime_reset_hook() -> bool { - const STOLEN_LEN: usize = 16; - let target = RUNTIME_RESET_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - runtime_reset_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { RUNTIME_RESET_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_allocator_hook() -> bool { - const STOLEN_LEN: usize = 5; - let target = STARTUP_RUNTIME_ALLOC_THUNK_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour(target, STOLEN_LEN, allocator_detour as *const () as usize) - }; - if trampoline == 0 { - return false; - } - unsafe { ALLOCATOR_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_load_screen_scalar_hook() -> bool { - const STOLEN_LEN: usize = 10; - let target = LOAD_SCREEN_SET_SCALAR_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - load_screen_scalar_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { LOAD_SCREEN_SCALAR_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_load_screen_construct_hook() -> bool { - const STOLEN_LEN: usize = 6; - let target = LOAD_SCREEN_CONSTRUCT_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - load_screen_construct_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { LOAD_SCREEN_CONSTRUCT_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_load_screen_message_hook() -> bool { - const STOLEN_LEN: usize = 25; - let target = LOAD_SCREEN_HANDLE_MESSAGE_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - load_screen_message_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { LOAD_SCREEN_MESSAGE_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_runtime_prime_hook() -> bool { - const STOLEN_LEN: usize = 12; - let target = SHELL_RUNTIME_PRIME_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - runtime_prime_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { RUNTIME_PRIME_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_frame_cycle_hook() -> bool { - const STOLEN_LEN: usize = 6; - let target = SHELL_FRAME_CYCLE_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour(target, STOLEN_LEN, frame_cycle_detour as *const () as usize) - }; - if trampoline == 0 { - return false; - } - unsafe { FRAME_CYCLE_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_object_service_hook() -> bool { - const STOLEN_LEN: usize = 6; - let target = SHELL_OBJECT_SERVICE_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - object_service_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { OBJECT_SERVICE_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_child_service_hook() -> bool { - const STOLEN_LEN: usize = 6; - let target = SHELL_CHILD_SERVICE_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - child_service_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { CHILD_SERVICE_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_shell_publish_hook() -> bool { - const STOLEN_LEN: usize = 6; - let target = SHELL_PUBLISH_WINDOW_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - shell_publish_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { SHELL_PUBLISH_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_shell_unpublish_hook() -> bool { - const STOLEN_LEN: usize = 10; - let target = SHELL_UNPUBLISH_WINDOW_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - shell_unpublish_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { SHELL_UNPUBLISH_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_shell_object_teardown_hook() -> bool { - const STOLEN_LEN: usize = 7; - let target = SHELL_OBJECT_TEARDOWN_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - shell_object_teardown_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { SHELL_OBJECT_TEARDOWN_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_shell_object_range_remove_hook() -> bool { - const STOLEN_LEN: usize = 12; - let target = SHELL_OBJECT_RANGE_REMOVE_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - shell_object_range_remove_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_shell_node_vcall_hook() -> bool { - const STOLEN_LEN: usize = 21; - let target = SHELL_NODE_VCALL_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - shell_node_vcall_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { SHELL_NODE_VCALL_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_shell_remove_node_hook() -> bool { - const STOLEN_LEN: usize = 10; - let target = SHELL_REMOVE_NODE_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - shell_remove_node_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { SHELL_REMOVE_NODE_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_mode2_teardown_hook() -> bool { - const STOLEN_LEN: usize = 14; - let target = MODE2_TEARDOWN_ADDR as *mut u8; - let trampoline = unsafe { - install_rel32_detour( - target, - STOLEN_LEN, - mode2_teardown_detour as *const () as usize, - ) - }; - if trampoline == 0 { - return false; - } - unsafe { MODE2_TEARDOWN_TRAMPOLINE = trampoline }; - true - } - - unsafe fn install_rel32_detour(target: *mut u8, stolen_len: usize, detour: usize) -> usize { - let trampoline_size = stolen_len + 5; - let trampoline = unsafe { - VirtualAlloc( - ptr::null_mut(), - trampoline_size, - MEM_COMMIT | MEM_RESERVE, - PAGE_EXECUTE_READWRITE, - ) - } as *mut u8; - if trampoline.is_null() { - return 0; - } - - unsafe { ptr::copy_nonoverlapping(target, trampoline, stolen_len) }; - unsafe { write_rel32_jump(trampoline.add(stolen_len), target.add(stolen_len) as usize) }; - - let mut old_protect = 0_u32; - if unsafe { - VirtualProtect( - target.cast(), - stolen_len, - PAGE_EXECUTE_READWRITE, - &mut old_protect, - ) - } == 0 - { - return 0; - } - - unsafe { write_rel32_jump(target, detour) }; - for offset in 5..stolen_len { - unsafe { ptr::write(target.add(offset), 0x90) }; - } - let mut restore_protect = 0_u32; - let _ = - unsafe { VirtualProtect(target.cast(), stolen_len, old_protect, &mut restore_protect) }; - let _ = unsafe { FlushInstructionCache(GetCurrentProcess(), target.cast(), stolen_len) }; - trampoline as usize - } - - unsafe fn write_rel32_jump(location: *mut u8, destination: usize) { - unsafe { ptr::write(location, 0xE9) }; - let next_ip = unsafe { location.add(5) } as usize; - let relative = (destination as isize - next_ip as isize) as i32; - unsafe { ptr::write_unaligned(location.add(1).cast::(), relative) }; - } - - unsafe fn resolve_first_active_company() -> Option<*mut u8> { - let collection = COMPANY_COLLECTION_ADDR as *const u8; - let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) }; - if id_bound <= 0 { - return None; - } - - for entry_id in 1..=id_bound as usize { - if unsafe { indexed_collection_entry_id_is_live(collection, entry_id) } { - let company = - unsafe { indexed_collection_resolve_live_entry_by_id(collection, entry_id) }; - if !company.is_null() && unsafe { read_u8(company.add(COMPANY_ACTIVE_OFFSET)) != 0 } - { - return Some(company); - } - } - } - - None - } - - unsafe fn capture_company_collection_probe() -> Option { - let collection = COMPANY_COLLECTION_ADDR as *const u8; - let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) }; - if id_bound <= 0 { - return Some(IndexedCollectionProbe { - collection_addr: COMPANY_COLLECTION_ADDR, - flat_payload: unsafe { - read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 - }, - stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) }, - id_bound, - payload_ptr: unsafe { - read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize - }, - tombstone_ptr: unsafe { - read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize - }, - first_rows: Vec::new(), - }); - } - - let mut first_rows = Vec::new(); - let sample_bound = (id_bound as usize).min(8); - for entry_id in 1..=sample_bound { - let live = unsafe { indexed_collection_entry_id_is_live(collection, entry_id) }; - let resolved_ptr = unsafe { - indexed_collection_resolve_live_entry_by_id(collection, entry_id) as usize - }; - let active_flag = if resolved_ptr == 0 { - None - } else { - Some(unsafe { read_u8((resolved_ptr as *const u8).add(COMPANY_ACTIVE_OFFSET)) }) - }; - first_rows.push(IndexedCollectionProbeRow { - entry_id, - live, - resolved_ptr, - active_flag, - }); - } - - Some(IndexedCollectionProbe { - collection_addr: COMPANY_COLLECTION_ADDR, - flat_payload: unsafe { - read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 - }, - stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) }, - id_bound, - payload_ptr: unsafe { - read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize - }, - tombstone_ptr: unsafe { - read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize - }, - first_rows, - }) - } - - unsafe fn capture_cargo_collection_probe() -> Option { - let collection = CARGO_COLLECTION_ADDR as *const u8; - let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) }; - if id_bound <= 0 { - return Some(CargoCollectionProbe { - collection_addr: CARGO_COLLECTION_ADDR, - flat_payload: unsafe { - read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 - }, - stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) }, - id_bound, - payload_ptr: unsafe { - read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize - }, - tombstone_ptr: unsafe { - read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize - }, - live_entry_count: 0, - rows: Vec::new(), - }); - } - - let mut live_entry_count = 0_usize; - let mut rows = Vec::with_capacity(id_bound as usize); - for entry_id in 1..=id_bound as usize { - let live = unsafe { indexed_collection_entry_id_is_live(collection, entry_id) }; - let resolved_ptr = unsafe { - indexed_collection_resolve_live_entry_by_id(collection, entry_id) as usize - }; - if live && resolved_ptr != 0 { - live_entry_count += 1; - } - let stem = if resolved_ptr == 0 { - None - } else { - Some(unsafe { - read_c_string((resolved_ptr as *const u8).add(CARGO_STEM_OFFSET), 0x1e) - }) - }; - let route_style_byte = if resolved_ptr == 0 { - None - } else { - Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_ROUTE_STYLE_OFFSET)) }) - }; - let subtype_byte = if resolved_ptr == 0 { - None - } else { - Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_SUBTYPE_OFFSET)) }) - }; - let class_marker = if live { - Some(unsafe { - read_u32( - collection.add(CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET + entry_id * 4), - ) - }) - } else { - None - }; - rows.push(CargoCollectionProbeRow { - entry_id, - live, - resolved_ptr, - stem, - route_style_byte, - subtype_byte, - class_marker, - }); - } - - Some(CargoCollectionProbe { - collection_addr: CARGO_COLLECTION_ADDR, - flat_payload: unsafe { - read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 - }, - stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) }, - id_bound, - payload_ptr: unsafe { - read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize - }, - tombstone_ptr: unsafe { - read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize - }, - live_entry_count, - rows, - }) - } - - unsafe fn capture_probe_snapshot_from_company(company: *mut u8) -> FinanceSnapshot { - let scenario = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as *const u8; - let current_year = unsafe { read_u16(scenario.add(SCENARIO_CURRENT_YEAR_OFFSET)) }; - let founding_year = unsafe { read_u16(company.add(COMPANY_FOUNDING_YEAR_OFFSET)) }; - let last_bankruptcy_year = - unsafe { read_u16(company.add(COMPANY_LAST_BANKRUPTCY_YEAR_OFFSET)) }; - let outstanding_share_count = - unsafe { read_u32(company.add(COMPANY_OUTSTANDING_SHARES_OFFSET)) }; - let bonds = unsafe { capture_bonds(company, current_year) }; - let company_value = unsafe { read_u32(company.add(COMPANY_COMPANY_VALUE_OFFSET)) as i64 }; - let growth_setting = unsafe { - growth_setting_from_raw(read_u8( - scenario.add(SCENARIO_BUILDING_DENSITY_GROWTH_OFFSET), - )) - }; - - FinanceSnapshot { - policy: AnnualFinancePolicy { - annual_mode: 0x0c, - bankruptcy_allowed: unsafe { - read_u8(scenario.add(SCENARIO_BANKRUPTCY_TOGGLE_OFFSET)) == 0 - }, - bond_issuance_allowed: unsafe { - read_u8(scenario.add(SCENARIO_BOND_TOGGLE_OFFSET)) == 0 - }, - stock_actions_allowed: unsafe { - read_u8(scenario.add(SCENARIO_STOCK_TOGGLE_OFFSET)) == 0 - }, - dividends_allowed: unsafe { - read_u8(scenario.add(SCENARIO_DIVIDEND_TOGGLE_OFFSET)) == 0 - }, - growth_setting, - ..AnnualFinancePolicy::default() - }, - company: CompanyFinanceState { - active: unsafe { read_u8(company.add(COMPANY_ACTIVE_OFFSET)) != 0 }, - years_since_founding: year_delta(current_year, founding_year), - years_since_last_bankruptcy: year_delta(current_year, last_bankruptcy_year), - current_company_value: company_value, - outstanding_share_count, - city_connection_bonus_latch: unsafe { - read_u8(company.add(COMPANY_CITY_CONNECTION_LATCH_OFFSET)) != 0 - }, - linked_transit_service_latch: unsafe { - read_u8(company.add(COMPANY_LINKED_TRANSIT_LATCH_OFFSET)) != 0 - }, - chairman_buyback_factor: None, - bonds, - ..CompanyFinanceState::default() - }, - } - } - - unsafe fn capture_bonds(company: *mut u8, current_year: u16) -> Vec { - let bond_count = unsafe { read_u8(company.add(COMPANY_BOND_COUNT_OFFSET)) as usize }; - let table = unsafe { company.add(COMPANY_BOND_TABLE_OFFSET) }; - let mut bonds = Vec::with_capacity(bond_count); - - for index in 0..bond_count { - let slot = unsafe { table.add(index * 12) }; - let principal = unsafe { read_i32(slot) } as i64; - let maturity_year = unsafe { read_u32(slot.add(4)) }; - let coupon_rate = unsafe { read_f32(slot.add(8)) } as f64; - - bonds.push(BondPosition { - principal, - coupon_rate, - years_remaining: maturity_year - .saturating_sub(current_year as u32) - .min(u8::MAX as u32) as u8, - }); - } - - bonds - } - - fn growth_setting_from_raw(raw: u8) -> GrowthSetting { - match raw { - 1 => GrowthSetting::ExpansionBias, - 2 => GrowthSetting::DividendSuppressed, - _ => GrowthSetting::Neutral, - } - } - - fn year_delta(current_year: u16, past_year: u16) -> u8 { - current_year.saturating_sub(past_year).min(u8::MAX as u16) as u8 - } - - unsafe fn indexed_collection_entry_id_is_live(collection: *const u8, entry_id: usize) -> bool { - let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) }; - if entry_id == 0 || entry_id > id_bound.max(0) as usize { - return false; - } - - let tombstone_bits = - unsafe { read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) }; - if tombstone_bits.is_null() { - return true; - } - - let bit_index = entry_id as u32; - let word = unsafe { - ptr::read_unaligned(tombstone_bits.add((bit_index / 32) as usize).cast::()) - }; - (word & (1_u32 << (bit_index % 32))) == 0 - } - - unsafe fn indexed_collection_resolve_live_entry_by_id( - collection: *const u8, - entry_id: usize, - ) -> *mut u8 { - if !unsafe { indexed_collection_entry_id_is_live(collection, entry_id) } { - return ptr::null_mut(); - } - - let payload = unsafe { read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) }; - if payload.is_null() { - return ptr::null_mut(); - } - - let stride = unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) as usize }; - let flat = unsafe { read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 }; - - if flat { - unsafe { payload.add(stride * entry_id) } - } else { - unsafe { ptr::read_unaligned(payload.add(stride * entry_id).cast::<*mut u8>()) } - } - } - - unsafe fn read_u8(address: *const u8) -> u8 { - unsafe { ptr::read_unaligned(address) } - } - - unsafe fn read_u16(address: *const u8) -> u16 { - unsafe { ptr::read_unaligned(address.cast::()) } - } - - unsafe fn read_u32(address: *const u8) -> u32 { - unsafe { ptr::read_unaligned(address.cast::()) } - } - - unsafe fn read_i32(address: *const u8) -> i32 { - unsafe { ptr::read_unaligned(address.cast::()) } - } - - unsafe fn read_f32(address: *const u8) -> f32 { - unsafe { ptr::read_unaligned(address.cast::()) } - } - - unsafe fn read_ptr(address: *const u8) -> *mut u8 { - unsafe { ptr::read_unaligned(address.cast::<*mut u8>()) } - } - - unsafe fn read_c_string(address: *const u8, max_len: usize) -> String { - let mut len = 0; - while len < max_len { - let byte = unsafe { read_u8(address.add(len)) }; - if byte == 0 { - break; - } - len += 1; - } - String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(address, len) }).into_owned() - } - - unsafe fn load_direct_input8_create() -> Option { - if let Some(callback) = unsafe { REAL_DINPUT8_CREATE } { - return Some(callback); - } - - let mut system_directory = [0_u8; 260]; - let length = unsafe { - GetSystemDirectoryA(system_directory.as_mut_ptr(), system_directory.len() as u32) - }; - if length == 0 || length as usize >= system_directory.len() { - return None; - } - - let mut dll_path = system_directory[..length as usize].to_vec(); - dll_path.extend_from_slice(br"\dinput8.dll"); - dll_path.push(0); - - let module = unsafe { LoadLibraryA(dll_path.as_ptr().cast()) }; - if module == 0 { - return None; - } - - let symbol = unsafe { GetProcAddress(module, DIRECT_INPUT8_CREATE_NAME.as_ptr().cast()) }; - if symbol.is_null() { - return None; - } - - let callback: DirectInput8CreateFn = unsafe { mem::transmute(symbol) }; - unsafe { - REAL_DINPUT8_CREATE = Some(callback); - } - Some(callback) - } -} +mod windows; #[cfg(not(windows))] pub fn host_build_marker() -> &'static str { @@ -3131,6 +19,7 @@ pub fn host_build_marker() -> &'static str { #[cfg(test)] mod tests { use super::*; + use std::fs; use std::time::{SystemTime, UNIX_EPOCH}; #[test] diff --git a/crates/rrt-hook/src/windows/auto_load.rs b/crates/rrt-hook/src/windows/auto_load.rs new file mode 100644 index 0000000..7619c41 --- /dev/null +++ b/crates/rrt-hook/src/windows/auto_load.rs @@ -0,0 +1,377 @@ +use super::*; + +pub(super) fn maybe_install_auto_load_hook() { + let save_stem = match env::var("RRT_AUTO_LOAD_SAVE") { + Ok(value) if !value.trim().is_empty() => value, + _ => return, + }; + let _ = AUTO_LOAD_SAVE_STEM.set(save_stem); + if AUTO_LOAD_HOOK_INSTALLED.swap(true, Ordering::AcqRel) { + return; + } + + append_log_message(AUTO_LOAD_STARTED_MESSAGE); + AUTO_LOAD_THREAD_STARTED.store(true, Ordering::Release); + if unsafe { install_shell_state_service_hook() } { + append_log_message(AUTO_LOAD_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_profile_startup_dispatch_hook() } { + append_log_message(AUTO_LOAD_PROFILE_DISPATCH_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_runtime_reset_hook() } { + append_log_message(AUTO_LOAD_RUNTIME_RESET_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_allocator_hook() } { + append_log_message(AUTO_LOAD_ALLOCATOR_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_load_screen_scalar_hook() } { + append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_load_screen_construct_hook() } { + append_log_message(AUTO_LOAD_LOAD_SCREEN_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_load_screen_message_hook() } { + append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_runtime_prime_hook() } { + append_log_message(AUTO_LOAD_RUNTIME_PRIME_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_frame_cycle_hook() } { + append_log_message(AUTO_LOAD_FRAME_CYCLE_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_object_service_hook() } { + append_log_message(AUTO_LOAD_OBJECT_SERVICE_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_child_service_hook() } { + append_log_message(AUTO_LOAD_CHILD_SERVICE_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_shell_publish_hook() } { + append_log_message(AUTO_LOAD_PUBLISH_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_shell_unpublish_hook() } { + append_log_message(AUTO_LOAD_UNPUBLISH_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_shell_object_teardown_hook() } { + append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_shell_object_range_remove_hook() } { + append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_shell_remove_node_hook() } { + append_log_message(AUTO_LOAD_REMOVE_NODE_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_shell_node_vcall_hook() } { + append_log_message(AUTO_LOAD_NODE_VCALL_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + if unsafe { install_mode2_teardown_hook() } { + append_log_message(AUTO_LOAD_MODE2_TEARDOWN_HOOK_INSTALLED_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } +} + +pub(super) fn run_auto_load_worker() { + append_log_message(AUTO_LOAD_CALLING_MESSAGE); + let staged = unsafe { invoke_manual_load_branch() }; + if staged { + append_log_message(AUTO_LOAD_TRIGGERED_MESSAGE); + append_log_message(AUTO_LOAD_SUCCESS_MESSAGE); + } else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + } + AUTO_LOAD_IN_PROGRESS.store(false, Ordering::Release); +} + +pub(super) unsafe fn invoke_manual_load_branch() -> bool { + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + if shell_state.is_null() { + return false; + } + + let shell_transition_mode: ShellTransitionModeFn = + unsafe { mem::transmute(SHELL_TRANSITION_MODE_ADDR) }; + + AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.store(0, Ordering::Release); + AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.store(true, Ordering::Release); + append_log_message(AUTO_LOAD_OWNER_ENTRY_MESSAGE); + let _ = unsafe { shell_transition_mode(shell_state, SHELL_MODE_STARTUP_LOAD_DISPATCH, 0) }; + AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.store(false, Ordering::Release); + append_log_message(AUTO_LOAD_OWNER_RETURNED_MESSAGE); + log_post_transition_state(); + + true +} + +pub(super) unsafe fn stage_manual_load_request(save_stem: &str) -> bool { + if save_stem.is_empty() || save_stem.as_bytes().contains(&0) { + return false; + } + + let runtime_profile = unsafe { read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8) }; + if runtime_profile.is_null() { + return false; + } + + let path_seed = unsafe { runtime_profile.add(RUNTIME_PROFILE_MANUAL_LOAD_PATH_OFFSET) }; + if unsafe { write_c_string(path_seed, 260, save_stem.as_bytes()) }.is_none() { + return false; + } + + unsafe { + ptr::write_unaligned( + runtime_profile + .add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET) + .cast::(), + STARTUP_SELECTOR_SCENARIO_LOAD, + ) + }; + unsafe { + ptr::write_unaligned( + runtime_profile + .add(RUNTIME_PROFILE_PENDING_LOAD_BYTE_OFFSET) + .cast::(), + 0, + ) + }; + log_auto_load_launch_state(runtime_profile); + true +} + +pub(super) unsafe fn write_c_string( + destination: *mut u8, + capacity: usize, + bytes: &[u8], +) -> Option<()> { + if bytes.len() + 1 > capacity { + return None; + } + + unsafe { ptr::write_bytes(destination, 0, capacity) }; + unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), destination, bytes.len()) }; + Some(()) +} + +pub(super) unsafe fn runtime_saved_world_restore_gate_mask() -> u32 { + let mut mask = 0_u32; + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + if !shell_state.is_null() { + mask |= 0x1; + } + let shell_controller = unsafe { read_ptr(SHELL_CONTROLLER_PTR_ADDR as *const u8) }; + if !shell_controller.is_null() { + mask |= 0x2; + } + let active_mode = unsafe { resolve_active_mode_ptr() }; + if !active_mode.is_null() { + mask |= 0x4; + } + mask +} + +pub(super) unsafe fn current_mode_id() -> u32 { + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + if shell_state.is_null() { + return 0; + } + unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) } +} + +pub(super) fn auto_load_ready_polls() -> u32 { + env::var("RRT_AUTO_LOAD_READY_POLLS") + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(AUTO_LOAD_READY_POLLS) +} + +pub(super) fn auto_load_defer_polls() -> u32 { + env::var("RRT_AUTO_LOAD_DEFER_POLLS") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(AUTO_LOAD_DEFER_POLLS) +} + +pub(super) fn maybe_service_auto_load_on_main_thread() { + if !AUTO_LOAD_HOOK_INSTALLED.load(Ordering::Acquire) + || AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) + || AUTO_LOAD_IN_PROGRESS.load(Ordering::Acquire) + { + return; + } + + let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() }; + let last_gate_mask = AUTO_LOAD_LAST_GATE_MASK.swap(gate_mask, Ordering::AcqRel); + if gate_mask != last_gate_mask { + log_auto_load_gate_mask(gate_mask); + } + + let mode_id = unsafe { current_mode_id() }; + let ready = gate_mask == 0x7 && mode_id == 2; + let ready_count = if ready { + AUTO_LOAD_READY_COUNT.fetch_add(1, Ordering::AcqRel) + 1 + } else { + AUTO_LOAD_READY_COUNT.store(0, Ordering::Release); + AUTO_LOAD_DEFERRED.store(false, Ordering::Release); + 0 + }; + if ready { + append_log_message(AUTO_LOAD_READY_COUNT_MESSAGE); + log_auto_load_ready_count(ready_count, gate_mask, mode_id); + } + + let ready_polls = auto_load_ready_polls(); + if ready_count < ready_polls { + return; + } + + if !AUTO_LOAD_DEFERRED.load(Ordering::Acquire) { + AUTO_LOAD_DEFERRED.store(true, Ordering::Release); + append_log_message(AUTO_LOAD_READY_MESSAGE); + append_log_message(AUTO_LOAD_DEFERRED_MESSAGE); + return; + } + + if ready_count < ready_polls.saturating_add(auto_load_defer_polls()) { + return; + } + + if AUTO_LOAD_ATTEMPTED.swap(true, Ordering::AcqRel) { + return; + } + + if !AUTO_LOAD_TRANSITION_ARMED.load(Ordering::Acquire) { + let Some(save_stem) = AUTO_LOAD_SAVE_STEM.get() else { + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release); + return; + }; + if unsafe { stage_manual_load_request(save_stem) } { + AUTO_LOAD_TRANSITION_ARMED.store(true, Ordering::Release); + AUTO_LOAD_ARMED_TICK_COUNT.store(0, Ordering::Release); + append_log_message(AUTO_LOAD_STAGED_MESSAGE); + log_auto_load_armed_tick(); + AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release); + AUTO_LOAD_IN_PROGRESS.store(true, Ordering::Release); + append_log_message(AUTO_LOAD_ARMED_TICK_MESSAGE); + append_log_message(AUTO_LOAD_READY_MESSAGE); + run_auto_load_worker(); + return; + } + append_log_message(AUTO_LOAD_FAILURE_MESSAGE); + AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release); + return; + } + + AUTO_LOAD_IN_PROGRESS.store(true, Ordering::Release); + append_log_message(AUTO_LOAD_READY_MESSAGE); + run_auto_load_worker(); +} + +pub(super) fn log_auto_load_gate_mask(mask: u32) { + let mut line = String::from("rrt-hook: auto load gate mask "); + let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + let mode_id = if shell_state.is_null() { + 0 + } else { + unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize } + }; + let field_active_mode_object = if shell_state.is_null() { + 0 + } else { + unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } + }; + let startup_selector = unsafe { + let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8); + if runtime_profile.is_null() { + 0 + } else { + read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize + } + }; + let _ = write!( + &mut line, + "0x{mask:01x} shell_state={} shell_controller={} active_mode={} global_active_mode=0x{global_active_mode:08x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x} field_active_mode_object=0x{field_active_mode_object:08x}\n", + (mask & 0x1) != 0, + (mask & 0x2) != 0, + (mask & 0x4) != 0, + ); + append_log_line(&line); +} + +pub(super) fn log_auto_load_armed_tick() { + let tick_count = AUTO_LOAD_ARMED_TICK_COUNT.fetch_add(1, Ordering::AcqRel) + 1; + let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() }; + let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + let mode_id = if shell_state.is_null() { + 0 + } else { + unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize } + }; + let startup_selector = unsafe { + let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8); + if runtime_profile.is_null() { + 0 + } else { + read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize + } + }; + let mut line = String::from("rrt-hook: auto load armed tick "); + let _ = write!( + &mut line, + "count=0x{tick_count:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x} global_active_mode=0x{global_active_mode:08x}\n", + ); + append_log_line(&line); +} + +pub(super) fn log_auto_load_ready_count(count: u32, gate_mask: u32, mode_id: u32) { + let startup_selector = unsafe { + let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8); + if runtime_profile.is_null() { + 0 + } else { + read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize + } + }; + let mut line = String::from("rrt-hook: auto load ready count "); + let _ = write!( + &mut line, + "count=0x{count:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x}\n", + ); + append_log_line(&line); +} diff --git a/crates/rrt-hook/src/windows/capture.rs b/crates/rrt-hook/src/windows/capture.rs new file mode 100644 index 0000000..e7f8831 --- /dev/null +++ b/crates/rrt-hook/src/windows/capture.rs @@ -0,0 +1,333 @@ +use super::*; + +pub(super) fn maybe_emit_finance_template_bundle() { + if env::var_os("RRT_WRITE_FINANCE_TEMPLATE").is_none() { + return; + } + if FINANCE_TEMPLATE_EMITTED.swap(true, Ordering::AcqRel) { + return; + } + + let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let _ = write_finance_snapshot_bundle(&base_dir, "attach_template", &sample_finance_snapshot()); +} + +pub(super) fn maybe_start_finance_capture_thread() { + if env::var_os("RRT_WRITE_FINANCE_CAPTURE").is_none() { + return; + } + if FINANCE_CAPTURE_STARTED.swap(true, Ordering::AcqRel) { + return; + } + + append_log_message(FINANCE_CAPTURE_STARTED_MESSAGE); + let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let _ = thread::Builder::new() + .name("rrt-finance-capture".to_string()) + .spawn(move || { + for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS { + if !FINANCE_COLLECTION_PROBE_WRITTEN.load(Ordering::Acquire) { + if let Some(probe) = unsafe { capture_company_collection_probe() } { + if write_indexed_collection_probe(&base_dir, "attach_probe", &probe).is_ok() + { + FINANCE_COLLECTION_PROBE_WRITTEN.store(true, Ordering::Release); + append_log_message(FINANCE_CAPTURE_PROBE_DUMP_WRITTEN_MESSAGE); + } + } + } + if let Some(snapshot) = unsafe { try_capture_probe_snapshot() } { + append_log_message(FINANCE_CAPTURE_COMPANY_RESOLVED_MESSAGE); + if write_finance_snapshot_only(&base_dir, "attach_probe", &snapshot).is_ok() { + append_log_message(FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE); + return; + } + } + thread::sleep(CAPTURE_POLL_INTERVAL); + } + + append_log_message(FINANCE_CAPTURE_TIMEOUT_MESSAGE); + }); +} + +pub(super) fn maybe_start_cargo_capture_thread() { + if env::var_os("RRT_WRITE_CARGO_CAPTURE").is_none() { + return; + } + if CARGO_CAPTURE_STARTED.swap(true, Ordering::AcqRel) { + return; + } + + append_log_message(CARGO_CAPTURE_STARTED_MESSAGE); + let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let _ = thread::Builder::new() + .name("rrt-cargo-capture".to_string()) + .spawn(move || { + let mut last_probe: Option = None; + for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS { + if let Some(probe) = unsafe { capture_cargo_collection_probe() } { + last_probe = Some(probe.clone()); + if probe.live_entry_count > 0 + && write_cargo_collection_probe(&base_dir, "attach_probe", &probe).is_ok() + { + append_log_message(CARGO_CAPTURE_WRITTEN_MESSAGE); + return; + } + } + thread::sleep(CAPTURE_POLL_INTERVAL); + } + + if let Some(probe) = last_probe { + let _ = write_cargo_collection_probe(&base_dir, "attach_probe_timeout", &probe); + } + append_log_message(CARGO_CAPTURE_TIMEOUT_MESSAGE); + }); +} + +pub(super) unsafe fn try_capture_probe_snapshot() -> Option { + append_log_message(FINANCE_CAPTURE_SCAN_MESSAGE); + let company = unsafe { resolve_first_active_company()? }; + Some(unsafe { capture_probe_snapshot_from_company(company) }) +} + +pub(super) unsafe fn resolve_first_active_company() -> Option<*mut u8> { + let collection = COMPANY_COLLECTION_ADDR as *const u8; + let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) }; + if id_bound <= 0 { + return None; + } + + for entry_id in 1..=id_bound as usize { + if unsafe { indexed_collection_entry_id_is_live(collection, entry_id) } { + let company = + unsafe { indexed_collection_resolve_live_entry_by_id(collection, entry_id) }; + if !company.is_null() && unsafe { read_u8(company.add(COMPANY_ACTIVE_OFFSET)) != 0 } { + return Some(company); + } + } + } + + None +} + +pub(super) unsafe fn capture_company_collection_probe() -> Option { + let collection = COMPANY_COLLECTION_ADDR as *const u8; + let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) }; + if id_bound <= 0 { + return Some(IndexedCollectionProbe { + collection_addr: COMPANY_COLLECTION_ADDR, + flat_payload: unsafe { + read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 + }, + stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) }, + id_bound, + payload_ptr: unsafe { + read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize + }, + tombstone_ptr: unsafe { + read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize + }, + first_rows: Vec::new(), + }); + } + + let mut first_rows = Vec::new(); + let sample_bound = (id_bound as usize).min(8); + for entry_id in 1..=sample_bound { + let live = unsafe { indexed_collection_entry_id_is_live(collection, entry_id) }; + let resolved_ptr = + unsafe { indexed_collection_resolve_live_entry_by_id(collection, entry_id) as usize }; + let active_flag = if resolved_ptr == 0 { + None + } else { + Some(unsafe { read_u8((resolved_ptr as *const u8).add(COMPANY_ACTIVE_OFFSET)) }) + }; + first_rows.push(IndexedCollectionProbeRow { + entry_id, + live, + resolved_ptr, + active_flag, + }); + } + + Some(IndexedCollectionProbe { + collection_addr: COMPANY_COLLECTION_ADDR, + flat_payload: unsafe { read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 }, + stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) }, + id_bound, + payload_ptr: unsafe { + read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize + }, + tombstone_ptr: unsafe { + read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize + }, + first_rows, + }) +} + +pub(super) unsafe fn capture_cargo_collection_probe() -> Option { + let collection = CARGO_COLLECTION_ADDR as *const u8; + let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) }; + if id_bound <= 0 { + return Some(CargoCollectionProbe { + collection_addr: CARGO_COLLECTION_ADDR, + flat_payload: unsafe { + read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 + }, + stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) }, + id_bound, + payload_ptr: unsafe { + read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize + }, + tombstone_ptr: unsafe { + read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize + }, + live_entry_count: 0, + rows: Vec::new(), + }); + } + + let mut live_entry_count = 0_usize; + let mut rows = Vec::with_capacity(id_bound as usize); + for entry_id in 1..=id_bound as usize { + let live = unsafe { indexed_collection_entry_id_is_live(collection, entry_id) }; + let resolved_ptr = + unsafe { indexed_collection_resolve_live_entry_by_id(collection, entry_id) as usize }; + if live && resolved_ptr != 0 { + live_entry_count += 1; + } + let stem = if resolved_ptr == 0 { + None + } else { + Some(unsafe { read_c_string((resolved_ptr as *const u8).add(CARGO_STEM_OFFSET), 0x1e) }) + }; + let route_style_byte = if resolved_ptr == 0 { + None + } else { + Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_ROUTE_STYLE_OFFSET)) }) + }; + let subtype_byte = if resolved_ptr == 0 { + None + } else { + Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_SUBTYPE_OFFSET)) }) + }; + let class_marker = if live { + Some(unsafe { + read_u32(collection.add(CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET + entry_id * 4)) + }) + } else { + None + }; + rows.push(CargoCollectionProbeRow { + entry_id, + live, + resolved_ptr, + stem, + route_style_byte, + subtype_byte, + class_marker, + }); + } + + Some(CargoCollectionProbe { + collection_addr: CARGO_COLLECTION_ADDR, + flat_payload: unsafe { read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 }, + stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) }, + id_bound, + payload_ptr: unsafe { + read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize + }, + tombstone_ptr: unsafe { + read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize + }, + live_entry_count, + rows, + }) +} + +pub(super) unsafe fn capture_probe_snapshot_from_company(company: *mut u8) -> FinanceSnapshot { + let scenario = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as *const u8; + let current_year = unsafe { read_u16(scenario.add(SCENARIO_CURRENT_YEAR_OFFSET)) }; + let founding_year = unsafe { read_u16(company.add(COMPANY_FOUNDING_YEAR_OFFSET)) }; + let last_bankruptcy_year = + unsafe { read_u16(company.add(COMPANY_LAST_BANKRUPTCY_YEAR_OFFSET)) }; + let outstanding_share_count = + unsafe { read_u32(company.add(COMPANY_OUTSTANDING_SHARES_OFFSET)) }; + let bonds = unsafe { capture_bonds(company, current_year) }; + let company_value = unsafe { read_u32(company.add(COMPANY_COMPANY_VALUE_OFFSET)) as i64 }; + let growth_setting = unsafe { + growth_setting_from_raw(read_u8( + scenario.add(SCENARIO_BUILDING_DENSITY_GROWTH_OFFSET), + )) + }; + + FinanceSnapshot { + policy: AnnualFinancePolicy { + annual_mode: 0x0c, + bankruptcy_allowed: unsafe { + read_u8(scenario.add(SCENARIO_BANKRUPTCY_TOGGLE_OFFSET)) == 0 + }, + bond_issuance_allowed: unsafe { + read_u8(scenario.add(SCENARIO_BOND_TOGGLE_OFFSET)) == 0 + }, + stock_actions_allowed: unsafe { + read_u8(scenario.add(SCENARIO_STOCK_TOGGLE_OFFSET)) == 0 + }, + dividends_allowed: unsafe { + read_u8(scenario.add(SCENARIO_DIVIDEND_TOGGLE_OFFSET)) == 0 + }, + growth_setting, + ..AnnualFinancePolicy::default() + }, + company: CompanyFinanceState { + active: unsafe { read_u8(company.add(COMPANY_ACTIVE_OFFSET)) != 0 }, + years_since_founding: year_delta(current_year, founding_year), + years_since_last_bankruptcy: year_delta(current_year, last_bankruptcy_year), + current_company_value: company_value, + outstanding_share_count, + city_connection_bonus_latch: unsafe { + read_u8(company.add(COMPANY_CITY_CONNECTION_LATCH_OFFSET)) != 0 + }, + linked_transit_service_latch: unsafe { + read_u8(company.add(COMPANY_LINKED_TRANSIT_LATCH_OFFSET)) != 0 + }, + chairman_buyback_factor: None, + bonds, + ..CompanyFinanceState::default() + }, + } +} + +pub(super) unsafe fn capture_bonds(company: *mut u8, current_year: u16) -> Vec { + let bond_count = unsafe { read_u8(company.add(COMPANY_BOND_COUNT_OFFSET)) as usize }; + let table = unsafe { company.add(COMPANY_BOND_TABLE_OFFSET) }; + let mut bonds = Vec::with_capacity(bond_count); + + for index in 0..bond_count { + let slot = unsafe { table.add(index * 12) }; + let principal = unsafe { read_i32(slot) } as i64; + let maturity_year = unsafe { read_u32(slot.add(4)) }; + let coupon_rate = unsafe { read_f32(slot.add(8)) } as f64; + + bonds.push(BondPosition { + principal, + coupon_rate, + years_remaining: maturity_year + .saturating_sub(current_year as u32) + .min(u8::MAX as u32) as u8, + }); + } + + bonds +} + +pub(super) fn growth_setting_from_raw(raw: u8) -> GrowthSetting { + match raw { + 1 => GrowthSetting::ExpansionBias, + 2 => GrowthSetting::DividendSuppressed, + _ => GrowthSetting::Neutral, + } +} + +pub(super) fn year_delta(current_year: u16, past_year: u16) -> u8 { + current_year.saturating_sub(past_year).min(u8::MAX as u16) as u8 +} diff --git a/crates/rrt-hook/src/windows/constants.rs b/crates/rrt-hook/src/windows/constants.rs new file mode 100644 index 0000000..f80875a --- /dev/null +++ b/crates/rrt-hook/src/windows/constants.rs @@ -0,0 +1,221 @@ +use super::*; + +pub(super) const DLL_PROCESS_ATTACH: u32 = 1; +pub(super) const E_FAIL: i32 = 0x8000_4005_u32 as i32; +pub(super) const FILE_APPEND_DATA: u32 = 0x0000_0004; +pub(super) const FILE_SHARE_READ: u32 = 0x0000_0001; +pub(super) const OPEN_ALWAYS: u32 = 4; +pub(super) const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080; +pub(super) const INVALID_HANDLE_VALUE: isize = -1; +pub(super) const FILE_END: u32 = 2; + +pub(super) static LOG_PATH: &[u8] = b"rrt_hook_attach.log\0"; +pub(super) static ATTACH_MESSAGE: &[u8] = b"rrt-hook: process attach\n"; +pub(super) static FINANCE_CAPTURE_STARTED_MESSAGE: &[u8] = + b"rrt-hook: finance capture thread started\n"; +pub(super) static FINANCE_CAPTURE_SCAN_MESSAGE: &[u8] = + b"rrt-hook: finance capture raw collection scan\n"; +pub(super) static FINANCE_CAPTURE_PROBE_DUMP_WRITTEN_MESSAGE: &[u8] = + b"rrt-hook: finance collection probe written\n"; +pub(super) static FINANCE_CAPTURE_COMPANY_RESOLVED_MESSAGE: &[u8] = + b"rrt-hook: finance capture company resolved\n"; +pub(super) static FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE: &[u8] = + b"rrt-hook: finance probe snapshot written\n"; +pub(super) static FINANCE_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: finance capture timed out\n"; +pub(super) static CARGO_CAPTURE_STARTED_MESSAGE: &[u8] = + b"rrt-hook: cargo capture thread started\n"; +pub(super) static CARGO_CAPTURE_WRITTEN_MESSAGE: &[u8] = + b"rrt-hook: cargo collection probe written\n"; +pub(super) static CARGO_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: cargo capture timed out\n"; +pub(super) static AUTO_LOAD_STARTED_MESSAGE: &[u8] = b"rrt-hook: auto load hook armed\n"; +pub(super) static AUTO_LOAD_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell-state hook installed\n"; +pub(super) static AUTO_LOAD_PROFILE_DISPATCH_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load startup-dispatch hook installed\n"; +pub(super) static AUTO_LOAD_RUNTIME_RESET_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load runtime-reset hook installed\n"; +pub(super) static AUTO_LOAD_ALLOCATOR_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load allocator hook installed\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_SCALAR_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen scalar hook installed\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen hook installed\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_MESSAGE_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen message hook installed\n"; +pub(super) static AUTO_LOAD_RUNTIME_PRIME_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell runtime-prime hook installed\n"; +pub(super) static AUTO_LOAD_FRAME_CYCLE_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell frame-cycle hook installed\n"; +pub(super) static AUTO_LOAD_OBJECT_SERVICE_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object-service hook installed\n"; +pub(super) static AUTO_LOAD_CHILD_SERVICE_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell child-service hook installed\n"; +pub(super) static AUTO_LOAD_PUBLISH_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell publish hook installed\n"; +pub(super) static AUTO_LOAD_UNPUBLISH_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell unpublish hook installed\n"; +pub(super) static AUTO_LOAD_OBJECT_TEARDOWN_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object teardown hook installed\n"; +pub(super) static AUTO_LOAD_OBJECT_RANGE_REMOVE_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object range-remove hook installed\n"; +pub(super) static AUTO_LOAD_REMOVE_NODE_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell remove-node hook installed\n"; +pub(super) static AUTO_LOAD_NODE_VCALL_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell node-vcall hook installed\n"; +pub(super) static AUTO_LOAD_MODE2_TEARDOWN_HOOK_INSTALLED_MESSAGE: &[u8] = + b"rrt-hook: auto load mode2 teardown hook installed\n"; +pub(super) static AUTO_LOAD_READY_MESSAGE: &[u8] = b"rrt-hook: auto load ready gate passed\n"; +pub(super) static AUTO_LOAD_DEFERRED_MESSAGE: &[u8] = + b"rrt-hook: auto load restore deferred to later service turn\n"; +pub(super) static AUTO_LOAD_CALLING_MESSAGE: &[u8] = b"rrt-hook: auto load restore calling\n"; +pub(super) static AUTO_LOAD_STAGED_MESSAGE: &[u8] = + b"rrt-hook: auto load restore staged for later transition\n"; +pub(super) static AUTO_LOAD_READY_COUNT_MESSAGE: &[u8] = b"rrt-hook: auto load ready count\n"; +pub(super) static AUTO_LOAD_ARMED_TICK_MESSAGE: &[u8] = + b"rrt-hook: auto load armed transition tick\n"; +pub(super) static AUTO_LOAD_OWNER_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell transition entering\n"; +pub(super) static AUTO_LOAD_OWNER_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell transition returned\n"; +pub(super) static AUTO_LOAD_PROFILE_DISPATCH_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load startup dispatch entering\n"; +pub(super) static AUTO_LOAD_PROFILE_DISPATCH_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load startup dispatch returned\n"; +pub(super) static AUTO_LOAD_RUNTIME_RESET_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load runtime reset entering\n"; +pub(super) static AUTO_LOAD_RUNTIME_RESET_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load runtime reset returned\n"; +pub(super) static AUTO_LOAD_ALLOCATOR_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load allocator entering\n"; +pub(super) static AUTO_LOAD_ALLOCATOR_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load allocator returned\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_SCALAR_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen scalar entering\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_SCALAR_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen scalar returned\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen construct entering\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen construct returned\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_MESSAGE_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen message entering\n"; +pub(super) static AUTO_LOAD_LOAD_SCREEN_MESSAGE_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load load-screen message returned\n"; +pub(super) static AUTO_LOAD_RUNTIME_PRIME_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell runtime-prime entering\n"; +pub(super) static AUTO_LOAD_RUNTIME_PRIME_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell runtime-prime returned\n"; +pub(super) static AUTO_LOAD_FRAME_CYCLE_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell frame-cycle entering\n"; +pub(super) static AUTO_LOAD_FRAME_CYCLE_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell frame-cycle returned\n"; +pub(super) static AUTO_LOAD_OBJECT_SERVICE_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object-service entering\n"; +pub(super) static AUTO_LOAD_OBJECT_SERVICE_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object-service returned\n"; +pub(super) static AUTO_LOAD_CHILD_SERVICE_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell child-service entering\n"; +pub(super) static AUTO_LOAD_CHILD_SERVICE_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell child-service returned\n"; +pub(super) static AUTO_LOAD_PUBLISH_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell publish entering\n"; +pub(super) static AUTO_LOAD_PUBLISH_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell publish returned\n"; +pub(super) static AUTO_LOAD_UNPUBLISH_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell unpublish entering\n"; +pub(super) static AUTO_LOAD_UNPUBLISH_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell unpublish returned\n"; +pub(super) static AUTO_LOAD_OBJECT_TEARDOWN_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object teardown entering\n"; +pub(super) static AUTO_LOAD_OBJECT_TEARDOWN_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object teardown returned\n"; +pub(super) static AUTO_LOAD_OBJECT_RANGE_REMOVE_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object range-remove entering\n"; +pub(super) static AUTO_LOAD_OBJECT_RANGE_REMOVE_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell object range-remove returned\n"; +pub(super) static AUTO_LOAD_REMOVE_NODE_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell remove-node entering\n"; +pub(super) static AUTO_LOAD_REMOVE_NODE_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell remove-node returned\n"; +pub(super) static AUTO_LOAD_NODE_VCALL_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load shell node-vcall entering\n"; +pub(super) static AUTO_LOAD_NODE_VCALL_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load shell node-vcall returned\n"; +pub(super) static AUTO_LOAD_MODE2_TEARDOWN_ENTRY_MESSAGE: &[u8] = + b"rrt-hook: auto load mode2 teardown entering\n"; +pub(super) static AUTO_LOAD_MODE2_TEARDOWN_RETURNED_MESSAGE: &[u8] = + b"rrt-hook: auto load mode2 teardown returned\n"; +pub(super) static AUTO_LOAD_TRIGGERED_MESSAGE: &[u8] = b"rrt-hook: auto load restore invoked\n"; +pub(super) static AUTO_LOAD_SUCCESS_MESSAGE: &[u8] = + b"rrt-hook: auto load request reported success\n"; +pub(super) static AUTO_LOAD_FAILURE_MESSAGE: &[u8] = + b"rrt-hook: auto load request reported failure\n"; +pub(super) static DEBUG_MESSAGE: &[u8] = b"rrt-hook: DllMain process attach\0"; +pub(super) static DIRECT_INPUT8_CREATE_NAME: &[u8] = b"DirectInput8Create\0"; + +pub(super) const COMPANY_COLLECTION_ADDR: usize = 0x0062be10; +pub(super) const CARGO_COLLECTION_ADDR: usize = 0x0062ba8c; +pub(super) const SHELL_CONTROLLER_PTR_ADDR: usize = 0x006d4024; +pub(super) const SHELL_STATE_PTR_ADDR: usize = 0x006cec74; +pub(super) const ACTIVE_MODE_PTR_ADDR: usize = 0x006cec78; +pub(super) const SHELL_STATE_SERVICE_ADDR: usize = 0x00482160; +pub(super) const SHELL_TRANSITION_MODE_ADDR: usize = 0x00482ec0; +pub(super) const PROFILE_STARTUP_DISPATCH_ADDR: usize = 0x00438890; +pub(super) const RUNTIME_RESET_ADDR: usize = 0x004336d0; +pub(super) const STARTUP_RUNTIME_ALLOC_THUNK_ADDR: usize = 0x0053b070; +pub(super) const LOAD_SCREEN_SET_SCALAR_ADDR: usize = 0x004ea710; +pub(super) const LOAD_SCREEN_CONSTRUCT_ADDR: usize = 0x004ea620; +pub(super) const LOAD_SCREEN_HANDLE_MESSAGE_ADDR: usize = 0x004e3a80; +pub(super) const SHELL_RUNTIME_PRIME_ADDR: usize = 0x00538b60; +pub(super) const SHELL_FRAME_CYCLE_ADDR: usize = 0x00520620; +pub(super) const SHELL_OBJECT_SERVICE_ADDR: usize = 0x0053fda0; +pub(super) const SHELL_CHILD_SERVICE_ADDR: usize = 0x005595d0; +pub(super) const SHELL_PUBLISH_WINDOW_ADDR: usize = 0x00538e50; +pub(super) const SHELL_UNPUBLISH_WINDOW_ADDR: usize = 0x005389c0; +pub(super) const SHELL_OBJECT_TEARDOWN_ADDR: usize = 0x005400c0; +pub(super) const SHELL_OBJECT_RANGE_REMOVE_ADDR: usize = 0x0053fe00; +pub(super) const SHELL_REMOVE_NODE_ADDR: usize = 0x0053f860; +pub(super) const SHELL_NODE_VCALL_ADDR: usize = 0x00540910; +pub(super) const MODE2_TEARDOWN_ADDR: usize = 0x00502720; +pub(super) const SHELL_STATE_ACTIVE_MODE_OFFSET: usize = 0x08; +pub(super) const SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET: usize = 0x0c; +pub(super) const RUNTIME_PROFILE_PTR_ADDR: usize = 0x006cec7c; +pub(super) const RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET: usize = 0x01; +pub(super) const RUNTIME_PROFILE_MANUAL_LOAD_PATH_OFFSET: usize = 0x11; +pub(super) const RUNTIME_PROFILE_PENDING_LOAD_BYTE_OFFSET: usize = 0x97; +pub(super) const SHELL_MODE_STARTUP_LOAD_DISPATCH: u32 = 1; +pub(super) const STARTUP_SELECTOR_SCENARIO_LOAD: u8 = 3; +pub(super) const STARTUP_RUNTIME_OBJECT_SIZE: u32 = 0x00046c40; +pub(super) const INDEXED_COLLECTION_FLAT_FLAG_OFFSET: usize = 0x04; +pub(super) const INDEXED_COLLECTION_STRIDE_OFFSET: usize = 0x08; +pub(super) const INDEXED_COLLECTION_ID_BOUND_OFFSET: usize = 0x14; +pub(super) const INDEXED_COLLECTION_PAYLOAD_OFFSET: usize = 0x30; +pub(super) const INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET: usize = 0x34; +pub(super) const CARGO_STEM_OFFSET: usize = 0x04; +pub(super) const CARGO_SUBTYPE_OFFSET: usize = 0x32; +pub(super) const CARGO_ROUTE_STYLE_OFFSET: usize = 0x46; +pub(super) const CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET: usize = 0x9a; +pub(super) const COMPANY_ACTIVE_OFFSET: usize = 0x3f; +pub(super) const COMPANY_OUTSTANDING_SHARES_OFFSET: usize = 0x47; +pub(super) const COMPANY_COMPANY_VALUE_OFFSET: usize = 0x57; +pub(super) const COMPANY_BOND_COUNT_OFFSET: usize = 0x5b; +pub(super) const COMPANY_BOND_TABLE_OFFSET: usize = 0x5f; +pub(super) const COMPANY_FOUNDING_YEAR_OFFSET: usize = 0x157; +pub(super) const COMPANY_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163; +pub(super) const COMPANY_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18; +pub(super) const COMPANY_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56; + +pub(super) const SCENARIO_CURRENT_YEAR_OFFSET: usize = 0x0d; +pub(super) const SCENARIO_BUILDING_DENSITY_GROWTH_OFFSET: usize = 0x4c7c; +pub(super) const SCENARIO_BANKRUPTCY_TOGGLE_OFFSET: usize = 0x4a8f; +pub(super) const SCENARIO_BOND_TOGGLE_OFFSET: usize = 0x4a8b; +pub(super) const SCENARIO_STOCK_TOGGLE_OFFSET: usize = 0x4a87; +pub(super) const SCENARIO_DIVIDEND_TOGGLE_OFFSET: usize = 0x4a93; + +pub(super) const MAX_CAPTURE_POLL_ATTEMPTS: usize = 120; +pub(super) const CAPTURE_POLL_INTERVAL: Duration = Duration::from_secs(1); +pub(super) const AUTO_LOAD_READY_POLLS: u32 = 1; +pub(super) const AUTO_LOAD_DEFER_POLLS: u32 = 0; +pub(super) const MEM_COMMIT: u32 = 0x1000; +pub(super) const MEM_RESERVE: u32 = 0x2000; +pub(super) const PAGE_EXECUTE_READWRITE: u32 = 0x40; diff --git a/crates/rrt-hook/src/windows/detours.rs b/crates/rrt-hook/src/windows/detours.rs new file mode 100644 index 0000000..3c3ae43 --- /dev/null +++ b/crates/rrt-hook/src/windows/detours.rs @@ -0,0 +1,292 @@ +use super::*; + +pub(super) unsafe extern "fastcall" fn shell_state_service_detour( + this: *mut u8, + _edx: usize, +) -> i32 { + log_shell_state_service_entry(this); + let trampoline: ShellStateServiceFn = unsafe { mem::transmute(SHELL_STATE_SERVICE_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + log_shell_state_service_return(this, result); + log_post_transition_service_state(); + maybe_service_auto_load_on_main_thread(); + result +} + +pub(super) unsafe extern "fastcall" fn profile_startup_dispatch_detour( + this: *mut u8, + _edx: usize, + arg1: u32, + arg2: u32, +) -> i32 { + log_profile_startup_dispatch_entry(this, arg1, arg2); + append_log_message(AUTO_LOAD_PROFILE_DISPATCH_ENTRY_MESSAGE); + let trampoline: ProfileStartupDispatchFn = + unsafe { mem::transmute(PROFILE_STARTUP_DISPATCH_TRAMPOLINE) }; + let result = unsafe { trampoline(this, arg1, arg2) }; + append_log_message(AUTO_LOAD_PROFILE_DISPATCH_RETURNED_MESSAGE); + log_profile_startup_dispatch_return(this, arg1, arg2, result); + result +} + +pub(super) unsafe extern "fastcall" fn runtime_reset_detour(this: *mut u8, _edx: usize) -> *mut u8 { + append_log_message(AUTO_LOAD_RUNTIME_RESET_ENTRY_MESSAGE); + log_runtime_reset_entry(this); + let trampoline: RuntimeResetFn = unsafe { mem::transmute(RUNTIME_RESET_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + append_log_message(AUTO_LOAD_RUNTIME_RESET_RETURNED_MESSAGE); + log_runtime_reset_return(this, result); + result +} + +pub(super) unsafe extern "cdecl" fn allocator_detour(size: u32) -> *mut u8 { + let trace = should_trace_allocator(size); + let count = if trace { + AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 + } else { + 0 + }; + if trace { + append_log_message(AUTO_LOAD_ALLOCATOR_ENTRY_MESSAGE); + log_allocator_entry(size, count); + } + let original: StartupRuntimeAllocThunkFn = unsafe { mem::transmute(0x005a125dusize) }; + let result = unsafe { original(size) }; + if trace { + append_log_message(AUTO_LOAD_ALLOCATOR_RETURNED_MESSAGE); + log_allocator_return(size, count, result); + } + result +} + +pub(super) unsafe extern "fastcall" fn load_screen_scalar_detour( + this: *mut u8, + _edx: usize, + value_bits: u32, +) -> u32 { + append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_ENTRY_MESSAGE); + log_load_screen_scalar_entry(this, value_bits); + let trampoline: LoadScreenSetScalarFn = + unsafe { mem::transmute(LOAD_SCREEN_SCALAR_TRAMPOLINE) }; + let result = unsafe { trampoline(this, value_bits) }; + append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_RETURNED_MESSAGE); + log_load_screen_scalar_return(this, value_bits, result); + result +} + +pub(super) unsafe extern "fastcall" fn load_screen_construct_detour( + this: *mut u8, + _edx: usize, +) -> *mut u8 { + append_log_message(AUTO_LOAD_LOAD_SCREEN_ENTRY_MESSAGE); + log_load_screen_construct_entry(this); + let trampoline: LoadScreenConstructFn = + unsafe { mem::transmute(LOAD_SCREEN_CONSTRUCT_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + append_log_message(AUTO_LOAD_LOAD_SCREEN_RETURNED_MESSAGE); + log_load_screen_construct_return(this, result); + result +} + +pub(super) unsafe extern "fastcall" fn load_screen_message_detour( + this: *mut u8, + _edx: usize, + message: *mut u8, +) -> i32 { + let trace = should_trace_load_screen_message(this); + let count = if trace { + AUTO_LOAD_LOAD_SCREEN_MESSAGE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 + } else { + 0 + }; + if trace { + append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_ENTRY_MESSAGE); + log_load_screen_message_entry(this, message, count); + } + let trampoline: LoadScreenHandleMessageFn = + unsafe { mem::transmute(LOAD_SCREEN_MESSAGE_TRAMPOLINE) }; + let result = unsafe { trampoline(this, message) }; + if trace { + append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_RETURNED_MESSAGE); + log_load_screen_message_return(this, message, count, result); + } + result +} + +pub(super) unsafe extern "fastcall" fn runtime_prime_detour(this: *mut u8, _edx: usize) -> i32 { + let trace = should_trace_runtime_prime(); + let count = if trace { + AUTO_LOAD_RUNTIME_PRIME_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 + } else { + 0 + }; + if trace { + append_log_message(AUTO_LOAD_RUNTIME_PRIME_ENTRY_MESSAGE); + log_runtime_prime_entry(this, count); + } + let trampoline: ShellRuntimePrimeFn = unsafe { mem::transmute(RUNTIME_PRIME_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + if trace { + append_log_message(AUTO_LOAD_RUNTIME_PRIME_RETURNED_MESSAGE); + log_runtime_prime_return(this, count, result); + } + result +} + +pub(super) unsafe extern "fastcall" fn frame_cycle_detour(this: *mut u8, _edx: usize) -> i32 { + let trace = should_trace_frame_cycle(); + let count = if trace { + AUTO_LOAD_FRAME_CYCLE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 + } else { + 0 + }; + if trace { + append_log_message(AUTO_LOAD_FRAME_CYCLE_ENTRY_MESSAGE); + log_frame_cycle_entry(this, count); + } + let trampoline: ShellFrameCycleFn = unsafe { mem::transmute(FRAME_CYCLE_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + if trace { + append_log_message(AUTO_LOAD_FRAME_CYCLE_RETURNED_MESSAGE); + log_frame_cycle_return(this, count, result); + } + result +} + +pub(super) unsafe extern "fastcall" fn object_service_detour(this: *mut u8, _edx: usize) -> i32 { + let trace = should_trace_object_service(); + let count = if trace { + AUTO_LOAD_OBJECT_SERVICE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 + } else { + 0 + }; + if trace { + append_log_message(AUTO_LOAD_OBJECT_SERVICE_ENTRY_MESSAGE); + log_object_service_entry(this, count); + } + let trampoline: ShellObjectServiceFn = unsafe { mem::transmute(OBJECT_SERVICE_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + if trace { + append_log_message(AUTO_LOAD_OBJECT_SERVICE_RETURNED_MESSAGE); + log_object_service_return(this, count, result); + } + result +} + +pub(super) unsafe extern "fastcall" fn child_service_detour(this: *mut u8, _edx: usize) -> i32 { + let trace = should_trace_child_service(); + let count = if trace { + AUTO_LOAD_CHILD_SERVICE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1 + } else { + 0 + }; + if trace { + append_log_message(AUTO_LOAD_CHILD_SERVICE_ENTRY_MESSAGE); + log_child_service_entry(this, count); + } + let trampoline: ShellChildServiceFn = unsafe { mem::transmute(CHILD_SERVICE_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + if trace { + append_log_message(AUTO_LOAD_CHILD_SERVICE_RETURNED_MESSAGE); + log_child_service_return(this, count, result); + } + result +} + +pub(super) unsafe extern "fastcall" fn shell_publish_detour( + this: *mut u8, + _edx: usize, + object: *mut u8, + flag: u32, +) -> i32 { + append_log_message(AUTO_LOAD_PUBLISH_ENTRY_MESSAGE); + log_shell_publish_entry(this, object, flag); + let trampoline: ShellPublishWindowFn = unsafe { mem::transmute(SHELL_PUBLISH_TRAMPOLINE) }; + let result = unsafe { trampoline(this, object, flag) }; + append_log_message(AUTO_LOAD_PUBLISH_RETURNED_MESSAGE); + log_shell_publish_return(this, object, flag, result); + result +} + +pub(super) unsafe extern "fastcall" fn shell_unpublish_detour( + this: *mut u8, + _edx: usize, + object: *mut u8, +) -> i32 { + append_log_message(AUTO_LOAD_UNPUBLISH_ENTRY_MESSAGE); + log_shell_unpublish_entry(this, object); + let trampoline: ShellUnpublishWindowFn = unsafe { mem::transmute(SHELL_UNPUBLISH_TRAMPOLINE) }; + let result = unsafe { trampoline(this, object) }; + append_log_message(AUTO_LOAD_UNPUBLISH_RETURNED_MESSAGE); + log_shell_unpublish_return(this, object, result); + result +} + +pub(super) unsafe extern "fastcall" fn shell_object_range_remove_detour( + this: *mut u8, + _edx: usize, + arg1: u32, + arg2: u32, + arg3: u32, +) -> i32 { + append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_ENTRY_MESSAGE); + log_shell_object_range_remove_entry(this, arg1, arg2, arg3); + let trampoline: ShellObjectRangeRemoveFn = + unsafe { mem::transmute(SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE) }; + let result = unsafe { trampoline(this, arg1, arg2, arg3) }; + append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_RETURNED_MESSAGE); + log_shell_object_range_remove_return(this, arg1, arg2, arg3, result); + result +} + +pub(super) unsafe extern "fastcall" fn shell_object_teardown_detour( + this: *mut u8, + _edx: usize, +) -> i32 { + append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_ENTRY_MESSAGE); + log_shell_object_teardown_entry(this); + let trampoline: ShellObjectTeardownFn = + unsafe { mem::transmute(SHELL_OBJECT_TEARDOWN_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_RETURNED_MESSAGE); + log_shell_object_teardown_return(this, result); + result +} + +pub(super) unsafe extern "fastcall" fn shell_node_vcall_detour( + this: *mut u8, + _edx: usize, + record: *mut u8, +) -> i32 { + append_log_message(AUTO_LOAD_NODE_VCALL_ENTRY_MESSAGE); + log_shell_node_vcall_entry(this, record); + let trampoline: ShellNodeVcallFn = unsafe { mem::transmute(SHELL_NODE_VCALL_TRAMPOLINE) }; + let result = unsafe { trampoline(this, record) }; + append_log_message(AUTO_LOAD_NODE_VCALL_RETURNED_MESSAGE); + log_shell_node_vcall_return(this, record, result); + result +} + +pub(super) unsafe extern "fastcall" fn shell_remove_node_detour( + this: *mut u8, + _edx: usize, + node: *mut u8, +) -> i32 { + append_log_message(AUTO_LOAD_REMOVE_NODE_ENTRY_MESSAGE); + log_shell_remove_node_entry(this, node); + let trampoline: ShellRemoveNodeFn = unsafe { mem::transmute(SHELL_REMOVE_NODE_TRAMPOLINE) }; + let result = unsafe { trampoline(this, node) }; + append_log_message(AUTO_LOAD_REMOVE_NODE_RETURNED_MESSAGE); + log_shell_remove_node_return(this, node, result); + result +} + +pub(super) unsafe extern "fastcall" fn mode2_teardown_detour(this: *mut u8, _edx: usize) -> i32 { + append_log_message(AUTO_LOAD_MODE2_TEARDOWN_ENTRY_MESSAGE); + log_mode2_teardown_entry(this); + let trampoline: Mode2TeardownFn = unsafe { mem::transmute(MODE2_TEARDOWN_TRAMPOLINE) }; + let result = unsafe { trampoline(this) }; + append_log_message(AUTO_LOAD_MODE2_TEARDOWN_RETURNED_MESSAGE); + log_mode2_teardown_return(this, result); + result +} diff --git a/crates/rrt-hook/src/windows/ffi.rs b/crates/rrt-hook/src/windows/ffi.rs new file mode 100644 index 0000000..862e284 --- /dev/null +++ b/crates/rrt-hook/src/windows/ffi.rs @@ -0,0 +1,86 @@ +use super::*; + +unsafe extern "system" { + pub(super) fn CreateFileA( + lp_file_name: *const c_char, + desired_access: u32, + share_mode: u32, + security_attributes: *mut c_void, + creation_disposition: u32, + flags_and_attributes: u32, + template_file: *mut c_void, + ) -> isize; + pub(super) fn SetFilePointer( + file: isize, + distance: i32, + distance_high: *mut i32, + move_method: u32, + ) -> u32; + pub(super) fn WriteFile( + file: isize, + buffer: *const c_void, + bytes_to_write: u32, + bytes_written: *mut u32, + overlapped: *mut c_void, + ) -> i32; + pub(super) fn CloseHandle(handle: isize) -> i32; + pub(super) fn DisableThreadLibraryCalls(module: *mut c_void) -> i32; + pub(super) fn FlushInstructionCache( + process: *mut c_void, + base_address: *const c_void, + size: usize, + ) -> i32; + pub(super) fn GetCurrentProcess() -> *mut c_void; + pub(super) fn GetSystemDirectoryA(buffer: *mut u8, size: u32) -> u32; + pub(super) fn GetProcAddress(module: isize, name: *const c_char) -> *mut c_void; + pub(super) fn LoadLibraryA(name: *const c_char) -> isize; + pub(super) fn OutputDebugStringA(output: *const c_char); + pub(super) fn VirtualAlloc( + address: *mut c_void, + size: usize, + allocation_type: u32, + protect: u32, + ) -> *mut c_void; + pub(super) fn VirtualProtect( + address: *mut c_void, + size: usize, + new_protect: u32, + old_protect: *mut u32, + ) -> i32; +} + +#[repr(C)] +pub(crate) struct Guid { + data1: u32, + data2: u16, + data3: u16, + data4: [u8; 8], +} + +pub(super) type DirectInput8CreateFn = unsafe extern "system" fn( + instance: *mut c_void, + version: u32, + riid: *const Guid, + out: *mut *mut c_void, + outer: *mut c_void, +) -> i32; +pub(super) type ShellStateServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32; +pub(super) type ShellTransitionModeFn = unsafe extern "thiscall" fn(*mut u8, u32, u32) -> i32; +pub(super) type ProfileStartupDispatchFn = unsafe extern "thiscall" fn(*mut u8, u32, u32) -> i32; +pub(super) type RuntimeResetFn = unsafe extern "thiscall" fn(*mut u8) -> *mut u8; +pub(super) type StartupRuntimeAllocThunkFn = unsafe extern "cdecl" fn(u32) -> *mut u8; +pub(super) type LoadScreenSetScalarFn = unsafe extern "thiscall" fn(*mut u8, u32) -> u32; +pub(super) type LoadScreenConstructFn = unsafe extern "thiscall" fn(*mut u8) -> *mut u8; +pub(super) type LoadScreenHandleMessageFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32; +pub(super) type ShellRuntimePrimeFn = unsafe extern "thiscall" fn(*mut u8) -> i32; +pub(super) type ShellFrameCycleFn = unsafe extern "thiscall" fn(*mut u8) -> i32; +pub(super) type ShellObjectServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32; +pub(super) type ShellChildServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32; +pub(super) type ShellPublishWindowFn = unsafe extern "thiscall" fn(*mut u8, *mut u8, u32) -> i32; +pub(super) type ShellUnpublishWindowFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32; +pub(super) type ShellObjectTeardownFn = unsafe extern "thiscall" fn(*mut u8) -> i32; +pub(super) type ShellObjectRangeRemoveFn = + unsafe extern "thiscall" fn(*mut u8, u32, u32, u32) -> i32; +pub(super) type ShellRemoveNodeFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32; +pub(super) type ShellNodeVcallFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32; +pub(super) type Mode2TeardownFn = unsafe extern "thiscall" fn(*mut u8) -> i32; diff --git a/crates/rrt-hook/src/windows/install.rs b/crates/rrt-hook/src/windows/install.rs new file mode 100644 index 0000000..3820e96 --- /dev/null +++ b/crates/rrt-hook/src/windows/install.rs @@ -0,0 +1,383 @@ +use super::*; + +pub(super) unsafe fn install_shell_state_service_hook() -> bool { + const STOLEN_LEN: usize = 6; + let target = SHELL_STATE_SERVICE_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + shell_state_service_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { SHELL_STATE_SERVICE_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_profile_startup_dispatch_hook() -> bool { + const STOLEN_LEN: usize = 16; + let target = PROFILE_STARTUP_DISPATCH_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + profile_startup_dispatch_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { PROFILE_STARTUP_DISPATCH_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_runtime_reset_hook() -> bool { + const STOLEN_LEN: usize = 16; + let target = RUNTIME_RESET_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + runtime_reset_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { RUNTIME_RESET_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_allocator_hook() -> bool { + const STOLEN_LEN: usize = 5; + let target = STARTUP_RUNTIME_ALLOC_THUNK_ADDR as *mut u8; + let trampoline = + unsafe { install_rel32_detour(target, STOLEN_LEN, allocator_detour as *const () as usize) }; + if trampoline == 0 { + return false; + } + unsafe { ALLOCATOR_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_load_screen_scalar_hook() -> bool { + const STOLEN_LEN: usize = 10; + let target = LOAD_SCREEN_SET_SCALAR_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + load_screen_scalar_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { LOAD_SCREEN_SCALAR_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_load_screen_construct_hook() -> bool { + const STOLEN_LEN: usize = 6; + let target = LOAD_SCREEN_CONSTRUCT_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + load_screen_construct_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { LOAD_SCREEN_CONSTRUCT_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_load_screen_message_hook() -> bool { + const STOLEN_LEN: usize = 25; + let target = LOAD_SCREEN_HANDLE_MESSAGE_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + load_screen_message_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { LOAD_SCREEN_MESSAGE_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_runtime_prime_hook() -> bool { + const STOLEN_LEN: usize = 12; + let target = SHELL_RUNTIME_PRIME_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + runtime_prime_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { RUNTIME_PRIME_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_frame_cycle_hook() -> bool { + const STOLEN_LEN: usize = 6; + let target = SHELL_FRAME_CYCLE_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour(target, STOLEN_LEN, frame_cycle_detour as *const () as usize) + }; + if trampoline == 0 { + return false; + } + unsafe { FRAME_CYCLE_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_object_service_hook() -> bool { + const STOLEN_LEN: usize = 6; + let target = SHELL_OBJECT_SERVICE_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + object_service_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { OBJECT_SERVICE_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_child_service_hook() -> bool { + const STOLEN_LEN: usize = 6; + let target = SHELL_CHILD_SERVICE_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + child_service_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { CHILD_SERVICE_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_shell_publish_hook() -> bool { + const STOLEN_LEN: usize = 6; + let target = SHELL_PUBLISH_WINDOW_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + shell_publish_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { SHELL_PUBLISH_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_shell_unpublish_hook() -> bool { + const STOLEN_LEN: usize = 10; + let target = SHELL_UNPUBLISH_WINDOW_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + shell_unpublish_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { SHELL_UNPUBLISH_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_shell_object_teardown_hook() -> bool { + const STOLEN_LEN: usize = 7; + let target = SHELL_OBJECT_TEARDOWN_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + shell_object_teardown_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { SHELL_OBJECT_TEARDOWN_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_shell_object_range_remove_hook() -> bool { + const STOLEN_LEN: usize = 12; + let target = SHELL_OBJECT_RANGE_REMOVE_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + shell_object_range_remove_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_shell_node_vcall_hook() -> bool { + const STOLEN_LEN: usize = 21; + let target = SHELL_NODE_VCALL_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + shell_node_vcall_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { SHELL_NODE_VCALL_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_shell_remove_node_hook() -> bool { + const STOLEN_LEN: usize = 10; + let target = SHELL_REMOVE_NODE_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + shell_remove_node_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { SHELL_REMOVE_NODE_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_mode2_teardown_hook() -> bool { + const STOLEN_LEN: usize = 14; + let target = MODE2_TEARDOWN_ADDR as *mut u8; + let trampoline = unsafe { + install_rel32_detour( + target, + STOLEN_LEN, + mode2_teardown_detour as *const () as usize, + ) + }; + if trampoline == 0 { + return false; + } + unsafe { MODE2_TEARDOWN_TRAMPOLINE = trampoline }; + true +} + +pub(super) unsafe fn install_rel32_detour( + target: *mut u8, + stolen_len: usize, + detour: usize, +) -> usize { + let trampoline_size = stolen_len + 5; + let trampoline = unsafe { + VirtualAlloc( + ptr::null_mut(), + trampoline_size, + MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE, + ) + } as *mut u8; + if trampoline.is_null() { + return 0; + } + + unsafe { ptr::copy_nonoverlapping(target, trampoline, stolen_len) }; + unsafe { write_rel32_jump(trampoline.add(stolen_len), target.add(stolen_len) as usize) }; + + let mut old_protect = 0_u32; + if unsafe { + VirtualProtect( + target.cast(), + stolen_len, + PAGE_EXECUTE_READWRITE, + &mut old_protect, + ) + } == 0 + { + return 0; + } + + unsafe { write_rel32_jump(target, detour) }; + for offset in 5..stolen_len { + unsafe { ptr::write(target.add(offset), 0x90) }; + } + let mut restore_protect = 0_u32; + let _ = unsafe { VirtualProtect(target.cast(), stolen_len, old_protect, &mut restore_protect) }; + let _ = unsafe { FlushInstructionCache(GetCurrentProcess(), target.cast(), stolen_len) }; + trampoline as usize +} + +pub(super) unsafe fn write_rel32_jump(location: *mut u8, destination: usize) { + unsafe { ptr::write(location, 0xE9) }; + let next_ip = unsafe { location.add(5) } as usize; + let relative = (destination as isize - next_ip as isize) as i32; + unsafe { ptr::write_unaligned(location.add(1).cast::(), relative) }; +} + +pub(super) unsafe fn load_direct_input8_create() -> Option { + if let Some(callback) = unsafe { REAL_DINPUT8_CREATE } { + return Some(callback); + } + + let mut system_directory = [0_u8; 260]; + let length = unsafe { + GetSystemDirectoryA(system_directory.as_mut_ptr(), system_directory.len() as u32) + }; + if length == 0 || length as usize >= system_directory.len() { + return None; + } + + let mut dll_path = system_directory[..length as usize].to_vec(); + dll_path.extend_from_slice(br"\dinput8.dll"); + dll_path.push(0); + + let module = unsafe { LoadLibraryA(dll_path.as_ptr().cast()) }; + if module == 0 { + return None; + } + + let symbol = unsafe { GetProcAddress(module, DIRECT_INPUT8_CREATE_NAME.as_ptr().cast()) }; + if symbol.is_null() { + return None; + } + + let callback: DirectInput8CreateFn = unsafe { mem::transmute(symbol) }; + unsafe { + REAL_DINPUT8_CREATE = Some(callback); + } + Some(callback) +} diff --git a/crates/rrt-hook/src/windows/logging.rs b/crates/rrt-hook/src/windows/logging.rs new file mode 100644 index 0000000..b2ec859 --- /dev/null +++ b/crates/rrt-hook/src/windows/logging.rs @@ -0,0 +1,1096 @@ +use super::*; + +pub(super) unsafe fn append_attach_log() { + append_log_message(ATTACH_MESSAGE); +} + +pub(super) fn append_log_message(message: &[u8]) { + let handle = unsafe { + CreateFileA( + LOG_PATH.as_ptr().cast(), + FILE_APPEND_DATA, + FILE_SHARE_READ, + ptr::null_mut(), + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + ptr::null_mut(), + ) + }; + if handle == INVALID_HANDLE_VALUE { + return; + } + + let _ = unsafe { SetFilePointer(handle, 0, ptr::null_mut(), FILE_END) }; + let mut bytes_written = 0_u32; + let _ = unsafe { + WriteFile( + handle, + message.as_ptr().cast(), + message.len() as u32, + &mut bytes_written, + ptr::null_mut(), + ) + }; + let _ = unsafe { CloseHandle(handle) }; +} + +pub(super) fn append_log_line(line: &str) { + append_log_message(line.as_bytes()); +} + +pub(super) fn log_auto_load_launch_state(runtime_profile: *mut u8) { + let startup_selector = + unsafe { read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) }; + let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + let mode_id = if shell_state.is_null() { + 0 + } else { + unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize } + }; + let mut line = String::from("rrt-hook: auto load launch state "); + let _ = write!( + &mut line, + "selector=0x{startup_selector:02x} mode_id=0x{mode_id:08x} global_active_mode=0x{global_active_mode:08x} target_mode=0x{SHELL_MODE_STARTUP_LOAD_DISPATCH:08x}\n", + ); + append_log_line(&line); +} + +pub(super) fn log_post_transition_state() { + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + let active_mode_global = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let field_active_mode_object = if shell_state.is_null() { + 0 + } else { + unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } + }; + let runtime_profile = unsafe { read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8) }; + let startup_selector = if runtime_profile.is_null() { + 0 + } else { + unsafe { read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize } + }; + let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) }; + let load_screen_scalar = if load_screen_singleton.is_null() { + 0 + } else { + unsafe { read_u32(load_screen_singleton.add(0x78)) } + }; + let mut line = String::from("rrt-hook: auto load post-transition state "); + let _ = write!( + &mut line, + "shell_state=0x{:08x} active_mode_global=0x{active_mode_global:08x} field_active_mode_object=0x{field_active_mode_object:08x} load_screen_singleton=0x{:08x} load_screen_scalar=0x{load_screen_scalar:08x} startup_selector=0x{startup_selector:08x}\n", + shell_state as usize, load_screen_singleton as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_post_transition_service_state() { + if !AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) { + return; + } + let count = AUTO_LOAD_POST_TRANSITION_SERVICE_LOG_COUNT.fetch_add(1, Ordering::AcqRel); + if count >= 8 { + return; + } + + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + let active_mode_global = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let field_active_mode_object = if shell_state.is_null() { + 0 + } else { + unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } + }; + let runtime_profile = unsafe { read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8) }; + let startup_selector = if runtime_profile.is_null() { + 0 + } else { + unsafe { read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize } + }; + let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) }; + let load_screen_scalar = if load_screen_singleton.is_null() { + 0 + } else { + unsafe { read_u32(load_screen_singleton.add(0x78)) } + }; + let mut line = String::from("rrt-hook: auto load post-transition service state "); + let _ = write!( + &mut line, + "count=0x{:08x} shell_state=0x{:08x} active_mode_global=0x{active_mode_global:08x} field_active_mode_object=0x{field_active_mode_object:08x} load_screen_singleton=0x{:08x} load_screen_scalar=0x{load_screen_scalar:08x} startup_selector=0x{startup_selector:08x}\n", + count + 1, + shell_state as usize, + load_screen_singleton as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_state_service_entry(this: *mut u8) { + let count = AUTO_LOAD_SERVICE_ENTRY_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1; + if count > 8 { + return; + } + let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() }; + let mode_id = unsafe { current_mode_id() }; + let active_mode_global = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let field_active_mode_object = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } + }; + let field_a0 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0xa0)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell-state service entry "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} active_mode_global=0x{active_mode_global:08x} field_active_mode_object=0x{field_active_mode_object:08x} field_a0=0x{field_a0:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_state_service_return(this: *mut u8, result: i32) { + let count = AUTO_LOAD_SERVICE_RETURN_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1; + if count > 8 { + return; + } + let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() }; + let mode_id = unsafe { current_mode_id() }; + let active_mode_global = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let field_active_mode_object = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize } + }; + let field_a0 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0xa0)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell-state service return "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} active_mode_global=0x{active_mode_global:08x} field_active_mode_object=0x{field_active_mode_object:08x} field_a0=0x{field_a0:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_profile_startup_dispatch_entry(this: *mut u8, arg1: u32, arg2: u32) { + let startup_selector = unsafe { + let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8); + if runtime_profile.is_null() { + 0 + } else { + read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize + } + }; + let mut line = String::from("rrt-hook: auto load startup dispatch entry "); + let _ = write!( + &mut line, + "this=0x{:08x} arg1=0x{arg1:08x} arg2=0x{arg2:08x} selector=0x{startup_selector:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_profile_startup_dispatch_return( + this: *mut u8, + arg1: u32, + arg2: u32, + result: i32, +) { + let active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load startup dispatch return "); + let _ = write!( + &mut line, + "this=0x{:08x} arg1=0x{arg1:08x} arg2=0x{arg2:08x} result=0x{result:08x} active_mode=0x{active_mode:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_runtime_reset_entry(this: *mut u8) { + let field_4cae = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x4cae)) as usize } + }; + let field_4cb2 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x4cb2)) as usize } + }; + let field_66b2 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x66b2)) as usize } + }; + let field_66b6 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x66b6)) as usize } + }; + let mut line = String::from("rrt-hook: auto load runtime reset entry "); + let _ = write!( + &mut line, + "this=0x{:08x} field_4cae=0x{field_4cae:08x} field_4cb2=0x{field_4cb2:08x} field_66b2=0x{field_66b2:08x} field_66b6=0x{field_66b6:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_runtime_reset_return(this: *mut u8, result: *mut u8) { + let field_4cae = if result.is_null() { + 0 + } else { + unsafe { read_u32(result.add(0x4cae)) as usize } + }; + let field_4cb2 = if result.is_null() { + 0 + } else { + unsafe { read_u32(result.add(0x4cb2)) as usize } + }; + let field_66b2 = if result.is_null() { + 0 + } else { + unsafe { read_u32(result.add(0x66b2)) as usize } + }; + let field_66b6 = if result.is_null() { + 0 + } else { + unsafe { read_u32(result.add(0x66b6)) as usize } + }; + let mut line = String::from("rrt-hook: auto load runtime reset return "); + let _ = write!( + &mut line, + "this=0x{:08x} result=0x{:08x} field_4cae=0x{field_4cae:08x} field_4cb2=0x{field_4cb2:08x} field_66b2=0x{field_66b2:08x} field_66b6=0x{field_66b6:08x}\n", + this as usize, result as usize, + ); + append_log_line(&line); +} + +pub(super) fn should_trace_allocator(size: u32) -> bool { + if size == STARTUP_RUNTIME_OBJECT_SIZE { + return true; + } + AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.load(Ordering::Acquire) + && AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.load(Ordering::Acquire) < 16 +} + +pub(super) fn log_allocator_entry(size: u32, count: u32) { + let mut line = String::from("rrt-hook: auto load allocator entry "); + let transition_window = AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.load(Ordering::Acquire); + let _ = write!( + &mut line, + "count=0x{count:08x} size=0x{size:08x} transition_window={transition_window}\n", + ); + append_log_line(&line); +} + +pub(super) fn log_allocator_return(size: u32, count: u32, result: *mut u8) { + let mut line = String::from("rrt-hook: auto load allocator return "); + let _ = write!( + &mut line, + "count=0x{count:08x} size=0x{size:08x} result=0x{:08x}\n", + result as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_load_screen_scalar_entry(this: *mut u8, value_bits: u32) { + let field_78 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x78)) } + }; + let mut line = String::from("rrt-hook: auto load load-screen scalar entry "); + let _ = write!( + &mut line, + "this=0x{:08x} value_bits=0x{value_bits:08x} field_78=0x{field_78:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_load_screen_scalar_return(this: *mut u8, value_bits: u32, result: u32) { + let field_78 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x78)) } + }; + let mut line = String::from("rrt-hook: auto load load-screen scalar return "); + let _ = write!( + &mut line, + "this=0x{:08x} value_bits=0x{value_bits:08x} result=0x{result:08x} field_78=0x{field_78:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_load_screen_construct_entry(this: *mut u8) { + let mut line = String::from("rrt-hook: auto load load-screen construct entry "); + let _ = write!(&mut line, "this=0x{:08x}\n", this as usize); + append_log_line(&line); +} + +pub(super) fn log_load_screen_construct_return(this: *mut u8, result: *mut u8) { + let singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load load-screen construct return "); + let _ = write!( + &mut line, + "this=0x{:08x} result=0x{:08x} singleton=0x{singleton:08x}\n", + this as usize, result as usize, + ); + append_log_line(&line); + if AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.load(Ordering::Acquire) { + AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.store(0, Ordering::Release); + } +} + +pub(super) fn should_trace_load_screen_message(this: *mut u8) -> bool { + if !AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) { + return false; + } + let singleton = unsafe { read_ptr(0x006d10b0 as *const u8) }; + if singleton.is_null() || this != singleton { + return false; + } + AUTO_LOAD_LOAD_SCREEN_MESSAGE_LOG_COUNT.load(Ordering::Acquire) < 16 +} + +pub(super) fn log_load_screen_message_entry(this: *mut u8, message: *mut u8, count: u32) { + let message_id = if message.is_null() { + 0 + } else { + unsafe { read_u32(message) } + }; + let message_arg8 = if message.is_null() { + 0 + } else { + unsafe { read_u32(message.add(0x08)) } + }; + let page = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x78)) } + }; + let page_substate = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x7c)) } + }; + let page_kind = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x80)) } + }; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load load-screen message entry "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} message=0x{:08x} message_id=0x{message_id:08x} message_arg8=0x{message_arg8:08x} page=0x{page:08x} page_substate=0x{page_substate:08x} page_kind=0x{page_kind:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, message as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_load_screen_message_return( + this: *mut u8, + message: *mut u8, + count: u32, + result: i32, +) { + let page = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x78)) } + }; + let page_substate = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x7c)) } + }; + let page_kind = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x80)) } + }; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load load-screen message return "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} message=0x{:08x} result=0x{result:08x} page=0x{page:08x} page_substate=0x{page_substate:08x} page_kind=0x{page_kind:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + message as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn should_trace_runtime_prime() -> bool { + AUTO_LOAD_RUNTIME_PRIME_LOG_COUNT.load(Ordering::Acquire) < 12 +} + +pub(super) fn log_runtime_prime_entry(this: *mut u8, count: u32) { + let list_head = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x04)) as usize } + }; + let field_0c68 = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x0c68)) as usize } + }; + let field_001c = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x1c)) as usize } + }; + let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell runtime-prime entry "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} list_head=0x{list_head:08x} field_0c68=0x{field_0c68:02x} field_1c=0x{field_001c:08x} load_screen_singleton=0x{load_screen_singleton:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_runtime_prime_return(this: *mut u8, count: u32, result: i32) { + let list_head = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x04)) as usize } + }; + let field_001c = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x1c)) as usize } + }; + let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell runtime-prime return "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} list_head=0x{list_head:08x} field_1c=0x{field_001c:08x} load_screen_singleton=0x{load_screen_singleton:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn should_trace_frame_cycle() -> bool { + AUTO_LOAD_FRAME_CYCLE_LOG_COUNT.load(Ordering::Acquire) < 12 +} + +pub(super) fn log_frame_cycle_entry(this: *mut u8, count: u32) { + let field_18 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x18)) as usize } + }; + let field_1c = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x1c)) as usize } + }; + let field_20 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x20)) as usize } + }; + let field_28 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x28)) as usize } + }; + let flag_55 = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x55)) as usize } + }; + let flag_56 = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x56)) as usize } + }; + let field_58 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x58)) as usize } + }; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell frame-cycle entry "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} field_18=0x{field_18:08x} field_1c=0x{field_1c:08x} field_20=0x{field_20:08x} field_28=0x{field_28:08x} flag_55=0x{flag_55:02x} flag_56=0x{flag_56:02x} field_58=0x{field_58:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_frame_cycle_return(this: *mut u8, count: u32, result: i32) { + let flag_55 = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x55)) as usize } + }; + let flag_56 = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x56)) as usize } + }; + let field_58 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x58)) as usize } + }; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell frame-cycle return "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} flag_55=0x{flag_55:02x} flag_56=0x{flag_56:02x} field_58=0x{field_58:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn should_trace_object_service() -> bool { + AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) + && AUTO_LOAD_OBJECT_SERVICE_LOG_COUNT.load(Ordering::Acquire) < 16 +} + +pub(super) fn log_object_service_entry(this: *mut u8, count: u32) { + let vtable = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this) as usize } + }; + let field_1d = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x1d)) as usize } + }; + let field_5c = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x5c)) as usize } + }; + let child_head = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x70)) as usize } + }; + let child_tail = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x74)) as usize } + }; + let first_child_vtable = if child_head == 0 { + 0 + } else { + unsafe { read_ptr(child_head as *const u8) as usize } + }; + let first_child_call18 = if first_child_vtable == 0 { + 0 + } else { + unsafe { read_ptr((first_child_vtable + 0x18) as *const u8) as usize } + }; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell object-service entry "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} vtable=0x{vtable:08x} field_1d=0x{field_1d:02x} field_5c=0x{field_5c:08x} child_head=0x{child_head:08x} child_tail=0x{child_tail:08x} child_call18=0x{first_child_call18:08x} load_screen_singleton=0x{load_screen_singleton:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_object_service_return(this: *mut u8, count: u32, result: i32) { + let field_1d = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x1d)) as usize } + }; + let child_head = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x70)) as usize } + }; + let child_tail = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x74)) as usize } + }; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell object-service return "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} field_1d=0x{field_1d:02x} child_head=0x{child_head:08x} child_tail=0x{child_tail:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn should_trace_child_service() -> bool { + AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire) + && AUTO_LOAD_CHILD_SERVICE_LOG_COUNT.load(Ordering::Acquire) < 16 +} + +pub(super) fn log_child_service_entry(this: *mut u8, count: u32) { + let vtable = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this) as usize } + }; + let field_4b = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x4b)) as usize } + }; + let flag_68 = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x68)) as usize } + }; + let flag_6a = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x6a)) as usize } + }; + let field_86 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x86)) as usize } + }; + let field_b0 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0xb0)) as usize } + }; + let field_b8 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0xb8)) as usize } + }; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let load_screen_singleton = unsafe { read_ptr(0x006d10b0 as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell child-service entry "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} vtable=0x{vtable:08x} field_4b=0x{field_4b:08x} flag_68=0x{flag_68:02x} flag_6a=0x{flag_6a:02x} field_86=0x{field_86:08x} field_b0=0x{field_b0:08x} field_b8=0x{field_b8:08x} load_screen_singleton=0x{load_screen_singleton:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_child_service_return(this: *mut u8, count: u32, result: i32) { + let field_b0 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0xb0)) as usize } + }; + let startup_runtime = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell child-service return "); + let _ = write!( + &mut line, + "count=0x{count:08x} this=0x{:08x} result=0x{result:08x} field_b0=0x{field_b0:08x} startup_runtime=0x{startup_runtime:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_publish_entry(this: *mut u8, object: *mut u8, flag: u32) { + let mut line = String::from("rrt-hook: auto load shell publish entry "); + let _ = write!( + &mut line, + "this=0x{:08x} object=0x{:08x} flag=0x{flag:08x}\n", + this as usize, object as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_publish_return(this: *mut u8, object: *mut u8, flag: u32, result: i32) { + let active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize; + let mut line = String::from("rrt-hook: auto load shell publish return "); + let _ = write!( + &mut line, + "this=0x{:08x} object=0x{:08x} flag=0x{flag:08x} result=0x{result:08x} active_mode=0x{active_mode:08x}\n", + this as usize, + object as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_unpublish_entry(this: *mut u8, object: *mut u8) { + let bundle_head = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this) as usize } + }; + let bundle_tail = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x04)) as usize } + }; + let object_vtable = if object.is_null() { + 0 + } else { + unsafe { read_ptr(object) as usize } + }; + let object_field_04 = if object.is_null() { + 0 + } else { + unsafe { read_ptr(object.add(0x04)) as usize } + }; + let object_field_08 = if object.is_null() { + 0 + } else { + unsafe { read_ptr(object.add(0x08)) as usize } + }; + let object_field_0c = if object.is_null() { + 0 + } else { + unsafe { read_ptr(object.add(0x0c)) as usize } + }; + let object_prev = if object.is_null() { + 0 + } else { + unsafe { read_ptr(object.add(0x54)) as usize } + }; + let object_next = if object.is_null() { + 0 + } else { + unsafe { read_ptr(object.add(0x58)) as usize } + }; + let object_list_74 = if object.is_null() { + 0 + } else { + unsafe { read_ptr(object.add(0x74)) as usize } + }; + let list_field_00 = if object_list_74 == 0 { + 0 + } else { + unsafe { read_ptr(object_list_74 as *const u8) as usize } + }; + let list_field_04 = if object_list_74 == 0 { + 0 + } else { + unsafe { read_u16((object_list_74 as *const u8).add(0x04)) as usize } + }; + let list_field_4b = if object_list_74 == 0 { + 0 + } else { + unsafe { read_u32((object_list_74 as *const u8).add(0x4b)) as usize } + }; + let list_field_8a = if object_list_74 == 0 { + 0 + } else { + unsafe { read_ptr((object_list_74 as *const u8).add(0x8a)) as usize } + }; + let list_vcall_04 = if list_field_00 == 0 { + 0 + } else { + unsafe { read_ptr((list_field_00 as *const u8).add(0x04)) as usize } + }; + let list2_field_00 = if list_field_8a == 0 { + 0 + } else { + unsafe { read_ptr(list_field_8a as *const u8) as usize } + }; + let list2_field_04 = if list_field_8a == 0 { + 0 + } else { + unsafe { read_u16((list_field_8a as *const u8).add(0x04)) as usize } + }; + let list2_field_4b = if list_field_8a == 0 { + 0 + } else { + unsafe { read_u32((list_field_8a as *const u8).add(0x4b)) as usize } + }; + let list2_field_8a = if list_field_8a == 0 { + 0 + } else { + unsafe { read_ptr((list_field_8a as *const u8).add(0x8a)) as usize } + }; + let list2_vcall_04 = if list2_field_00 == 0 { + 0 + } else { + unsafe { read_ptr((list2_field_00 as *const u8).add(0x04)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell unpublish entry "); + let _ = write!( + &mut line, + "this=0x{:08x} object=0x{:08x} bundle_head=0x{bundle_head:08x} bundle_tail=0x{bundle_tail:08x} object_vtable=0x{object_vtable:08x} field_04=0x{object_field_04:08x} field_08=0x{object_field_08:08x} field_0c=0x{object_field_0c:08x} prev=0x{object_prev:08x} next=0x{object_next:08x} list_74=0x{object_list_74:08x} list_00=0x{list_field_00:08x} list_call4=0x{list_vcall_04:08x} list_04=0x{list_field_04:04x} list_4b=0x{list_field_4b:08x} list_8a=0x{list_field_8a:08x} list2_00=0x{list2_field_00:08x} list2_call4=0x{list2_vcall_04:08x} list2_04=0x{list2_field_04:04x} list2_4b=0x{list2_field_4b:08x} list2_8a=0x{list2_field_8a:08x}\n", + this as usize, object as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_unpublish_return(this: *mut u8, object: *mut u8, result: i32) { + let mut line = String::from("rrt-hook: auto load shell unpublish return "); + let _ = write!( + &mut line, + "this=0x{:08x} object=0x{:08x} result=0x{result:08x}\n", + this as usize, + object as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_object_teardown_entry(this: *mut u8) { + let object_vtable = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this) as usize } + }; + let field_04 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x04)) as usize } + }; + let field_08 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x08)) as usize } + }; + let field_0c = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x0c)) as usize } + }; + let head_70 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x70)) as usize } + }; + let head_74 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x74)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell object teardown entry "); + let _ = write!( + &mut line, + "this=0x{:08x} vtable=0x{object_vtable:08x} field_04=0x{field_04:08x} field_08=0x{field_08:08x} field_0c=0x{field_0c:08x} head_70=0x{head_70:08x} head_74=0x{head_74:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_object_teardown_return(this: *mut u8, result: i32) { + let head_70 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x70)) as usize } + }; + let head_74 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x74)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell object teardown return "); + let _ = write!( + &mut line, + "this=0x{:08x} result=0x{result:08x} head_70=0x{head_70:08x} head_74=0x{head_74:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_object_range_remove_entry(this: *mut u8, arg1: u32, arg2: u32, arg3: u32) { + let head_74 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x74)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell object range-remove entry "); + let _ = write!( + &mut line, + "this=0x{:08x} arg1=0x{arg1:08x} arg2=0x{arg2:08x} arg3=0x{arg3:08x} head_74=0x{head_74:08x}\n", + this as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_object_range_remove_return( + this: *mut u8, + arg1: u32, + arg2: u32, + arg3: u32, + result: i32, +) { + let head_74 = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x74)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell object range-remove return "); + let _ = write!( + &mut line, + "this=0x{:08x} arg1=0x{arg1:08x} arg2=0x{arg2:08x} arg3=0x{arg3:08x} result=0x{result:08x} head_74=0x{head_74:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_node_vcall_entry(this: *mut u8, record: *mut u8) { + let node_vtable = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this) as usize } + }; + let node_type = if this.is_null() { + 0 + } else { + unsafe { read_u32(this) as usize } + }; + let node_field_08 = if this.is_null() { + 0 + } else { + unsafe { read_u32(this.add(0x08)) as usize } + }; + let node_field_20 = if this.is_null() { + 0 + } else { + unsafe { read_u8(this.add(0x20)) as usize } + }; + let record_kind = if record.is_null() { + 0 + } else { + unsafe { read_u32(record) as usize } + }; + let record_x = if record.is_null() { + 0 + } else { + unsafe { read_u32(record.add(0x24)) as usize } + }; + let record_y = if record.is_null() { + 0 + } else { + unsafe { read_u32(record.add(0x28)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell node-vcall entry "); + let _ = write!( + &mut line, + "this=0x{:08x} node_vtable=0x{node_vtable:08x} node_type=0x{node_type:08x} node_08=0x{node_field_08:08x} node_20=0x{node_field_20:02x} record=0x{:08x} record_kind=0x{record_kind:08x} record_x=0x{record_x:08x} record_y=0x{record_y:08x}\n", + this as usize, record as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_node_vcall_return(this: *mut u8, record: *mut u8, result: i32) { + let mut line = String::from("rrt-hook: auto load shell node-vcall return "); + let _ = write!( + &mut line, + "this=0x{:08x} record=0x{:08x} result=0x{result:08x}\n", + this as usize, + record as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_remove_node_entry(this: *mut u8, node: *mut u8) { + let owner_head = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x70)) as usize } + }; + let owner_tail = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x74)) as usize } + }; + let node_vtable = if node.is_null() { + 0 + } else { + unsafe { read_ptr(node) as usize } + }; + let node_kind = if node.is_null() { + 0 + } else { + unsafe { read_u16(node.add(0x04)) as usize } + }; + let node_owner = if node.is_null() { + 0 + } else { + unsafe { read_u32(node.add(0x4b)) as usize } + }; + let node_prev = if node.is_null() { + 0 + } else { + unsafe { read_ptr(node.add(0x8a)) as usize } + }; + let node_next = if node.is_null() { + 0 + } else { + unsafe { read_ptr(node.add(0x8e)) as usize } + }; + let node_call0 = if node_vtable == 0 { + 0 + } else { + unsafe { read_ptr(node_vtable as *const u8) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell remove-node entry "); + let _ = write!( + &mut line, + "this=0x{:08x} owner_head=0x{owner_head:08x} owner_tail=0x{owner_tail:08x} node=0x{:08x} node_vtable=0x{node_vtable:08x} node_call0=0x{node_call0:08x} node_kind=0x{node_kind:04x} node_owner=0x{node_owner:08x} node_prev=0x{node_prev:08x} node_next=0x{node_next:08x}\n", + this as usize, node as usize, + ); + append_log_line(&line); +} + +pub(super) fn log_shell_remove_node_return(this: *mut u8, node: *mut u8, result: i32) { + let owner_head = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x70)) as usize } + }; + let owner_tail = if this.is_null() { + 0 + } else { + unsafe { read_ptr(this.add(0x74)) as usize } + }; + let mut line = String::from("rrt-hook: auto load shell remove-node return "); + let _ = write!( + &mut line, + "this=0x{:08x} owner_head=0x{owner_head:08x} owner_tail=0x{owner_tail:08x} node=0x{:08x} result=0x{result:08x}\n", + this as usize, + node as usize, + result = result as u32, + ); + append_log_line(&line); +} + +pub(super) fn log_mode2_teardown_entry(this: *mut u8) { + let mut line = String::from("rrt-hook: auto load mode2 teardown entry "); + let _ = write!(&mut line, "this=0x{:08x}\n", this as usize); + append_log_line(&line); +} + +pub(super) fn log_mode2_teardown_return(this: *mut u8, result: i32) { + let mut line = String::from("rrt-hook: auto load mode2 teardown return "); + let _ = write!( + &mut line, + "this=0x{:08x} result=0x{result:08x}\n", + this as usize, + result = result as u32, + ); + append_log_line(&line); +} diff --git a/crates/rrt-hook/src/windows/memory.rs b/crates/rrt-hook/src/windows/memory.rs new file mode 100644 index 0000000..7c8fe12 --- /dev/null +++ b/crates/rrt-hook/src/windows/memory.rs @@ -0,0 +1,99 @@ +use super::*; + +pub(super) unsafe fn resolve_active_mode_ptr() -> *mut u8 { + let global_active_mode = unsafe { resolve_global_active_mode_ptr() }; + if !global_active_mode.is_null() { + return global_active_mode; + } + + let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) }; + if shell_state.is_null() { + return ptr::null_mut(); + } + + unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) } +} + +pub(super) unsafe fn resolve_global_active_mode_ptr() -> *mut u8 { + unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } +} + +pub(super) unsafe fn indexed_collection_entry_id_is_live( + collection: *const u8, + entry_id: usize, +) -> bool { + let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) }; + if entry_id == 0 || entry_id > id_bound.max(0) as usize { + return false; + } + + let tombstone_bits = + unsafe { read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) }; + if tombstone_bits.is_null() { + return true; + } + + let bit_index = entry_id as u32; + let word = + unsafe { ptr::read_unaligned(tombstone_bits.add((bit_index / 32) as usize).cast::()) }; + (word & (1_u32 << (bit_index % 32))) == 0 +} + +pub(super) unsafe fn indexed_collection_resolve_live_entry_by_id( + collection: *const u8, + entry_id: usize, +) -> *mut u8 { + if !unsafe { indexed_collection_entry_id_is_live(collection, entry_id) } { + return ptr::null_mut(); + } + + let payload = unsafe { read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) }; + if payload.is_null() { + return ptr::null_mut(); + } + + let stride = unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) as usize }; + let flat = unsafe { read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 }; + + if flat { + unsafe { payload.add(stride * entry_id) } + } else { + unsafe { ptr::read_unaligned(payload.add(stride * entry_id).cast::<*mut u8>()) } + } +} + +pub(super) unsafe fn read_u8(address: *const u8) -> u8 { + unsafe { ptr::read_unaligned(address) } +} + +pub(super) unsafe fn read_u16(address: *const u8) -> u16 { + unsafe { ptr::read_unaligned(address.cast::()) } +} + +pub(super) unsafe fn read_u32(address: *const u8) -> u32 { + unsafe { ptr::read_unaligned(address.cast::()) } +} + +pub(super) unsafe fn read_i32(address: *const u8) -> i32 { + unsafe { ptr::read_unaligned(address.cast::()) } +} + +pub(super) unsafe fn read_f32(address: *const u8) -> f32 { + unsafe { ptr::read_unaligned(address.cast::()) } +} + +pub(super) unsafe fn read_ptr(address: *const u8) -> *mut u8 { + unsafe { ptr::read_unaligned(address.cast::<*mut u8>()) } +} + +pub(super) unsafe fn read_c_string(address: *const u8, max_len: usize) -> String { + let mut len = 0; + while len < max_len { + let byte = unsafe { read_u8(address.add(len)) }; + if byte == 0 { + break; + } + len += 1; + } + String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(address, len) }).into_owned() +} diff --git a/crates/rrt-hook/src/windows/mod.rs b/crates/rrt-hook/src/windows/mod.rs new file mode 100644 index 0000000..ffd9d2e --- /dev/null +++ b/crates/rrt-hook/src/windows/mod.rs @@ -0,0 +1,71 @@ +use crate::capture::{ + CargoCollectionProbe, CargoCollectionProbeRow, IndexedCollectionProbe, + IndexedCollectionProbeRow, sample_finance_snapshot, write_cargo_collection_probe, + write_finance_snapshot_bundle, write_finance_snapshot_only, write_indexed_collection_probe, +}; +use core::ffi::{c_char, c_void}; +use core::mem; +use core::ptr; +use std::env; +use std::fmt::Write as _; +use std::path::PathBuf; +use std::sync::OnceLock; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::thread; +use std::time::Duration; + +use rrt_model::finance::{ + AnnualFinancePolicy, BondPosition, CompanyFinanceState, FinanceSnapshot, GrowthSetting, +}; + +mod auto_load; +mod capture; +mod constants; +mod detours; +mod ffi; +mod install; +mod logging; +mod memory; +mod state; + +use auto_load::*; +use capture::*; +use constants::*; +use detours::*; +use ffi::*; +use install::*; +use logging::*; +use memory::*; +use state::*; + +#[unsafe(no_mangle)] +pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut c_void) -> i32 { + if reason == DLL_PROCESS_ATTACH { + unsafe { + let _ = DisableThreadLibraryCalls(module); + OutputDebugStringA(DEBUG_MESSAGE.as_ptr().cast()); + append_attach_log(); + } + } + 1 +} + +#[unsafe(no_mangle)] +pub extern "system" fn DirectInput8Create( + instance: *mut c_void, + version: u32, + riid: *const Guid, + out: *mut *mut c_void, + outer: *mut c_void, +) -> i32 { + maybe_emit_finance_template_bundle(); + maybe_start_finance_capture_thread(); + maybe_start_cargo_capture_thread(); + maybe_install_auto_load_hook(); + + let direct_input8_create = unsafe { load_direct_input8_create() }; + match direct_input8_create { + Some(callback) => unsafe { callback(instance, version, riid, out, outer) }, + None => E_FAIL, + } +} diff --git a/crates/rrt-hook/src/windows/state.rs b/crates/rrt-hook/src/windows/state.rs new file mode 100644 index 0000000..e50614d --- /dev/null +++ b/crates/rrt-hook/src/windows/state.rs @@ -0,0 +1,45 @@ +use super::*; + +pub(super) static mut REAL_DINPUT8_CREATE: Option = None; +pub(super) static FINANCE_TEMPLATE_EMITTED: AtomicBool = AtomicBool::new(false); +pub(super) static FINANCE_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false); +pub(super) static FINANCE_COLLECTION_PROBE_WRITTEN: AtomicBool = AtomicBool::new(false); +pub(super) static CARGO_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false); +pub(super) static AUTO_LOAD_THREAD_STARTED: AtomicBool = AtomicBool::new(false); +pub(super) static AUTO_LOAD_HOOK_INSTALLED: AtomicBool = AtomicBool::new(false); +pub(super) static AUTO_LOAD_ATTEMPTED: AtomicBool = AtomicBool::new(false); +pub(super) static AUTO_LOAD_IN_PROGRESS: AtomicBool = AtomicBool::new(false); +pub(super) static AUTO_LOAD_DEFERRED: AtomicBool = AtomicBool::new(false); +pub(super) static AUTO_LOAD_TRANSITION_ARMED: AtomicBool = AtomicBool::new(false); +pub(super) static AUTO_LOAD_LAST_GATE_MASK: AtomicU32 = AtomicU32::new(u32::MAX); +pub(super) static AUTO_LOAD_READY_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_ARMED_TICK_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE: AtomicBool = AtomicBool::new(false); +pub(super) static AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_POST_TRANSITION_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_LOAD_SCREEN_MESSAGE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_SERVICE_ENTRY_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_SERVICE_RETURN_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_RUNTIME_PRIME_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_FRAME_CYCLE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_OBJECT_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_CHILD_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0); +pub(super) static AUTO_LOAD_SAVE_STEM: OnceLock = OnceLock::new(); +pub(super) static mut SHELL_STATE_SERVICE_TRAMPOLINE: usize = 0; +pub(super) static mut PROFILE_STARTUP_DISPATCH_TRAMPOLINE: usize = 0; +pub(super) static mut RUNTIME_RESET_TRAMPOLINE: usize = 0; +pub(super) static mut ALLOCATOR_TRAMPOLINE: usize = 0; +pub(super) static mut LOAD_SCREEN_SCALAR_TRAMPOLINE: usize = 0; +pub(super) static mut LOAD_SCREEN_CONSTRUCT_TRAMPOLINE: usize = 0; +pub(super) static mut LOAD_SCREEN_MESSAGE_TRAMPOLINE: usize = 0; +pub(super) static mut RUNTIME_PRIME_TRAMPOLINE: usize = 0; +pub(super) static mut FRAME_CYCLE_TRAMPOLINE: usize = 0; +pub(super) static mut OBJECT_SERVICE_TRAMPOLINE: usize = 0; +pub(super) static mut CHILD_SERVICE_TRAMPOLINE: usize = 0; +pub(super) static mut SHELL_PUBLISH_TRAMPOLINE: usize = 0; +pub(super) static mut SHELL_UNPUBLISH_TRAMPOLINE: usize = 0; +pub(super) static mut SHELL_OBJECT_TEARDOWN_TRAMPOLINE: usize = 0; +pub(super) static mut SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE: usize = 0; +pub(super) static mut SHELL_REMOVE_NODE_TRAMPOLINE: usize = 0; +pub(super) static mut SHELL_NODE_VCALL_TRAMPOLINE: usize = 0; +pub(super) static mut MODE2_TEARDOWN_TRAMPOLINE: usize = 0; diff --git a/crates/rrt-runtime/src/building.rs b/crates/rrt-runtime/src/building.rs deleted file mode 100644 index 5f78bf9..0000000 --- a/crates/rrt-runtime/src/building.rs +++ /dev/null @@ -1,1274 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::fs; -use std::path::Path; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum BuildingTypeSourceKind { - Bca, - Bty, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeSourceFile { - pub file_name: String, - pub raw_stem: String, - pub canonical_stem: String, - pub source_kind: BuildingTypeSourceKind, - #[serde(default)] - pub byte_len: Option, - #[serde(default)] - pub bca_selector_probe: Option, - #[serde(default)] - pub bty_header_probe: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeSourceEntry { - pub canonical_stem: String, - pub raw_stems: Vec, - pub source_kinds: Vec, - pub file_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeBcaSelectorProbe { - pub byte_0xb8: u8, - pub byte_0xb8_hex: String, - pub byte_0xb9: u8, - pub byte_0xb9_hex: String, - pub byte_0xba: u8, - pub byte_0xba_hex: String, - pub byte_0xbb: u8, - pub byte_0xbb_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeBtyHeaderProbe { - pub type_id: u32, - pub type_id_hex: String, - pub name_0x04: String, - pub name_0x22: String, - pub name_0x40: String, - pub name_0x5e: String, - pub name_0x7c: String, - pub name_0x9a: String, - pub byte_0xb8: u8, - pub byte_0xb8_hex: String, - pub byte_0xb9: u8, - pub byte_0xb9_hex: String, - pub byte_0xba: u8, - pub byte_0xba_hex: String, - pub dword_0xbb: u32, - pub dword_0xbb_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeBcaSelectorPatternSummary { - pub byte_len: usize, - pub byte_0xb8_hex: String, - pub byte_0xb9_hex: String, - pub byte_0xba_hex: String, - pub byte_0xbb_hex: String, - pub file_count: usize, - pub sample_file_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeNamedBindingComparison { - pub bindings_path: String, - pub named_binding_count: usize, - pub shared_canonical_stem_count: usize, - pub binding_only_canonical_stems: Vec, - pub source_only_canonical_stems: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeRecoveredTableSummary { - pub recovered_style_themes: Vec, - pub recovered_source_kinds: Vec, - pub present_style_station_entries: Vec, - pub present_standalone_entries: Vec, - pub recovered_source_family_summaries: Vec, - pub bare_port_warehouse_files: Vec, - pub nonzero_bty_header_dword_summaries: Vec, - pub nonzero_bty_header_name_0x40_summaries: Vec, - pub nonzero_bty_header_name_0x5e_summaries: Vec, - pub nonzero_bty_header_name_0x7c_summaries: Vec, - pub nonzero_bty_header_alias_selector_summaries: Vec, - pub bty_header_name_0x5e_dword_summaries: Vec, - pub bty_name_0x5e_bca_selector_summaries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeBtyHeaderDwordSummary { - pub dword_0xbb: u32, - pub dword_0xbb_hex: String, - pub file_count: usize, - pub sample_file_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeRecoveredSourceFamilySummary { - pub canonical_stem: String, - pub sample_raw_stem: String, - pub has_bca_pair: bool, - pub type_id_hex: String, - pub name_0x40: String, - pub name_0x5e: String, - pub name_0x7c: String, - pub dword_0xbb_hex: String, - #[serde(default)] - pub byte_0xba_hex: Option, - #[serde(default)] - pub byte_0xbb_hex: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeBtyHeaderNameSummary { - pub header_offset_hex: String, - pub header_value: String, - pub file_count: usize, - pub sample_file_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeBtyHeaderNameDwordSummary { - pub header_offset_hex: String, - pub header_value: String, - pub dword_0xbb: u32, - pub dword_0xbb_hex: String, - pub file_count: usize, - pub sample_file_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeBtyHeaderAliasSelectorSummary { - pub name_0x40: String, - pub name_0x5e: String, - pub name_0x7c: String, - pub dword_0xbb: u32, - pub dword_0xbb_hex: String, - pub byte_0xb8_hex: String, - pub byte_0xb9_hex: String, - pub byte_0xba_hex: String, - pub byte_0xbb_hex: String, - pub file_count: usize, - pub sample_file_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeBtyNameBcaSelectorSummary { - pub header_offset_hex: String, - pub header_value: String, - pub dword_0xbb: u32, - pub dword_0xbb_hex: String, - pub byte_0xba_hex: String, - pub byte_0xbb_hex: String, - pub file_count: usize, - pub sample_file_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BuildingTypeSourceReport { - pub directory_path: String, - pub bca_file_count: usize, - pub bty_file_count: usize, - pub unique_canonical_stem_count: usize, - pub bca_selector_pattern_count: usize, - #[serde(default)] - pub named_binding_comparison: Option, - pub recovered_table_summary: BuildingTypeRecoveredTableSummary, - pub notes: Vec, - pub bca_selector_patterns: Vec, - pub files: Vec, - pub entries: Vec, -} - -pub fn inspect_building_types_dir( - path: &Path, -) -> Result> { - inspect_building_types_dir_with_bindings(path, None) -} - -pub fn inspect_building_types_dir_with_bindings( - path: &Path, - bindings_path: Option<&Path>, -) -> Result> { - let mut files = Vec::new(); - for entry in fs::read_dir(path)? { - let entry = entry?; - if !entry.file_type()?.is_file() { - continue; - } - let file_name = entry.file_name().to_string_lossy().into_owned(); - let Some(extension) = Path::new(&file_name) - .extension() - .and_then(|extension| extension.to_str()) - .map(|extension| extension.to_ascii_lowercase()) - else { - continue; - }; - let source_kind = match extension.as_str() { - "bca" => BuildingTypeSourceKind::Bca, - "bty" => BuildingTypeSourceKind::Bty, - _ => continue, - }; - let bytes = fs::read(entry.path())?; - let raw_stem = Path::new(&file_name) - .file_stem() - .and_then(|stem| stem.to_str()) - .unwrap_or("") - .to_string(); - if raw_stem.is_empty() { - continue; - } - files.push(BuildingTypeSourceFile { - file_name, - canonical_stem: canonicalize_building_stem(&raw_stem), - raw_stem, - source_kind: source_kind.clone(), - byte_len: Some(bytes.len()), - bca_selector_probe: match source_kind { - BuildingTypeSourceKind::Bca => Some(probe_bca_selector_bytes(&bytes)), - BuildingTypeSourceKind::Bty => None, - }, - bty_header_probe: match source_kind { - BuildingTypeSourceKind::Bca => None, - BuildingTypeSourceKind::Bty => Some(probe_bty_header(&bytes)), - }, - }); - } - - files.sort_by(|left, right| { - left.canonical_stem - .cmp(&right.canonical_stem) - .then_with(|| left.file_name.cmp(&right.file_name)) - }); - - let mut grouped = BTreeMap::>::new(); - for file in &files { - grouped - .entry(file.canonical_stem.clone()) - .or_default() - .push(file); - } - - let entries = grouped - .into_iter() - .map(|(canonical_stem, group)| BuildingTypeSourceEntry { - canonical_stem, - raw_stems: group - .iter() - .map(|file| file.raw_stem.clone()) - .collect::>() - .into_iter() - .collect(), - source_kinds: group - .iter() - .map(|file| file.source_kind.clone()) - .collect::>() - .into_iter() - .collect(), - file_names: group - .iter() - .map(|file| file.file_name.clone()) - .collect::>() - .into_iter() - .collect(), - }) - .collect::>(); - - let bca_file_count = files - .iter() - .filter(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bca)) - .count(); - let bty_file_count = files - .iter() - .filter(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bty)) - .count(); - let mut grouped_selector_patterns = - BTreeMap::<(usize, String, String, String, String), Vec>::new(); - for file in &files { - let Some(probe) = &file.bca_selector_probe else { - continue; - }; - grouped_selector_patterns - .entry(( - file.byte_len.unwrap_or_default(), - probe.byte_0xb8_hex.clone(), - probe.byte_0xb9_hex.clone(), - probe.byte_0xba_hex.clone(), - probe.byte_0xbb_hex.clone(), - )) - .or_default() - .push(file.file_name.clone()); - } - let bca_selector_patterns = grouped_selector_patterns - .into_iter() - .map( - |( - (byte_len, byte_0xb8_hex, byte_0xb9_hex, byte_0xba_hex, byte_0xbb_hex), - file_names, - )| BuildingTypeBcaSelectorPatternSummary { - byte_len, - byte_0xb8_hex, - byte_0xb9_hex, - byte_0xba_hex, - byte_0xbb_hex, - file_count: file_names.len(), - sample_file_names: file_names.into_iter().take(12).collect(), - }, - ) - .collect::>(); - - let notes = vec![ - "BuildingTypes sources are grouped by a canonical stem that lowercases and strips spaces, underscores, and hyphens so paired .bca/.bty variants collapse onto one asset token.".to_string(), - "This report is an offline asset-pool view only; it does not by itself assign live candidate ids or prove scenario candidate-table availability.".to_string(), - "For .bca files, the report also exposes the narrow selector-byte window at offsets 0xb8..0xbb used by the grounded aux-candidate and live-candidate stream decoders.".to_string(), - "The recovered stock table above the Tier-2 building seam combines one style/theme subset with one source-kind table; this report now surfaces the matching on-disk filename families directly.".to_string(), - ]; - - let named_binding_comparison = if let Some(bindings_path) = bindings_path { - Some(load_named_binding_comparison(bindings_path, &entries)?) - } else { - None - }; - let recovered_table_summary = summarize_recovered_table_families(&entries, &files); - - Ok(BuildingTypeSourceReport { - directory_path: path.display().to_string(), - bca_file_count, - bty_file_count, - unique_canonical_stem_count: entries.len(), - bca_selector_pattern_count: bca_selector_patterns.len(), - named_binding_comparison, - recovered_table_summary, - notes, - bca_selector_patterns, - files, - entries, - }) -} - -fn probe_bca_selector_bytes(bytes: &[u8]) -> BuildingTypeBcaSelectorProbe { - let byte_0xb8 = bytes.get(0xb8).copied().unwrap_or(0); - let byte_0xb9 = bytes.get(0xb9).copied().unwrap_or(0); - let byte_0xba = bytes.get(0xba).copied().unwrap_or(0); - let byte_0xbb = bytes.get(0xbb).copied().unwrap_or(0); - BuildingTypeBcaSelectorProbe { - byte_0xb8, - byte_0xb8_hex: format!("0x{byte_0xb8:02x}"), - byte_0xb9, - byte_0xb9_hex: format!("0x{byte_0xb9:02x}"), - byte_0xba, - byte_0xba_hex: format!("0x{byte_0xba:02x}"), - byte_0xbb, - byte_0xbb_hex: format!("0x{byte_0xbb:02x}"), - } -} - -fn probe_bty_header(bytes: &[u8]) -> BuildingTypeBtyHeaderProbe { - let type_id = read_u32_le(bytes, 0x00); - let byte_0xb8 = bytes.get(0xb8).copied().unwrap_or(0); - let byte_0xb9 = bytes.get(0xb9).copied().unwrap_or(0); - let byte_0xba = bytes.get(0xba).copied().unwrap_or(0); - let dword_0xbb = read_u32_le(bytes, 0xbb); - BuildingTypeBtyHeaderProbe { - type_id, - type_id_hex: format!("0x{type_id:08x}"), - name_0x04: read_c_string(bytes, 0x04, 0x1e), - name_0x22: read_c_string(bytes, 0x22, 0x1e), - name_0x40: read_c_string(bytes, 0x40, 0x1e), - name_0x5e: read_c_string(bytes, 0x5e, 0x1e), - name_0x7c: read_c_string(bytes, 0x7c, 0x1e), - name_0x9a: read_c_string(bytes, 0x9a, 0x1e), - byte_0xb8, - byte_0xb8_hex: format!("0x{byte_0xb8:02x}"), - byte_0xb9, - byte_0xb9_hex: format!("0x{byte_0xb9:02x}"), - byte_0xba, - byte_0xba_hex: format!("0x{byte_0xba:02x}"), - dword_0xbb, - dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), - } -} - -fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { - bytes - .get(offset..offset + 4) - .and_then(|slice| <[u8; 4]>::try_from(slice).ok()) - .map(u32::from_le_bytes) - .unwrap_or(0) -} - -fn read_c_string(bytes: &[u8], offset: usize, max_len: usize) -> String { - let Some(slice) = bytes.get(offset..offset.saturating_add(max_len)) else { - return String::new(); - }; - let end = slice - .iter() - .position(|byte| *byte == 0) - .unwrap_or(slice.len()); - String::from_utf8_lossy(&slice[..end]).into_owned() -} - -fn load_named_binding_comparison( - bindings_path: &Path, - entries: &[BuildingTypeSourceEntry], -) -> Result> { - let artifact = - serde_json::from_str::(&fs::read_to_string(bindings_path)?)?; - let named_binding_stems = artifact - .bindings - .into_iter() - .filter_map(|binding| binding.candidate_name) - .map(|candidate_name| canonicalize_building_stem(&candidate_name)) - .collect::>(); - let source_stems = entries - .iter() - .map(|entry| entry.canonical_stem.clone()) - .collect::>(); - - Ok(BuildingTypeNamedBindingComparison { - bindings_path: bindings_path.display().to_string(), - named_binding_count: named_binding_stems.len(), - shared_canonical_stem_count: named_binding_stems.intersection(&source_stems).count(), - binding_only_canonical_stems: named_binding_stems - .difference(&source_stems) - .cloned() - .collect(), - source_only_canonical_stems: source_stems - .difference(&named_binding_stems) - .cloned() - .collect(), - }) -} - -fn canonicalize_building_stem(stem: &str) -> String { - stem.chars() - .filter(|ch| !matches!(ch, ' ' | '_' | '-')) - .flat_map(|ch| ch.to_lowercase()) - .collect() -} - -fn summarize_recovered_table_families( - entries: &[BuildingTypeSourceEntry], - files: &[BuildingTypeSourceFile], -) -> BuildingTypeRecoveredTableSummary { - const RECOVERED_STYLE_THEMES: [&str; 6] = - ["Victorian", "Tudor", "SoWest", "Persian", "Kyoto", "ClpBrd"]; - const RECOVERED_SOURCE_KINDS: [&str; 5] = [ - "StationSml", - "StationMed", - "StationLrg", - "ServiceTower", - "Maintenance", - ]; - - let entry_by_canonical = entries - .iter() - .map(|entry| (entry.canonical_stem.clone(), entry)) - .collect::>(); - - let mut present_style_station_entries = Vec::new(); - for style in RECOVERED_STYLE_THEMES { - for source_kind in ["StationSml", "StationMed", "StationLrg"] { - let canonical = canonicalize_building_stem(&format!("{style}{source_kind}")); - if let Some(entry) = entry_by_canonical.get(&canonical) { - if let Some(raw_stem) = entry.raw_stems.first() { - present_style_station_entries.push(raw_stem.clone()); - } - } - } - } - present_style_station_entries.sort(); - present_style_station_entries.dedup(); - - let mut present_standalone_entries = Vec::new(); - for raw_name in ["ServiceTower", "Maintenance"] { - let canonical = canonicalize_building_stem(raw_name); - if let Some(entry) = entry_by_canonical.get(&canonical) { - if let Some(raw_stem) = entry.raw_stems.first() { - present_standalone_entries.push(raw_stem.clone()); - } - } - } - present_standalone_entries.sort(); - present_standalone_entries.dedup(); - - let recovered_source_family_summaries = summarize_recovered_source_family_rows( - entries, - files, - &present_style_station_entries, - &present_standalone_entries, - ); - - let mut bare_port_warehouse_files = files - .iter() - .filter(|file| matches!(file.canonical_stem.as_str(), "port" | "warehouse")) - .map(|file| file.file_name.clone()) - .collect::>(); - bare_port_warehouse_files.sort(); - bare_port_warehouse_files.dedup(); - - let mut nonzero_bty_header_dword_groups = BTreeMap::>::new(); - for file in files { - let Some(probe) = &file.bty_header_probe else { - continue; - }; - if probe.dword_0xbb == 0 { - continue; - } - nonzero_bty_header_dword_groups - .entry(probe.dword_0xbb) - .or_default() - .push(file.file_name.clone()); - } - let nonzero_bty_header_dword_summaries = nonzero_bty_header_dword_groups - .into_iter() - .map(|(dword_0xbb, mut file_names)| { - file_names.sort(); - file_names.dedup(); - BuildingTypeBtyHeaderDwordSummary { - dword_0xbb, - dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), - file_count: file_names.len(), - sample_file_names: file_names.into_iter().take(24).collect(), - } - }) - .collect(); - - let nonzero_bty_header_name_0x40_summaries = - summarize_nonzero_bty_header_name_lane(files, 0x40, |probe| &probe.name_0x40); - let nonzero_bty_header_name_0x5e_summaries = - summarize_nonzero_bty_header_name_lane(files, 0x5e, |probe| &probe.name_0x5e); - let nonzero_bty_header_name_0x7c_summaries = - summarize_nonzero_bty_header_name_lane(files, 0x7c, |probe| &probe.name_0x7c); - let nonzero_bty_header_alias_selector_summaries = - summarize_nonzero_bty_header_alias_selector_patterns(entries, files); - let bty_header_name_0x5e_dword_summaries = - summarize_bty_header_name_lane_by_dword(files, 0x5e, |probe| &probe.name_0x5e); - let bty_name_0x5e_bca_selector_summaries = - summarize_bty_name_0x5e_bca_selector_patterns(entries, files); - - BuildingTypeRecoveredTableSummary { - recovered_style_themes: RECOVERED_STYLE_THEMES - .into_iter() - .map(str::to_string) - .collect(), - recovered_source_kinds: RECOVERED_SOURCE_KINDS - .into_iter() - .map(str::to_string) - .collect(), - present_style_station_entries, - present_standalone_entries, - recovered_source_family_summaries, - bare_port_warehouse_files, - nonzero_bty_header_dword_summaries, - nonzero_bty_header_name_0x40_summaries, - nonzero_bty_header_name_0x5e_summaries, - nonzero_bty_header_name_0x7c_summaries, - nonzero_bty_header_alias_selector_summaries, - bty_header_name_0x5e_dword_summaries, - bty_name_0x5e_bca_selector_summaries, - } -} - -fn summarize_nonzero_bty_header_name_lane( - files: &[BuildingTypeSourceFile], - offset: u32, - selector: impl Fn(&BuildingTypeBtyHeaderProbe) -> &String, -) -> Vec { - let mut groups = BTreeMap::>::new(); - for file in files { - let Some(probe) = &file.bty_header_probe else { - continue; - }; - if probe.dword_0xbb == 0 { - continue; - } - let header_value = selector(probe).trim(); - if header_value.is_empty() { - continue; - } - groups - .entry(header_value.to_string()) - .or_default() - .push(file.file_name.clone()); - } - - let mut summaries = groups - .into_iter() - .map(|(header_value, mut file_names)| { - file_names.sort(); - file_names.dedup(); - BuildingTypeBtyHeaderNameSummary { - header_offset_hex: format!("0x{offset:02x}"), - header_value, - file_count: file_names.len(), - sample_file_names: file_names.into_iter().take(24).collect(), - } - }) - .collect::>(); - summaries.sort_by(|left, right| { - right - .file_count - .cmp(&left.file_count) - .then_with(|| left.header_offset_hex.cmp(&right.header_offset_hex)) - .then_with(|| left.header_value.cmp(&right.header_value)) - }); - summaries -} - -fn summarize_recovered_source_family_rows( - entries: &[BuildingTypeSourceEntry], - files: &[BuildingTypeSourceFile], - present_style_station_entries: &[String], - present_standalone_entries: &[String], -) -> Vec { - let file_by_name = files - .iter() - .map(|file| (file.file_name.as_str(), file)) - .collect::>(); - - let mut canonical_stems = present_style_station_entries - .iter() - .chain(present_standalone_entries.iter()) - .map(|raw_stem| canonicalize_building_stem(raw_stem)) - .collect::>(); - canonical_stems.sort(); - canonical_stems.dedup(); - - let mut summaries = Vec::new(); - for canonical_stem in canonical_stems { - let Some(entry) = entries - .iter() - .find(|entry| entry.canonical_stem == canonical_stem) - else { - continue; - }; - let bty_file = entry - .file_names - .iter() - .filter_map(|name| file_by_name.get(name.as_str())) - .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bty)); - let Some(bty_file) = bty_file else { - continue; - }; - let Some(bty_probe) = &bty_file.bty_header_probe else { - continue; - }; - let bca_file = entry - .file_names - .iter() - .filter_map(|name| file_by_name.get(name.as_str())) - .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bca)); - let bca_probe = bca_file.and_then(|file| file.bca_selector_probe.as_ref()); - summaries.push(BuildingTypeRecoveredSourceFamilySummary { - canonical_stem: canonical_stem.clone(), - sample_raw_stem: entry - .raw_stems - .first() - .cloned() - .unwrap_or_else(|| canonical_stem.clone()), - has_bca_pair: bca_file.is_some(), - type_id_hex: bty_probe.type_id_hex.clone(), - name_0x40: bty_probe.name_0x40.clone(), - name_0x5e: bty_probe.name_0x5e.clone(), - name_0x7c: bty_probe.name_0x7c.clone(), - dword_0xbb_hex: bty_probe.dword_0xbb_hex.clone(), - byte_0xba_hex: bca_probe.map(|probe| probe.byte_0xba_hex.clone()), - byte_0xbb_hex: bca_probe.map(|probe| probe.byte_0xbb_hex.clone()), - }); - } - summaries.sort_by(|left, right| { - left.canonical_stem - .cmp(&right.canonical_stem) - .then_with(|| left.sample_raw_stem.cmp(&right.sample_raw_stem)) - }); - summaries -} - -fn summarize_bty_header_name_lane_by_dword( - files: &[BuildingTypeSourceFile], - offset: u32, - selector: impl Fn(&BuildingTypeBtyHeaderProbe) -> &String, -) -> Vec { - let mut groups = BTreeMap::<(String, u32), Vec>::new(); - for file in files { - let Some(probe) = &file.bty_header_probe else { - continue; - }; - let header_value = selector(probe).trim(); - if header_value.is_empty() { - continue; - } - groups - .entry((header_value.to_string(), probe.dword_0xbb)) - .or_default() - .push(file.file_name.clone()); - } - - let mut summaries = groups - .into_iter() - .map(|((header_value, dword_0xbb), mut file_names)| { - file_names.sort(); - file_names.dedup(); - BuildingTypeBtyHeaderNameDwordSummary { - header_offset_hex: format!("0x{offset:02x}"), - header_value, - dword_0xbb, - dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), - file_count: file_names.len(), - sample_file_names: file_names.into_iter().take(24).collect(), - } - }) - .collect::>(); - summaries.sort_by(|left, right| { - right - .file_count - .cmp(&left.file_count) - .then_with(|| left.dword_0xbb.cmp(&right.dword_0xbb)) - .then_with(|| left.header_offset_hex.cmp(&right.header_offset_hex)) - .then_with(|| left.header_value.cmp(&right.header_value)) - }); - summaries -} - -fn summarize_bty_name_0x5e_bca_selector_patterns( - entries: &[BuildingTypeSourceEntry], - files: &[BuildingTypeSourceFile], -) -> Vec { - let file_by_name = files - .iter() - .map(|file| (file.file_name.as_str(), file)) - .collect::>(); - let mut groups = BTreeMap::<(String, u32, String, String), Vec>::new(); - - for entry in entries { - let bty_file = entry - .file_names - .iter() - .filter_map(|name| file_by_name.get(name.as_str())) - .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bty)); - let bca_file = entry - .file_names - .iter() - .filter_map(|name| file_by_name.get(name.as_str())) - .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bca)); - let (Some(bty_file), Some(bca_file)) = (bty_file, bca_file) else { - continue; - }; - let (Some(bty_probe), Some(bca_probe)) = - (&bty_file.bty_header_probe, &bca_file.bca_selector_probe) - else { - continue; - }; - let header_value = bty_probe.name_0x5e.trim(); - if header_value.is_empty() { - continue; - } - groups - .entry(( - header_value.to_string(), - bty_probe.dword_0xbb, - bca_probe.byte_0xba_hex.clone(), - bca_probe.byte_0xbb_hex.clone(), - )) - .or_default() - .push(bty_file.file_name.clone()); - } - - let mut summaries = groups - .into_iter() - .map( - |((header_value, dword_0xbb, byte_0xba_hex, byte_0xbb_hex), mut file_names)| { - file_names.sort(); - file_names.dedup(); - BuildingTypeBtyNameBcaSelectorSummary { - header_offset_hex: "0x5e".to_string(), - header_value, - dword_0xbb, - dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), - byte_0xba_hex, - byte_0xbb_hex, - file_count: file_names.len(), - sample_file_names: file_names.into_iter().take(24).collect(), - } - }, - ) - .collect::>(); - summaries.sort_by(|left, right| { - right - .file_count - .cmp(&left.file_count) - .then_with(|| left.dword_0xbb.cmp(&right.dword_0xbb)) - .then_with(|| left.header_value.cmp(&right.header_value)) - .then_with(|| left.byte_0xba_hex.cmp(&right.byte_0xba_hex)) - .then_with(|| left.byte_0xbb_hex.cmp(&right.byte_0xbb_hex)) - }); - summaries -} - -fn summarize_nonzero_bty_header_alias_selector_patterns( - entries: &[BuildingTypeSourceEntry], - files: &[BuildingTypeSourceFile], -) -> Vec { - let file_by_name = files - .iter() - .map(|file| (file.file_name.as_str(), file)) - .collect::>(); - let mut groups = BTreeMap::< - (String, String, String, u32, String, String, String, String), - Vec, - >::new(); - - for entry in entries { - let bty_file = entry - .file_names - .iter() - .filter_map(|name| file_by_name.get(name.as_str())) - .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bty)); - let bca_file = entry - .file_names - .iter() - .filter_map(|name| file_by_name.get(name.as_str())) - .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bca)); - let (Some(bty_file), Some(bca_file)) = (bty_file, bca_file) else { - continue; - }; - let (Some(bty_probe), Some(bca_probe)) = - (&bty_file.bty_header_probe, &bca_file.bca_selector_probe) - else { - continue; - }; - if bty_probe.dword_0xbb == 0 { - continue; - } - let name_0x40 = bty_probe.name_0x40.trim(); - let name_0x5e = bty_probe.name_0x5e.trim(); - let name_0x7c = bty_probe.name_0x7c.trim(); - if name_0x40.is_empty() || name_0x5e.is_empty() || name_0x7c.is_empty() { - continue; - } - groups - .entry(( - name_0x40.to_string(), - name_0x5e.to_string(), - name_0x7c.to_string(), - bty_probe.dword_0xbb, - bca_probe.byte_0xb8_hex.clone(), - bca_probe.byte_0xb9_hex.clone(), - bca_probe.byte_0xba_hex.clone(), - bca_probe.byte_0xbb_hex.clone(), - )) - .or_default() - .push(bty_file.file_name.clone()); - } - - let mut summaries = groups - .into_iter() - .map( - |( - ( - name_0x40, - name_0x5e, - name_0x7c, - dword_0xbb, - byte_0xb8_hex, - byte_0xb9_hex, - byte_0xba_hex, - byte_0xbb_hex, - ), - mut file_names, - )| { - file_names.sort(); - file_names.dedup(); - BuildingTypeBtyHeaderAliasSelectorSummary { - name_0x40, - name_0x5e, - name_0x7c, - dword_0xbb, - dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), - byte_0xb8_hex, - byte_0xb9_hex, - byte_0xba_hex, - byte_0xbb_hex, - file_count: file_names.len(), - sample_file_names: file_names.into_iter().take(24).collect(), - } - }, - ) - .collect::>(); - summaries.sort_by(|left, right| { - right - .file_count - .cmp(&left.file_count) - .then_with(|| left.dword_0xbb.cmp(&right.dword_0xbb)) - .then_with(|| left.name_0x40.cmp(&right.name_0x40)) - .then_with(|| left.name_0x5e.cmp(&right.name_0x5e)) - .then_with(|| left.name_0x7c.cmp(&right.name_0x7c)) - .then_with(|| left.byte_0xb8_hex.cmp(&right.byte_0xb8_hex)) - .then_with(|| left.byte_0xb9_hex.cmp(&right.byte_0xb9_hex)) - .then_with(|| left.byte_0xba_hex.cmp(&right.byte_0xba_hex)) - .then_with(|| left.byte_0xbb_hex.cmp(&right.byte_0xbb_hex)) - }); - summaries -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct BuildingBindingArtifact { - bindings: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct BuildingBindingRow { - #[serde(default)] - candidate_name: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn probes_bca_selector_bytes_from_fixed_offsets() { - let mut bytes = vec![0u8; 0xbc + 1]; - bytes[0xb8] = 0x12; - bytes[0xb9] = 0x34; - bytes[0xba] = 0x56; - bytes[0xbb] = 0x78; - let probe = probe_bca_selector_bytes(&bytes); - assert_eq!(probe.byte_0xb8, 0x12); - assert_eq!(probe.byte_0xb9, 0x34); - assert_eq!(probe.byte_0xba, 0x56); - assert_eq!(probe.byte_0xbb, 0x78); - assert_eq!(probe.byte_0xb8_hex, "0x12"); - assert_eq!(probe.byte_0xbb_hex, "0x78"); - } - - #[test] - fn probes_bty_header_from_fixed_offsets() { - let mut bytes = vec![0u8; 0xc0]; - bytes[0x00..0x04].copy_from_slice(&0x03ebu32.to_le_bytes()); - bytes[0x04..0x04 + 5].copy_from_slice(b"Port\0"); - bytes[0x22..0x22 + 7].copy_from_slice(b"Cargo\0\0"); - bytes[0x40..0x40 + 6].copy_from_slice(b"Dock\0\0"); - bytes[0x5e..0x5e + 5].copy_from_slice(b"Sea\0\0"); - bytes[0x7c..0x7c + 6].copy_from_slice(b"Coast\0"); - bytes[0x9a..0x9a + 5].copy_from_slice(b"Port\0"); - bytes[0xb8] = 0x12; - bytes[0xb9] = 0x34; - bytes[0xba] = 0x56; - bytes[0xbb..0xbf].copy_from_slice(&0x89abcdefu32.to_le_bytes()); - - let probe = probe_bty_header(&bytes); - assert_eq!(probe.type_id, 0x03eb); - assert_eq!(probe.type_id_hex, "0x000003eb"); - assert_eq!(probe.name_0x04, "Port"); - assert_eq!(probe.name_0x22, "Cargo"); - assert_eq!(probe.name_0x40, "Dock"); - assert_eq!(probe.name_0x5e, "Sea"); - assert_eq!(probe.name_0x7c, "Coast"); - assert_eq!(probe.name_0x9a, "Port"); - assert_eq!(probe.byte_0xb8_hex, "0x12"); - assert_eq!(probe.byte_0xb9_hex, "0x34"); - assert_eq!(probe.byte_0xba_hex, "0x56"); - assert_eq!(probe.dword_0xbb_hex, "0x89abcdef"); - } - - #[test] - fn summarizes_recovered_table_families_from_entries_and_files() { - let entries = vec![ - BuildingTypeSourceEntry { - canonical_stem: canonicalize_building_stem("VictorianStationSml"), - raw_stems: vec!["VictorianStationSml".to_string()], - source_kinds: vec![BuildingTypeSourceKind::Bty], - file_names: vec!["VictorianStationSml.bty".to_string()], - }, - BuildingTypeSourceEntry { - canonical_stem: canonicalize_building_stem("ClpBrdStationLrg"), - raw_stems: vec!["ClpbrdStationLrg".to_string()], - source_kinds: vec![BuildingTypeSourceKind::Bty], - file_names: vec!["ClpbrdStationLrg.bty".to_string()], - }, - BuildingTypeSourceEntry { - canonical_stem: canonicalize_building_stem("Maintenance"), - raw_stems: vec!["Maintenance".to_string()], - source_kinds: vec![BuildingTypeSourceKind::Bty], - file_names: vec!["Maintenance.bty".to_string()], - }, - BuildingTypeSourceEntry { - canonical_stem: canonicalize_building_stem("ServiceTower"), - raw_stems: vec!["ServiceTower".to_string()], - source_kinds: vec![BuildingTypeSourceKind::Bty], - file_names: vec!["ServiceTower.bty".to_string()], - }, - BuildingTypeSourceEntry { - canonical_stem: canonicalize_building_stem("Port"), - raw_stems: vec!["Port".to_string()], - source_kinds: vec![BuildingTypeSourceKind::Bca, BuildingTypeSourceKind::Bty], - file_names: vec!["Port.bca".to_string(), "Port.bty".to_string()], - }, - ]; - let files = vec![ - BuildingTypeSourceFile { - file_name: "VictorianStationSml.bty".to_string(), - raw_stem: "VictorianStationSml".to_string(), - canonical_stem: canonicalize_building_stem("VictorianStationSml"), - source_kind: BuildingTypeSourceKind::Bty, - byte_len: None, - bca_selector_probe: None, - bty_header_probe: Some(BuildingTypeBtyHeaderProbe { - type_id: 0x03ec, - type_id_hex: "0x000003ec".to_string(), - name_0x04: "VictorianStationSml".to_string(), - name_0x22: "VictorianStationSml".to_string(), - name_0x40: "VictorianStations".to_string(), - name_0x5e: "SmallTudorHouse".to_string(), - name_0x7c: "VictorianStations".to_string(), - name_0x9a: "VictorianStationSml".to_string(), - byte_0xb8: 0x06, - byte_0xb8_hex: "0x06".to_string(), - byte_0xb9: 0x06, - byte_0xb9_hex: "0x06".to_string(), - byte_0xba: 0x30, - byte_0xba_hex: "0x30".to_string(), - dword_0xbb: 0, - dword_0xbb_hex: "0x00000000".to_string(), - }), - }, - BuildingTypeSourceFile { - file_name: "ClpbrdStationLrg.bty".to_string(), - raw_stem: "ClpbrdStationLrg".to_string(), - canonical_stem: canonicalize_building_stem("ClpbrdStationLrg"), - source_kind: BuildingTypeSourceKind::Bty, - byte_len: None, - bca_selector_probe: None, - bty_header_probe: Some(BuildingTypeBtyHeaderProbe { - type_id: 0x03ec, - type_id_hex: "0x000003ec".to_string(), - name_0x04: "ClpbrdStationLrg".to_string(), - name_0x22: "ClpbrdStationLrg".to_string(), - name_0x40: "ClpBrdStations".to_string(), - name_0x5e: "SmallTudorHouse".to_string(), - name_0x7c: "ClpBrdStations".to_string(), - name_0x9a: "ClpbrdStationLrg".to_string(), - byte_0xb8: 0x06, - byte_0xb8_hex: "0x06".to_string(), - byte_0xb9: 0x06, - byte_0xb9_hex: "0x06".to_string(), - byte_0xba: 0x30, - byte_0xba_hex: "0x30".to_string(), - dword_0xbb: 0, - dword_0xbb_hex: "0x00000000".to_string(), - }), - }, - BuildingTypeSourceFile { - file_name: "Maintenance.bty".to_string(), - raw_stem: "Maintenance".to_string(), - canonical_stem: canonicalize_building_stem("Maintenance"), - source_kind: BuildingTypeSourceKind::Bty, - byte_len: None, - bca_selector_probe: None, - bty_header_probe: Some(BuildingTypeBtyHeaderProbe { - type_id: 0x03ec, - type_id_hex: "0x000003ec".to_string(), - name_0x04: "Maintenance".to_string(), - name_0x22: "Maintenance".to_string(), - name_0x40: "Maintenance Facility".to_string(), - name_0x5e: "200FtRulerCross".to_string(), - name_0x7c: "Maintenance Facility".to_string(), - name_0x9a: "Maintenance".to_string(), - byte_0xb8: 0x06, - byte_0xb8_hex: "0x06".to_string(), - byte_0xb9: 0x06, - byte_0xb9_hex: "0x06".to_string(), - byte_0xba: 0x30, - byte_0xba_hex: "0x30".to_string(), - dword_0xbb: 0, - dword_0xbb_hex: "0x00000000".to_string(), - }), - }, - BuildingTypeSourceFile { - file_name: "ServiceTower.bty".to_string(), - raw_stem: "ServiceTower".to_string(), - canonical_stem: canonicalize_building_stem("ServiceTower"), - source_kind: BuildingTypeSourceKind::Bty, - byte_len: None, - bca_selector_probe: None, - bty_header_probe: Some(BuildingTypeBtyHeaderProbe { - type_id: 0x03ec, - type_id_hex: "0x000003ec".to_string(), - name_0x04: "ServiceTower".to_string(), - name_0x22: "ServiceTower".to_string(), - name_0x40: "Service Tower".to_string(), - name_0x5e: "200FtRulerCross".to_string(), - name_0x7c: "Service Tower".to_string(), - name_0x9a: "ServiceTower".to_string(), - byte_0xb8: 0x06, - byte_0xb8_hex: "0x06".to_string(), - byte_0xb9: 0x06, - byte_0xb9_hex: "0x06".to_string(), - byte_0xba: 0x30, - byte_0xba_hex: "0x30".to_string(), - dword_0xbb: 0, - dword_0xbb_hex: "0x00000000".to_string(), - }), - }, - BuildingTypeSourceFile { - file_name: "Port.bty".to_string(), - raw_stem: "Port".to_string(), - canonical_stem: canonicalize_building_stem("Port"), - source_kind: BuildingTypeSourceKind::Bty, - byte_len: None, - bca_selector_probe: None, - bty_header_probe: Some(BuildingTypeBtyHeaderProbe { - type_id: 0x03ec, - type_id_hex: "0x000003ec".to_string(), - name_0x04: "Port".to_string(), - name_0x22: "Port".to_string(), - name_0x40: "Port".to_string(), - name_0x5e: "TextileMill".to_string(), - name_0x7c: "Port".to_string(), - name_0x9a: "Port".to_string(), - byte_0xb8: 0x06, - byte_0xb8_hex: "0x06".to_string(), - byte_0xb9: 0x06, - byte_0xb9_hex: "0x06".to_string(), - byte_0xba: 0x30, - byte_0xba_hex: "0x30".to_string(), - dword_0xbb: 0x01f4, - dword_0xbb_hex: "0x000001f4".to_string(), - }), - }, - BuildingTypeSourceFile { - file_name: "Port.bca".to_string(), - raw_stem: "Port".to_string(), - canonical_stem: canonicalize_building_stem("Port"), - source_kind: BuildingTypeSourceKind::Bca, - byte_len: None, - bca_selector_probe: Some(BuildingTypeBcaSelectorProbe { - byte_0xb8: 0x00, - byte_0xb8_hex: "0x00".to_string(), - byte_0xb9: 0x00, - byte_0xb9_hex: "0x00".to_string(), - byte_0xba: 0x00, - byte_0xba_hex: "0x00".to_string(), - byte_0xbb: 0x00, - byte_0xbb_hex: "0x00".to_string(), - }), - bty_header_probe: None, - }, - ]; - - let summary = summarize_recovered_table_families(&entries, &files); - assert!( - summary - .present_style_station_entries - .contains(&"VictorianStationSml".to_string()) - ); - assert!( - summary - .present_style_station_entries - .contains(&"ClpbrdStationLrg".to_string()) - ); - assert_eq!( - summary.present_standalone_entries, - vec!["Maintenance".to_string(), "ServiceTower".to_string()] - ); - assert_eq!(summary.recovered_source_family_summaries.len(), 4); - assert!(summary.recovered_source_family_summaries.iter().any(|row| { - row.canonical_stem == canonicalize_building_stem("Maintenance") - && row.name_0x40 == "Maintenance Facility" - && row.dword_0xbb_hex == "0x00000000" - && row.byte_0xba_hex.is_none() - })); - assert!(summary.recovered_source_family_summaries.iter().any(|row| { - row.canonical_stem == canonicalize_building_stem("VictorianStationSml") - && row.name_0x40 == "VictorianStations" - && row.dword_0xbb_hex == "0x00000000" - })); - assert_eq!( - summary.bare_port_warehouse_files, - vec!["Port.bca".to_string(), "Port.bty".to_string()] - ); - assert_eq!(summary.nonzero_bty_header_dword_summaries.len(), 1); - assert_eq!( - summary.nonzero_bty_header_dword_summaries[0].dword_0xbb_hex, - "0x000001f4" - ); - assert_eq!( - summary.nonzero_bty_header_dword_summaries[0].sample_file_names, - vec!["Port.bty".to_string()] - ); - assert_eq!( - summary.nonzero_bty_header_name_0x40_summaries, - vec![BuildingTypeBtyHeaderNameSummary { - header_offset_hex: "0x40".to_string(), - header_value: "Port".to_string(), - file_count: 1, - sample_file_names: vec!["Port.bty".to_string()], - }] - ); - assert_eq!( - summary.nonzero_bty_header_name_0x5e_summaries, - vec![BuildingTypeBtyHeaderNameSummary { - header_offset_hex: "0x5e".to_string(), - header_value: "TextileMill".to_string(), - file_count: 1, - sample_file_names: vec!["Port.bty".to_string()], - }] - ); - assert_eq!( - summary.nonzero_bty_header_name_0x7c_summaries, - vec![BuildingTypeBtyHeaderNameSummary { - header_offset_hex: "0x7c".to_string(), - header_value: "Port".to_string(), - file_count: 1, - sample_file_names: vec!["Port.bty".to_string()], - }] - ); - assert!( - summary.bty_header_name_0x5e_dword_summaries.iter().any(|row| { - row.header_offset_hex == "0x5e" - && row.header_value == "TextileMill" - && row.dword_0xbb == 0x01f4 - && row.file_count == 1 - && row.sample_file_names == vec!["Port.bty".to_string()] - }) - ); - assert_eq!( - summary.nonzero_bty_header_alias_selector_summaries, - vec![BuildingTypeBtyHeaderAliasSelectorSummary { - name_0x40: "Port".to_string(), - name_0x5e: "TextileMill".to_string(), - name_0x7c: "Port".to_string(), - dword_0xbb: 0x01f4, - dword_0xbb_hex: "0x000001f4".to_string(), - byte_0xb8_hex: "0x00".to_string(), - byte_0xb9_hex: "0x00".to_string(), - byte_0xba_hex: "0x00".to_string(), - byte_0xbb_hex: "0x00".to_string(), - file_count: 1, - sample_file_names: vec!["Port.bty".to_string()], - }] - ); - assert_eq!( - summary.bty_name_0x5e_bca_selector_summaries, - vec![BuildingTypeBtyNameBcaSelectorSummary { - header_offset_hex: "0x5e".to_string(), - header_value: "TextileMill".to_string(), - dword_0xbb: 0x01f4, - dword_0xbb_hex: "0x000001f4".to_string(), - byte_0xba_hex: "0x00".to_string(), - byte_0xbb_hex: "0x00".to_string(), - file_count: 1, - sample_file_names: vec!["Port.bty".to_string()], - }] - ); - } -} diff --git a/crates/rrt-runtime/src/derived/company.rs b/crates/rrt-runtime/src/derived/company.rs new file mode 100644 index 0000000..18f34c3 --- /dev/null +++ b/crates/rrt-runtime/src/derived/company.rs @@ -0,0 +1,320 @@ +use std::collections::BTreeMap; + +use crate::derived::{ + runtime_company_live_bond_coupon_burden_total, runtime_company_trailing_full_year_stat_series, + runtime_decode_packed_calendar_tuple, runtime_pack_packed_calendar_tuple_to_absolute_counter, + runtime_world_absolute_counter, runtime_world_partial_year_weight_numerator, +}; +use crate::event::metrics::RuntimeCompanyMarketMetric; +use crate::state::{ + RuntimeCompanyAnnualFinanceState, RuntimeCompanyPeriodicServiceState, RuntimeState, + RuntimeWorldRoutePreferenceOverrideState, +}; + +pub fn runtime_company_unassigned_share_pool(state: &RuntimeState, company_id: u32) -> Option { + let outstanding_shares = state + .service_state + .company_market_state + .get(&company_id)? + .outstanding_shares; + let assigned_shares = runtime_company_assigned_share_pool(state, company_id)?; + Some(outstanding_shares.saturating_sub(assigned_shares)) +} + +pub fn runtime_company_assigned_share_pool(state: &RuntimeState, company_id: u32) -> Option { + state.service_state.company_market_state.get(&company_id)?; + Some( + state + .chairman_profiles + .iter() + .filter_map(|profile| profile.company_holdings.get(&company_id).copied()) + .sum::(), + ) +} + +pub fn runtime_company_annual_finance_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + let periodic_service_state = runtime_company_periodic_service_state(state, company_id); + let assigned_share_pool = runtime_company_assigned_share_pool(state, company_id)?; + let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id)?; + let years_since_founding = + derive_runtime_company_elapsed_years(state.calendar.year, market_state.founding_year); + let years_since_last_bankruptcy = derive_runtime_company_elapsed_years( + state.calendar.year, + market_state.last_bankruptcy_year, + ); + let years_since_last_dividend = + derive_runtime_company_elapsed_years(state.calendar.year, market_state.last_dividend_year); + let current_issue_absolute_counter = runtime_pack_packed_calendar_tuple_to_absolute_counter( + runtime_decode_packed_calendar_tuple( + market_state.current_issue_calendar_word, + market_state.current_issue_calendar_word_2, + ), + ); + let prior_issue_absolute_counter = runtime_pack_packed_calendar_tuple_to_absolute_counter( + runtime_decode_packed_calendar_tuple( + market_state.prior_issue_calendar_word, + market_state.prior_issue_calendar_word_2, + ), + ); + let current_issue_age_absolute_counter_delta = match ( + runtime_world_absolute_counter(state), + current_issue_absolute_counter, + ) { + (Some(world_counter), Some(issue_counter)) if world_counter >= issue_counter => { + Some(i64::from(world_counter - issue_counter)) + } + _ => None, + }; + let (trailing_full_year_year_words, trailing_full_year_net_profits) = + runtime_company_trailing_full_year_stat_series(state, company_id, 0x2b, 4) + .unwrap_or_default(); + let (_, trailing_full_year_revenues) = + runtime_company_trailing_full_year_stat_series(state, company_id, 0x2c, 4) + .unwrap_or_default(); + let (_, trailing_full_year_fuel_costs) = + runtime_company_trailing_full_year_stat_series(state, company_id, 0x09, 4) + .unwrap_or_default(); + Some(RuntimeCompanyAnnualFinanceState { + company_id, + outstanding_shares: market_state.outstanding_shares, + bond_count: market_state.bond_count, + largest_live_bond_principal: market_state.largest_live_bond_principal, + highest_coupon_live_bond_principal: market_state.highest_coupon_live_bond_principal, + live_bond_coupon_burden_total: runtime_company_live_bond_coupon_burden_total( + state, company_id, + ), + assigned_share_pool, + unassigned_share_pool, + cached_share_price: rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32), + chairman_salary_baseline: market_state.chairman_salary_baseline, + chairman_salary_current: market_state.chairman_salary_current, + chairman_bonus_year: market_state.chairman_bonus_year, + chairman_bonus_amount: market_state.chairman_bonus_amount, + founding_year: market_state.founding_year, + last_bankruptcy_year: market_state.last_bankruptcy_year, + last_dividend_year: market_state.last_dividend_year, + years_since_founding, + years_since_last_bankruptcy, + years_since_last_dividend, + current_partial_year_weight_numerator: runtime_world_partial_year_weight_numerator(state), + trailing_full_year_year_words, + trailing_full_year_net_profits, + trailing_full_year_revenues, + trailing_full_year_fuel_costs, + current_issue_absolute_counter, + prior_issue_absolute_counter, + current_issue_age_absolute_counter_delta, + current_issue_calendar_word: market_state.current_issue_calendar_word, + current_issue_calendar_word_2: market_state.current_issue_calendar_word_2, + prior_issue_calendar_word: market_state.prior_issue_calendar_word, + prior_issue_calendar_word_2: market_state.prior_issue_calendar_word_2, + preferred_locomotive_engine_type_raw_u8: periodic_service_state + .as_ref() + .and_then(|service_state| service_state.preferred_locomotive_engine_type_raw_u8), + city_connection_latch: periodic_service_state + .as_ref() + .map(|service_state| service_state.city_connection_latch) + .unwrap_or(market_state.city_connection_latch), + linked_transit_latch: periodic_service_state + .as_ref() + .map(|service_state| service_state.linked_transit_latch) + .unwrap_or(market_state.linked_transit_latch), + }) +} + +pub fn runtime_company_periodic_service_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + const DEFAULT_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS: i64 = 140; + const ELECTRIC_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS: i64 = 180; + const ELECTRIC_ENGINE_TYPE_RAW_U8: u8 = 2; + + let market_state = state.service_state.company_market_state.get(&company_id)?; + let periodic_side_latch_state = state + .service_state + .company_periodic_side_latch_state + .get(&company_id); + let preferred_locomotive_engine_type_raw_u8 = periodic_side_latch_state + .and_then(|latch_state| latch_state.preferred_locomotive_engine_type_raw_u8); + let base_route_preference_raw_u8 = state.world_restore.auto_show_grade_during_track_lay_raw_u8; + let electric_route_preference_override_active = + preferred_locomotive_engine_type_raw_u8 == Some(ELECTRIC_ENGINE_TYPE_RAW_U8); + let effective_route_preference_raw_u8 = if electric_route_preference_override_active { + Some(ELECTRIC_ENGINE_TYPE_RAW_U8) + } else { + base_route_preference_raw_u8 + }; + Some(RuntimeCompanyPeriodicServiceState { + company_id, + preferred_locomotive_engine_type_raw_u8, + city_connection_latch: periodic_side_latch_state + .map(|latch_state| latch_state.city_connection_latch) + .unwrap_or(market_state.city_connection_latch), + linked_transit_latch: periodic_side_latch_state + .map(|latch_state| latch_state.linked_transit_latch) + .unwrap_or(market_state.linked_transit_latch), + base_route_preference_raw_u8, + effective_route_preference_raw_u8, + electric_route_preference_override_active, + effective_route_quality_multiplier_basis_points: + if electric_route_preference_override_active { + ELECTRIC_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS + } else { + DEFAULT_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS + }, + }) +} + +pub fn runtime_apply_company_periodic_route_preference_override( + state: &mut RuntimeState, + company_id: u32, +) -> Option { + let periodic_service_state = runtime_company_periodic_service_state(state, company_id)?; + if !periodic_service_state.electric_route_preference_override_active { + return None; + } + let override_state = RuntimeWorldRoutePreferenceOverrideState { + company_id, + base_route_preference_raw_u8: periodic_service_state.base_route_preference_raw_u8, + effective_route_preference_raw_u8: periodic_service_state.effective_route_preference_raw_u8, + electric_route_preference_override_active: true, + }; + state.world_restore.auto_show_grade_during_track_lay_raw_u8 = + override_state.effective_route_preference_raw_u8; + state + .service_state + .active_periodic_route_preference_override = Some(override_state.clone()); + state.service_state.last_periodic_route_preference_override = Some(override_state.clone()); + state + .service_state + .periodic_route_preference_override_apply_count += 1; + Some(override_state) +} + +pub fn runtime_restore_company_periodic_route_preference_override( + state: &mut RuntimeState, +) -> Option { + let override_state = state + .service_state + .active_periodic_route_preference_override + .take()?; + state.world_restore.auto_show_grade_during_track_lay_raw_u8 = + override_state.base_route_preference_raw_u8; + state + .service_state + .periodic_route_preference_override_restore_count += 1; + Some(override_state) +} + +pub fn runtime_company_market_metric_value( + state: &RuntimeState, + company_id: u32, + metric: RuntimeCompanyMarketMetric, +) -> Option { + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + match metric { + RuntimeCompanyMarketMetric::OutstandingShares => { + Some(annual_finance_state.outstanding_shares as i64) + } + RuntimeCompanyMarketMetric::BondCount => Some(annual_finance_state.bond_count as i64), + RuntimeCompanyMarketMetric::LargestLiveBondPrincipal => annual_finance_state + .largest_live_bond_principal + .map(|value| value as i64), + RuntimeCompanyMarketMetric::HighestCouponLiveBondPrincipal => annual_finance_state + .highest_coupon_live_bond_principal + .map(|value| value as i64), + RuntimeCompanyMarketMetric::AssignedSharePool => { + Some(annual_finance_state.assigned_share_pool as i64) + } + RuntimeCompanyMarketMetric::UnassignedSharePool => { + Some(annual_finance_state.unassigned_share_pool as i64) + } + RuntimeCompanyMarketMetric::CachedSharePrice => annual_finance_state.cached_share_price, + RuntimeCompanyMarketMetric::ChairmanSalaryBaseline => { + Some(annual_finance_state.chairman_salary_baseline as i64) + } + RuntimeCompanyMarketMetric::ChairmanSalaryCurrent => { + Some(annual_finance_state.chairman_salary_current as i64) + } + RuntimeCompanyMarketMetric::ChairmanBonusAmount => { + Some(annual_finance_state.chairman_bonus_amount as i64) + } + RuntimeCompanyMarketMetric::CurrentIssueAbsoluteCounter => annual_finance_state + .current_issue_absolute_counter + .map(i64::from), + RuntimeCompanyMarketMetric::PriorIssueAbsoluteCounter => annual_finance_state + .prior_issue_absolute_counter + .map(i64::from), + RuntimeCompanyMarketMetric::CurrentIssueAgeAbsoluteCounterDelta => { + annual_finance_state.current_issue_age_absolute_counter_delta + } + RuntimeCompanyMarketMetric::CurrentIssueCalendarWord => { + Some(annual_finance_state.current_issue_calendar_word as i64) + } + RuntimeCompanyMarketMetric::CurrentIssueCalendarWord2 => { + Some(annual_finance_state.current_issue_calendar_word_2 as i64) + } + RuntimeCompanyMarketMetric::PriorIssueCalendarWord => { + Some(annual_finance_state.prior_issue_calendar_word as i64) + } + RuntimeCompanyMarketMetric::PriorIssueCalendarWord2 => { + Some(annual_finance_state.prior_issue_calendar_word_2 as i64) + } + } +} + +pub(crate) fn rounded_cached_share_price_i64(raw_u32: u32) -> Option { + let value = f32::from_bits(raw_u32); + if !value.is_finite() { + return None; + } + if value < i64::MIN as f32 || value > i64::MAX as f32 { + return None; + } + Some(value.round() as i64) +} + +pub(in crate::derived) fn runtime_decode_saved_f64_bits(bits: u64) -> Option { + let value = f64::from_bits(bits); + if !value.is_finite() { + return None; + } + Some(value) +} + +pub(crate) fn runtime_round_f64_to_i64(value: f64) -> Option { + if !value.is_finite() { + return None; + } + if value < i64::MIN as f64 || value > i64::MAX as f64 { + return None; + } + Some(value.round() as i64) +} + +pub(in crate::derived) fn derive_runtime_company_elapsed_years( + current_year: u32, + prior_year: u32, +) -> Option { + if prior_year == 0 || prior_year > current_year { + return None; + } + Some(current_year - prior_year) +} + +pub(crate) fn derive_runtime_chairman_holdings_share_price_total( + holdings_by_company: &BTreeMap, + company_share_prices: &BTreeMap, +) -> Option { + let mut total = 0i64; + for (company_id, units) in holdings_by_company { + let share_price = *company_share_prices.get(company_id)?; + total = total.checked_add((*units as i64).checked_mul(share_price)?)?; + } + Some(total) +} diff --git a/crates/rrt-runtime/src/derived/finance/annual_policy.rs b/crates/rrt-runtime/src/derived/finance/annual_policy.rs new file mode 100644 index 0000000..3613974 --- /dev/null +++ b/crates/rrt-runtime/src/derived/finance/annual_policy.rs @@ -0,0 +1,138 @@ +use crate::derived::{ + runtime_annual_bond_principal_flow_relation_label, runtime_company_annual_bond_policy_state, + runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_state, + runtime_company_annual_dividend_policy_state, runtime_company_annual_finance_state, + runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state, +}; +use crate::state::{ + RuntimeCompanyAnnualFinancePolicyAction, RuntimeCompanyAnnualFinancePolicyState, RuntimeState, +}; + +pub fn runtime_world_annual_finance_mode_active(state: &RuntimeState) -> Option { + Some(state.world_restore.partial_year_progress_raw_u8? == 0x0c) +} + +pub fn runtime_world_bankruptcy_allowed(state: &RuntimeState) -> Option { + Some(state.world_restore.bankruptcy_policy_raw_u8? == 0) +} + +pub fn runtime_world_bond_issue_and_repayment_allowed(state: &RuntimeState) -> Option { + Some(state.world_restore.bond_issue_and_repayment_policy_raw_u8? == 0) +} + +pub fn runtime_world_stock_issue_and_buyback_allowed(state: &RuntimeState) -> Option { + Some(state.world_restore.stock_issue_and_buyback_policy_raw_u8? == 0) +} + +pub fn runtime_world_dividend_adjustment_allowed(state: &RuntimeState) -> Option { + Some(state.world_restore.dividend_policy_raw_u8? == 0) +} + +pub fn runtime_world_building_density_growth_setting(state: &RuntimeState) -> Option { + state.world_restore.building_density_growth_setting_raw_u32 +} + +pub fn runtime_company_annual_finance_policy_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + runtime_company_annual_finance_state(state, company_id)?; + let creditor_pressure_bankruptcy_eligible = + runtime_company_annual_creditor_pressure_state(state, company_id) + .map(|state| state.eligible_for_bankruptcy_branch) + .unwrap_or(false); + let deep_distress_bankruptcy_fallback_eligible = + runtime_company_annual_deep_distress_state(state, company_id) + .map(|state| state.eligible_for_bankruptcy_fallback) + .unwrap_or(false); + let bond_issue_eligible = runtime_company_annual_bond_policy_state(state, company_id) + .map(|state| state.eligible_for_bond_issue_branch) + .unwrap_or(false); + let stock_repurchase_eligible = + runtime_company_annual_stock_repurchase_state(state, company_id) + .map(|state| state.eligible_for_single_batch_repurchase) + .unwrap_or(false); + let stock_issue_eligible = runtime_company_annual_stock_issue_state(state, company_id) + .map(|state| state.eligible_for_double_tranche_issue) + .unwrap_or(false); + let dividend_adjustment_eligible = + runtime_company_annual_dividend_policy_state(state, company_id) + .map(|state| state.eligible_for_dividend_adjustment_branch) + .unwrap_or(false); + let action = if creditor_pressure_bankruptcy_eligible { + RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy + } else if deep_distress_bankruptcy_fallback_eligible { + RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback + } else if bond_issue_eligible { + RuntimeCompanyAnnualFinancePolicyAction::BondIssue + } else if stock_repurchase_eligible { + RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase + } else if stock_issue_eligible { + RuntimeCompanyAnnualFinancePolicyAction::StockIssue + } else if dividend_adjustment_eligible { + RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment + } else { + RuntimeCompanyAnnualFinancePolicyAction::None + }; + Some(RuntimeCompanyAnnualFinancePolicyState { + company_id, + action, + creditor_pressure_bankruptcy_eligible, + deep_distress_bankruptcy_fallback_eligible, + bond_issue_eligible, + stock_repurchase_eligible, + stock_issue_eligible, + dividend_adjustment_eligible, + }) +} + +pub fn runtime_company_annual_finance_policy_action_label( + action: RuntimeCompanyAnnualFinancePolicyAction, +) -> &'static str { + match action { + RuntimeCompanyAnnualFinancePolicyAction::None => "none", + RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy => { + "creditor_pressure_bankruptcy" + } + RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => { + "deep_distress_bankruptcy_fallback" + } + RuntimeCompanyAnnualFinancePolicyAction::BondIssue => "bond_issue", + RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => "stock_repurchase", + RuntimeCompanyAnnualFinancePolicyAction::StockIssue => "stock_issue", + RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment => "dividend_adjustment", + } +} + +pub fn runtime_annual_finance_news_family_candidate_label( + action: RuntimeCompanyAnnualFinancePolicyAction, + retired_principal_total: u64, + issued_principal_total: u64, + repurchased_share_count: u64, + issued_share_count: u64, +) -> Option<&'static str> { + match action { + RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy + | RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => Some("2881"), + RuntimeCompanyAnnualFinancePolicyAction::BondIssue => { + match runtime_annual_bond_principal_flow_relation_label( + retired_principal_total, + issued_principal_total, + ) { + Some("retired_equals_issued") => Some("2882"), + Some("issued_exceeds_retired") => Some("2883"), + Some("retired_exceeds_issued") => Some("2884"), + Some("retired_only") => Some("2885"), + Some("issued_only") => Some("2886"), + _ => None, + } + } + RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => { + (repurchased_share_count > 0).then_some("2887") + } + RuntimeCompanyAnnualFinancePolicyAction::StockIssue => { + (issued_share_count > 0).then_some("4053") + } + _ => None, + } +} diff --git a/crates/rrt-runtime/src/derived/finance/bond_policy.rs b/crates/rrt-runtime/src/derived/finance/bond_policy.rs new file mode 100644 index 0000000..e7a70d8 --- /dev/null +++ b/crates/rrt-runtime/src/derived/finance/bond_policy.rs @@ -0,0 +1,168 @@ +use crate::derived::{ + runtime_company_annual_finance_state, runtime_company_control_transfer_stat_value_f64, + runtime_company_credit_rating, runtime_company_matured_live_bond_count, + runtime_company_matured_live_bond_principal_total, + runtime_company_next_live_bond_maturity_year, runtime_company_prime_rate, + runtime_company_total_live_bond_principal, runtime_round_f64_to_i64, + runtime_world_annual_finance_mode_active, runtime_world_bond_issue_and_repayment_allowed, +}; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::{RuntimeCompanyAnnualBondPolicyState, RuntimeState}; + +pub fn runtime_company_average_live_bond_coupon( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + if market_state.live_bond_slots.is_empty() { + return Some(0.0); + } + let mut weighted_coupon_sum = 0.0f64; + let mut total_principal = 0u64; + for slot in &market_state.live_bond_slots { + let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32) as f64; + if !coupon_rate.is_finite() { + continue; + } + weighted_coupon_sum += coupon_rate * (slot.principal as f64); + total_principal = total_principal.checked_add(slot.principal as u64)?; + } + if total_principal == 0 { + return Some(0.0); + } + Some(weighted_coupon_sum / total_principal as f64) +} + +pub fn runtime_company_live_bond_coupon_burden_total( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + let mut total = 0i64; + for slot in &market_state.live_bond_slots { + let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32) as f64; + if !coupon_rate.is_finite() { + continue; + } + let coupon_burden = + runtime_round_f64_to_i64((slot.principal as f64) * coupon_rate).unwrap_or(0); + total = total.checked_add(coupon_burden)?; + } + Some(total) +} + +pub fn runtime_company_bond_interest_rate_quote( + state: &RuntimeState, + company_id: u32, + _principal: u32, + _years_to_maturity: u32, +) -> Option { + let credit_rating = runtime_company_credit_rating(state, company_id)? as f64; + let prime_rate_percent = runtime_company_prime_rate(state, company_id)? as f64; + let quote = (prime_rate_percent + (10.0 - credit_rating)) / 100.0; + quote.is_finite().then_some(quote) +} + +pub fn runtime_company_annual_bond_policy_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + const STANDARD_CASH_FLOOR: i64 = -250_000; + const LINKED_TRANSIT_CASH_FLOOR: i64 = -30_000; + const ISSUE_PRINCIPAL_STEP: u32 = 500_000; + const ISSUE_YEARS_TO_MATURITY: u32 = 30; + + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_year_word = state + .world_restore + .packed_year_word_raw_u16 + .map(u32::from) + .unwrap_or(state.calendar.year); + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let live_bond_principal_total = runtime_company_total_live_bond_principal(state, company_id); + let matured_live_bond_count = + runtime_company_matured_live_bond_count(state, company_id, current_year_word); + let matured_live_bond_principal_total = + runtime_company_matured_live_bond_principal_total(state, company_id, current_year_word); + let next_live_bond_maturity_year = + runtime_company_next_live_bond_maturity_year(state, company_id); + let cash_after_full_repayment = current_cash + .zip(live_bond_principal_total) + .map(|(cash, principal)| cash - i64::from(principal)); + let issue_cash_floor = Some(if annual_finance_state.linked_transit_latch { + LINKED_TRANSIT_CASH_FLOOR + } else { + STANDARD_CASH_FLOOR + }); + let proposed_issue_bond_count = cash_after_full_repayment.zip(issue_cash_floor).map( + |(cash_after_repayment, cash_floor)| { + if cash_after_repayment >= cash_floor { + 0 + } else { + let deficit = (cash_floor - cash_after_repayment) as u64; + deficit.div_ceil(u64::from(ISSUE_PRINCIPAL_STEP)) as u32 + } + }, + ); + let proposed_issue_total_principal = + proposed_issue_bond_count.and_then(|count| count.checked_mul(ISSUE_PRINCIPAL_STEP)); + let eligible_for_bond_issue_branch = runtime_world_annual_finance_mode_active(state) + == Some(true) + && runtime_world_bond_issue_and_repayment_allowed(state) == Some(true) + && (matured_live_bond_principal_total.is_some_and(|principal| principal > 0) + || proposed_issue_bond_count.is_some_and(|count| count > 0)); + Some(RuntimeCompanyAnnualBondPolicyState { + company_id, + annual_mode_active: runtime_world_annual_finance_mode_active(state), + bond_issue_and_repayment_allowed: runtime_world_bond_issue_and_repayment_allowed(state), + linked_transit_latch: annual_finance_state.linked_transit_latch, + live_bond_count: Some(annual_finance_state.bond_count), + live_bond_principal_total, + matured_live_bond_count, + matured_live_bond_principal_total, + next_live_bond_maturity_year, + live_bond_coupon_burden_total: annual_finance_state.live_bond_coupon_burden_total, + current_cash, + cash_after_full_repayment, + issue_cash_floor, + issue_principal_step: Some(ISSUE_PRINCIPAL_STEP), + proposed_issue_bond_count, + proposed_issue_total_principal, + proposed_issue_years_to_maturity: Some(ISSUE_YEARS_TO_MATURITY), + eligible_for_bond_issue_branch, + }) +} + +pub fn runtime_annual_bond_principal_flow_relation_label( + retired_principal_total: u64, + issued_principal_total: u64, +) -> Option<&'static str> { + match retired_principal_total.cmp(&issued_principal_total) { + std::cmp::Ordering::Equal => { + if retired_principal_total == 0 { + None + } else { + Some("retired_equals_issued") + } + } + std::cmp::Ordering::Greater => { + if issued_principal_total == 0 { + Some("retired_only") + } else { + Some("retired_exceeds_issued") + } + } + std::cmp::Ordering::Less => { + if retired_principal_total == 0 { + Some("issued_only") + } else { + Some("issued_exceeds_retired") + } + } + } +} diff --git a/crates/rrt-runtime/src/derived/finance/credit_rating.rs b/crates/rrt-runtime/src/derived/finance/credit_rating.rs new file mode 100644 index 0000000..dd6a51d --- /dev/null +++ b/crates/rrt-runtime/src/derived/finance/credit_rating.rs @@ -0,0 +1,151 @@ +use crate::derived::{ + runtime_company_annual_finance_state, runtime_company_average_live_bond_coupon, + runtime_company_control_transfer_stat_value_f64, runtime_company_derived_stat_value, + runtime_round_f64_to_i64, runtime_world_issue_opinion_term_sum_raw, + runtime_world_prime_rate_baseline, +}; +use crate::event::metrics::{ + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_WORLD_ISSUE_CREDIT_MARKET, + RUNTIME_WORLD_ISSUE_PRIME_RATE, +}; +use crate::state::RuntimeState; + +pub fn runtime_company_credit_rating(state: &RuntimeState, company_id: u32) -> Option { + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + if let Some(credit_rating_score) = company.credit_rating_score { + return Some(credit_rating_score); + } + + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + if annual_finance_state.outstanding_shares == 0 { + return Some(-512); + } + + let mut weighted_recent_profit_total = 0.0f64; + let mut weighted_recent_profit_weight = 0.0f64; + for (index, (net_profit, fuel_cost)) in annual_finance_state + .trailing_full_year_net_profits + .iter() + .zip(annual_finance_state.trailing_full_year_fuel_costs.iter()) + .take(4) + .enumerate() + { + let weight = (4 - index) as f64; + weighted_recent_profit_total += (*net_profit - *fuel_cost) as f64 * weight; + weighted_recent_profit_weight += weight; + } + let weighted_recent_profit = if weighted_recent_profit_weight > 0.0 { + weighted_recent_profit_total / weighted_recent_profit_weight + } else { + 0.0 + }; + + let current_slot_12 = runtime_company_control_transfer_stat_value_f64(state, company_id, 0x12)?; + let current_slot_30 = runtime_company_derived_stat_value( + state, + company_id, + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + 0x30, + )?; + let current_slot_31 = runtime_company_derived_stat_value( + state, + company_id, + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + 0x31, + )?; + let average_live_bond_coupon = runtime_company_average_live_bond_coupon(state, company_id)?; + + let mut finance_pressure = average_live_bond_coupon * current_slot_12; + if company.current_cash > 0 { + let prime_baseline = runtime_world_prime_rate_baseline(state)?; + let raw_issue_39 = runtime_world_issue_opinion_term_sum_raw( + state, + RUNTIME_WORLD_ISSUE_PRIME_RATE, + company.linked_chairman_profile_id, + Some(company_id), + None, + )? as f64; + finance_pressure += + company.current_cash as f64 * (prime_baseline + raw_issue_39 * 0.01 + 0.03); + } + + let profitability_ratio = if finance_pressure < 0.0 { + weighted_recent_profit / (-finance_pressure) + } else { + 10.0 + }; + let mut profitability_score = runtime_credit_rating_profitability_ladder(profitability_ratio); + if let Some(years_since_founding) = annual_finance_state.years_since_founding { + if years_since_founding < 5 { + let missing_years = (5 - years_since_founding) as f64; + profitability_score += (10.0 - profitability_score) * missing_years * 0.1; + } + } + if current_slot_31 > 1_000_000.0 { + profitability_score += current_slot_31 / 100_000.0 - 10.0; + } + + let burden_ratio = if current_slot_30 > 0.0 { + (weighted_recent_profit - current_slot_12) / current_slot_30 + } else { + 1.0 + }; + let burden_score = runtime_credit_rating_burden_ladder(burden_ratio); + + let mut rating = + (profitability_score * burden_score / 10.0 + profitability_score + burden_score) / 3.0; + rating *= runtime_world_credit_market_scale(state)?; + if let Some(years_since_last_bankruptcy) = annual_finance_state.years_since_last_bankruptcy { + if years_since_last_bankruptcy < 15 { + rating *= years_since_last_bankruptcy as f64 * 0.0666; + } + } + + let raw_issue_38 = runtime_world_issue_opinion_term_sum_raw( + state, + RUNTIME_WORLD_ISSUE_CREDIT_MARKET, + company.linked_chairman_profile_id, + Some(company_id), + None, + )? as f64; + runtime_round_f64_to_i64((rating + raw_issue_38 + 0.5).clamp(0.0, 10.0)) +} + +pub(super) fn runtime_credit_rating_profitability_ladder(ratio: f64) -> f64 { + if !ratio.is_finite() || ratio <= 0.0 { + 0.0 + } else if ratio < 1.0 { + 1.0 + ratio * 4.0 + } else if ratio < 2.0 { + 3.0 + ratio * 2.0 + } else if ratio < 5.0 { + 5.0 + ratio + } else { + 10.0 + } +} + +pub(super) fn runtime_credit_rating_burden_ladder(ratio: f64) -> f64 { + if !ratio.is_finite() || ratio > 1.0 { + 0.0 + } else if ratio > 0.75 { + 16.0 - ratio * 16.0 + } else if ratio > 0.5 { + 13.0 - ratio * 12.0 + } else if ratio > 0.25 { + 11.0 - ratio * 8.0 + } else if ratio > 0.1 { + 10.0 - ratio * 4.0 + } else { + 10.0 + } +} + +pub fn runtime_world_credit_market_scale(state: &RuntimeState) -> Option { + const ISSUE_38_SCALE_TABLE: [f64; 8] = [0.8, 0.9, 1.0, 1.1, 1.2, 0.9, 0.95, 1.0]; + let index = state.world_restore.issue_38_value? as usize; + ISSUE_38_SCALE_TABLE.get(index).copied() +} diff --git a/crates/rrt-runtime/src/derived/finance/distress.rs b/crates/rrt-runtime/src/derived/finance/distress.rs new file mode 100644 index 0000000..4cda665 --- /dev/null +++ b/crates/rrt-runtime/src/derived/finance/distress.rs @@ -0,0 +1,168 @@ +use crate::derived::{ + runtime_company_annual_finance_state, runtime_company_control_transfer_stat_value_f64, + runtime_company_stat_value_f64, runtime_company_support_adjusted_share_price_scalar, + runtime_round_f64_to_i64, runtime_world_annual_finance_mode_active, + runtime_world_bankruptcy_allowed, +}; +use crate::event::metrics::{ + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + RuntimeCompanyStatSelector, +}; +use crate::state::{ + RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState, RuntimeState, +}; + +pub fn runtime_company_annual_creditor_pressure_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_cash_plus_slot_12_total = + runtime_company_control_transfer_stat_value_f64(state, company_id, 0x12) + .and_then(runtime_round_f64_to_i64) + .and_then(|slot_12| { + runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64) + .map(|current_cash| current_cash + slot_12) + }); + let support_adjusted_share_price_scalar = + runtime_company_support_adjusted_share_price_scalar(state, company_id) + .and_then(runtime_round_f64_to_i64); + let current_fuel_cost = runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: 0x09, + }, + ) + .and_then(runtime_round_f64_to_i64); + let recent_bad_net_profit_year_count = annual_finance_state + .trailing_full_year_net_profits + .iter() + .take(3) + .filter(|value| **value < -10_000) + .count() as u32; + let recent_peak_revenue = annual_finance_state + .trailing_full_year_revenues + .iter() + .take(3) + .copied() + .max(); + let recent_three_year_net_profit_total = + if annual_finance_state.trailing_full_year_net_profits.len() >= 3 { + Some( + annual_finance_state + .trailing_full_year_net_profits + .iter() + .take(3) + .sum::(), + ) + } else { + None + }; + let pressure_ladder_cash_floor = recent_peak_revenue.map(|revenue| { + if revenue < 120_000 { + -600_000 + } else if revenue < 230_000 { + -1_100_000 + } else if revenue < 340_000 { + -1_600_000 + } else { + -2_000_000 + } + }); + let support_adjusted_share_price_floor = Some(if recent_bad_net_profit_year_count == 3 { + 20 + } else { + 15 + }); + let current_fuel_cost_floor = pressure_ladder_cash_floor.map(|floor| floor * 8 / 100); + let eligible_for_bankruptcy_branch = runtime_world_annual_finance_mode_active(state) + == Some(true) + && runtime_world_bankruptcy_allowed(state) == Some(true) + && annual_finance_state + .years_since_last_bankruptcy + .is_some_and(|years| years >= 13) + && annual_finance_state + .years_since_founding + .is_some_and(|years| years >= 4) + && recent_bad_net_profit_year_count >= 2 + && current_cash_plus_slot_12_total + .zip(pressure_ladder_cash_floor) + .is_some_and(|(value, floor)| value <= floor) + && support_adjusted_share_price_scalar + .zip(support_adjusted_share_price_floor) + .is_some_and(|(value, floor)| value >= floor) + && current_fuel_cost + .zip(current_fuel_cost_floor) + .is_some_and(|(value, floor)| value <= floor) + && recent_three_year_net_profit_total.is_some_and(|value| value <= -60_000); + Some(RuntimeCompanyAnnualCreditorPressureState { + company_id, + annual_mode_active: runtime_world_annual_finance_mode_active(state), + bankruptcy_allowed: runtime_world_bankruptcy_allowed(state), + years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy, + years_since_founding: annual_finance_state.years_since_founding, + recent_bad_net_profit_year_count, + recent_peak_revenue, + recent_three_year_net_profit_total, + pressure_ladder_cash_floor, + current_cash_plus_slot_12_total, + support_adjusted_share_price_floor, + support_adjusted_share_price_scalar, + current_fuel_cost, + current_fuel_cost_floor, + eligible_for_bankruptcy_branch, + }) +} + +pub fn runtime_company_annual_deep_distress_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let recent_first_three_net_profit_years = annual_finance_state + .trailing_full_year_net_profits + .iter() + .take(3) + .copied() + .collect::>(); + let deep_distress_cash_floor = Some(-300_000); + let deep_distress_net_profit_floor = Some(-20_000); + let eligible_for_bankruptcy_fallback = runtime_world_bankruptcy_allowed(state) == Some(true) + && current_cash + .zip(deep_distress_cash_floor) + .is_some_and(|(value, floor)| value <= floor) + && annual_finance_state + .years_since_founding + .is_some_and(|years| years >= 3) + && recent_first_three_net_profit_years.len() == 3 + && recent_first_three_net_profit_years + .iter() + .all(|value| *value <= deep_distress_net_profit_floor.unwrap()) + && annual_finance_state + .years_since_last_bankruptcy + .is_some_and(|years| years >= 5); + Some(RuntimeCompanyAnnualDeepDistressState { + company_id, + bankruptcy_allowed: runtime_world_bankruptcy_allowed(state), + years_since_founding: annual_finance_state.years_since_founding, + years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy, + current_cash, + recent_first_three_net_profit_years, + deep_distress_cash_floor, + deep_distress_net_profit_floor, + eligible_for_bankruptcy_fallback, + }) +} diff --git a/crates/rrt-runtime/src/derived/finance/dividend_policy.rs b/crates/rrt-runtime/src/derived/finance/dividend_policy.rs new file mode 100644 index 0000000..30bc542 --- /dev/null +++ b/crates/rrt-runtime/src/derived/finance/dividend_policy.rs @@ -0,0 +1,227 @@ +use crate::derived::{ + runtime_company_annual_bond_policy_state, runtime_company_annual_creditor_pressure_state, + runtime_company_annual_deep_distress_state, runtime_company_annual_finance_state, + runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state, + runtime_company_control_transfer_stat_value_f64, + runtime_company_year_or_control_transfer_metric_value, runtime_decode_saved_f32_value, + runtime_round_f64_to_i64, runtime_world_annual_finance_mode_active, + runtime_world_building_density_growth_setting, runtime_world_dividend_adjustment_allowed, +}; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::{RuntimeCompanyAnnualDividendPolicyState, RuntimeState}; + +pub(super) fn runtime_company_board_approved_dividend_rate_ceiling_f64( + state: &RuntimeState, + company_id: u32, +) -> Option { + const REVENUE_GUARD_DIVISOR: f64 = 2.0; + const EARLY_SUPPORT_MULTIPLIER: f64 = 0.05; + const HISTORICAL_GUARD_SCALE: f64 = 1.25; + const ANCHOR_SCALE: f64 = 0.35; + + let market_state = state.service_state.company_market_state.get(&company_id)?; + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + )?; + let shares_plus_one = market_state.outstanding_shares.checked_add(1)?; + let shares_plus_one_f64 = shares_plus_one as f64; + let current_cash_per_share_ceiling = current_cash / shares_plus_one_f64; + let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); + let years_since_founding = current_year_word + .checked_sub(market_state.founding_year) + .unwrap_or(0) + .min(3); + let start_year_offset = if state.world_restore.partial_year_progress_raw_u8 == Some(0x0c) { + 0 + } else { + 1 + }; + + let mut strongest_net_profit_guard = 0.0f64; + let mut strongest_revenue_guard = 0.0f64; + if start_year_offset <= years_since_founding { + for year_offset in start_year_offset..=years_since_founding { + let year_word = current_year_word.checked_sub(year_offset)?; + let net_profit = runtime_company_year_or_control_transfer_metric_value( + state, company_id, year_word, 0x2b, + )?; + strongest_net_profit_guard = strongest_net_profit_guard.max(net_profit); + + let revenue = runtime_company_year_or_control_transfer_metric_value( + state, company_id, year_word, 0x2c, + )?; + strongest_revenue_guard = strongest_revenue_guard.max(revenue); + } + } + + let mut historical_guard_total = + strongest_net_profit_guard.min(strongest_revenue_guard / REVENUE_GUARD_DIVISOR); + if years_since_founding <= 1 { + let early_support_guard = market_state.outstanding_shares as f64 + * runtime_decode_saved_f32_value(market_state.young_company_support_scalar_raw_u32)? + * EARLY_SUPPORT_MULTIPLIER; + historical_guard_total = historical_guard_total.max(early_support_guard); + } + + let historical_guard_per_share_ceiling = + historical_guard_total / shares_plus_one_f64 * HISTORICAL_GUARD_SCALE; + let mut ceiling = current_cash_per_share_ceiling.min(historical_guard_per_share_ceiling); + let anchor_value = if years_since_founding == 0 { + runtime_decode_saved_f32_value(market_state.young_company_support_scalar_raw_u32)? + } else { + runtime_company_year_or_control_transfer_metric_value( + state, + company_id, + current_year_word.checked_sub(1)?, + 0x1c, + )? + }; + ceiling = ceiling.min(anchor_value * ANCHOR_SCALE); + Some(ceiling.max(0.0)) +} + +pub fn runtime_company_annual_dividend_policy_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + const WEIGHTED_NET_PROFIT_DIVISOR: f64 = 6.0; + const CASH_SUPPLEMENT_DIVISOR: f64 = 3.0; + const STANDARD_TARGET_DIVISOR: f64 = 6.0; + const DIVIDEND_DELTA_COLLAPSE_THRESHOLD: f64 = 0.1; + const GROWTH_SETTING_ONE_DIVIDEND_SCALE: f64 = 0.66; + + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); + let current_dividend_per_share = + runtime_company_control_transfer_stat_value_f64(state, company_id, 0x20)?; + let building_density_growth_setting = runtime_world_building_density_growth_setting(state); + let weighted_recent_net_profit_total = Some( + runtime_company_year_or_control_transfer_metric_value( + state, + company_id, + current_year_word, + 0x2b, + ) + .and_then(runtime_round_f64_to_i64)? + .checked_mul(3)? + .checked_add( + runtime_company_year_or_control_transfer_metric_value( + state, + company_id, + current_year_word.checked_sub(1)?, + 0x2b, + ) + .and_then(runtime_round_f64_to_i64)? + .checked_mul(2)?, + )? + .checked_add( + runtime_company_year_or_control_transfer_metric_value( + state, + company_id, + current_year_word.checked_sub(2)?, + 0x2b, + ) + .and_then(runtime_round_f64_to_i64)?, + )?, + ); + let weighted_recent_net_profit_average = weighted_recent_net_profit_total + .and_then(|value| runtime_round_f64_to_i64(value as f64 / WEIGHTED_NET_PROFIT_DIVISOR)); + let tiny_unassigned_share_cash_supplement_branch = + annual_finance_state.unassigned_share_pool <= 1_000; + let tentative_target_dividend_per_share = + weighted_recent_net_profit_average.and_then(|value| { + if annual_finance_state.outstanding_shares == 0 { + return None; + } + let shares = annual_finance_state.outstanding_shares as f64; + if tiny_unassigned_share_cash_supplement_branch { + let cash_component = current_cash.unwrap_or(0).max(0) as f64; + Some( + ((value as f64 / CASH_SUPPLEMENT_DIVISOR) + + cash_component / CASH_SUPPLEMENT_DIVISOR) + / shares, + ) + } else { + Some((value as f64 / STANDARD_TARGET_DIVISOR) / shares) + } + }); + let growth_adjusted_current_dividend_per_share = Some(match building_density_growth_setting { + Some(1) => current_dividend_per_share * GROWTH_SETTING_ONE_DIVIDEND_SCALE, + Some(2) => 0.0, + _ => current_dividend_per_share, + }); + let proposed_dividend_per_share = if tentative_target_dividend_per_share + .is_some_and(|value| value <= DIVIDEND_DELTA_COLLAPSE_THRESHOLD) + { + Some(0.0) + } else { + growth_adjusted_current_dividend_per_share + .zip(tentative_target_dividend_per_share) + .map(|(current_dividend, target)| { + ((current_dividend + target + DIVIDEND_DELTA_COLLAPSE_THRESHOLD) / 2.0 * 10.0) + .round() + / 10.0 + }) + }; + let board_approved_dividend_rate_ceiling = + runtime_company_board_approved_dividend_rate_ceiling_f64(state, company_id); + let proposed_dividend_per_share = proposed_dividend_per_share + .zip(board_approved_dividend_rate_ceiling) + .map(|(proposed, ceiling)| proposed.min(ceiling)); + let current_dividend_per_share_tenths = + runtime_round_f64_to_i64(current_dividend_per_share * 10.0); + let eligible_for_dividend_adjustment_branch = runtime_world_annual_finance_mode_active(state) + == Some(true) + && runtime_world_dividend_adjustment_allowed(state) == Some(true) + && annual_finance_state + .years_since_last_dividend + .is_some_and(|years| years >= 1) + && annual_finance_state + .years_since_founding + .is_some_and(|years| years >= 2) + && !runtime_company_annual_creditor_pressure_state(state, company_id)? + .eligible_for_bankruptcy_branch + && !runtime_company_annual_deep_distress_state(state, company_id)? + .eligible_for_bankruptcy_fallback + && !runtime_company_annual_bond_policy_state(state, company_id)? + .eligible_for_bond_issue_branch + && !runtime_company_annual_stock_repurchase_state(state, company_id)? + .eligible_for_single_batch_repurchase + && !runtime_company_annual_stock_issue_state(state, company_id)? + .eligible_for_double_tranche_issue + && proposed_dividend_per_share.and_then(|value| runtime_round_f64_to_i64(value * 10.0)) + != current_dividend_per_share_tenths; + Some(RuntimeCompanyAnnualDividendPolicyState { + company_id, + annual_mode_active: runtime_world_annual_finance_mode_active(state), + dividend_adjustment_allowed: runtime_world_dividend_adjustment_allowed(state), + years_since_last_dividend: annual_finance_state.years_since_last_dividend, + years_since_founding: annual_finance_state.years_since_founding, + outstanding_shares: Some(annual_finance_state.outstanding_shares), + unassigned_share_pool: Some(annual_finance_state.unassigned_share_pool), + weighted_recent_net_profit_total, + weighted_recent_net_profit_average, + current_cash, + tiny_unassigned_share_cash_supplement_branch, + tentative_target_dividend_per_share_tenths: tentative_target_dividend_per_share + .and_then(|value| runtime_round_f64_to_i64(value * 10.0)), + current_dividend_per_share_tenths, + building_density_growth_setting, + growth_adjusted_current_dividend_per_share_tenths: + growth_adjusted_current_dividend_per_share + .and_then(|value| runtime_round_f64_to_i64(value * 10.0)), + board_approved_dividend_rate_ceiling_tenths: board_approved_dividend_rate_ceiling + .and_then(|value| runtime_round_f64_to_i64(value * 10.0)), + proposed_dividend_per_share_tenths: proposed_dividend_per_share + .and_then(|value| runtime_round_f64_to_i64(value * 10.0)), + eligible_for_dividend_adjustment_branch, + }) +} diff --git a/crates/rrt-runtime/src/derived/finance/mod.rs b/crates/rrt-runtime/src/derived/finance/mod.rs new file mode 100644 index 0000000..ea90e77 --- /dev/null +++ b/crates/rrt-runtime/src/derived/finance/mod.rs @@ -0,0 +1,13 @@ +mod annual_policy; +mod bond_policy; +mod credit_rating; +mod distress; +mod dividend_policy; +mod stock_actions; + +pub use annual_policy::*; +pub use bond_policy::*; +pub use credit_rating::*; +pub use distress::*; +pub use dividend_policy::*; +pub use stock_actions::*; diff --git a/crates/rrt-runtime/src/derived/finance/stock_actions.rs b/crates/rrt-runtime/src/derived/finance/stock_actions.rs new file mode 100644 index 0000000..37d7079 --- /dev/null +++ b/crates/rrt-runtime/src/derived/finance/stock_actions.rs @@ -0,0 +1,271 @@ +use crate::derived::{ + runtime_company_annual_bond_policy_state, runtime_company_annual_creditor_pressure_state, + runtime_company_annual_deep_distress_state, runtime_company_annual_finance_state, + runtime_company_book_value_per_share, runtime_company_control_transfer_stat_value_f64, + runtime_company_highest_live_bond_coupon_rate, + runtime_company_support_adjusted_share_price_scalar, + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64, + runtime_company_unassigned_share_pool, runtime_round_f64_to_i64, + runtime_world_annual_finance_mode_active, runtime_world_bond_issue_and_repayment_allowed, + runtime_world_building_density_growth_setting, runtime_world_stock_issue_and_buyback_allowed, +}; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::{ + RuntimeCompanyAnnualStockIssueState, RuntimeCompanyAnnualStockRepurchaseState, RuntimeState, +}; + +pub(super) fn runtime_chairman_stock_repurchase_factor_f64( + state: &RuntimeState, + chairman_profile_id: Option, +) -> Option { + let personality_byte = chairman_profile_id + .and_then(|profile_id| { + state + .service_state + .chairman_personality_raw_u8 + .get(&profile_id) + }) + .copied(); + let mut factor = personality_byte + .map(|byte| (f64::from(byte) * 39.0 + 300.0) / 400.0) + .unwrap_or(1.0); + if runtime_world_building_density_growth_setting(state) == Some(1) { + factor *= 1.6; + } + Some(factor) +} + +pub fn runtime_company_annual_stock_repurchase_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let support_adjusted_share_price_scalar = + runtime_company_support_adjusted_share_price_scalar(state, company_id); + let repurchase_factor = + runtime_chairman_stock_repurchase_factor_f64(state, company.linked_chairman_profile_id)?; + let repurchase_factor_basis_points = runtime_round_f64_to_i64(repurchase_factor * 100.0); + let stock_value_gate_cash_floor = runtime_round_f64_to_i64(repurchase_factor * 800_000.0); + let affordability_cash_floor = support_adjusted_share_price_scalar + .and_then(|value| runtime_round_f64_to_i64(value * repurchase_factor * 1_000.0 * 1.2)); + let support_adjusted_share_price_scalar = + support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64); + let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id); + let eligible_for_single_batch_repurchase = runtime_world_annual_finance_mode_active(state) + == Some(true) + && runtime_world_stock_issue_and_buyback_allowed(state) == Some(true) + && annual_finance_state.city_connection_latch + && current_cash + .zip(stock_value_gate_cash_floor) + .is_some_and(|(value, floor)| value >= floor) + && current_cash + .zip(affordability_cash_floor) + .is_some_and(|(value, floor)| value >= floor) + && unassigned_share_pool.is_some_and(|value| value >= 1_000); + Some(RuntimeCompanyAnnualStockRepurchaseState { + company_id, + annual_mode_active: runtime_world_annual_finance_mode_active(state), + stock_issue_and_buyback_allowed: runtime_world_stock_issue_and_buyback_allowed(state), + city_connection_latch: annual_finance_state.city_connection_latch, + building_density_growth_setting: runtime_world_building_density_growth_setting(state), + linked_chairman_profile_id: company.linked_chairman_profile_id, + linked_chairman_personality_raw_u8: company + .linked_chairman_profile_id + .and_then(|profile_id| { + state + .service_state + .chairman_personality_raw_u8 + .get(&profile_id) + }) + .copied(), + repurchase_batch_size: Some(1_000), + repurchase_factor_basis_points, + current_cash, + stock_value_gate_cash_floor, + support_adjusted_share_price_scalar, + affordability_cash_floor, + unassigned_share_pool, + eligible_for_single_batch_repurchase, + }) +} + +pub(super) fn runtime_company_stock_issue_price_to_book_ratio_f64( + pressured_support_adjusted_share_price_scalar: f64, + book_value_per_share: f64, +) -> Option { + let denominator = book_value_per_share.max(1.0); + if !pressured_support_adjusted_share_price_scalar.is_finite() || !denominator.is_finite() { + return None; + } + Some(pressured_support_adjusted_share_price_scalar / denominator) +} + +pub(super) fn runtime_company_stock_issue_minimum_price_to_book_ratio_f64( + highest_coupon_rate: f64, +) -> Option { + if !highest_coupon_rate.is_finite() || highest_coupon_rate <= 0.0 { + return None; + } + Some(if highest_coupon_rate <= 0.07 { + 1.30 + } else if highest_coupon_rate <= 0.08 { + 1.20 + } else if highest_coupon_rate <= 0.09 { + 1.10 + } else if highest_coupon_rate <= 0.10 { + 0.95 + } else if highest_coupon_rate <= 0.11 { + 0.80 + } else if highest_coupon_rate <= 0.12 { + 0.62 + } else if highest_coupon_rate <= 0.13 { + 0.50 + } else if highest_coupon_rate <= 0.14 { + 0.35 + } else { + return None; + }) +} + +pub fn runtime_company_annual_stock_issue_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + const ISSUE_PROCEEDS_CAP: i64 = 55_000; + const SHARE_PRICE_FLOOR: i64 = 22; + const ONE_YEAR_ABSOLUTE_COUNTER_SPAN: i64 = 12 * 28 * 24 * 60; + + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let highest_coupon_live_bond_principal = + annual_finance_state.highest_coupon_live_bond_principal; + let highest_coupon_live_bond_rate = + runtime_company_highest_live_bond_coupon_rate(state, company_id); + let highest_coupon_live_bond_rate_basis_points = + highest_coupon_live_bond_rate.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); + let mut initial_issue_batch_size = + (annual_finance_state.outstanding_shares / 10 / 1_000) * 1_000; + if initial_issue_batch_size < 2_000 { + initial_issue_batch_size = 2_000; + } + let initial_issue_batch_size = Some(initial_issue_batch_size); + let mut trimmed_issue_batch_size = initial_issue_batch_size?; + let mut pressured_support_adjusted_share_price_scalar = + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( + state, + company_id, + -(trimmed_issue_batch_size as i64), + ); + let mut pressured_proceeds = pressured_support_adjusted_share_price_scalar + .and_then(|value| runtime_round_f64_to_i64(value * trimmed_issue_batch_size as f64)); + while trimmed_issue_batch_size > 2_000 + && pressured_proceeds.is_some_and(|value| value > ISSUE_PROCEEDS_CAP) + { + trimmed_issue_batch_size = trimmed_issue_batch_size.saturating_sub(1_000); + pressured_support_adjusted_share_price_scalar = + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( + state, + company_id, + -(trimmed_issue_batch_size as i64), + ); + pressured_proceeds = pressured_support_adjusted_share_price_scalar + .and_then(|value| runtime_round_f64_to_i64(value * trimmed_issue_batch_size as f64)); + } + let pressured_support_adjusted_share_price_scalar_i64 = + pressured_support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64); + let book_value_per_share_floor_applied = + runtime_company_book_value_per_share(state, company_id).map(|value| value.max(1)); + let price_to_book_ratio = pressured_support_adjusted_share_price_scalar + .zip(book_value_per_share_floor_applied) + .and_then(|(share_price, book_value)| { + runtime_company_stock_issue_price_to_book_ratio_f64(share_price, book_value as f64) + }); + let price_to_book_ratio_basis_points = + price_to_book_ratio.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); + let minimum_price_to_book_ratio = highest_coupon_live_bond_rate + .and_then(runtime_company_stock_issue_minimum_price_to_book_ratio_f64); + let minimum_price_to_book_ratio_basis_points = + minimum_price_to_book_ratio.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); + let passes_share_price_floor = + pressured_support_adjusted_share_price_scalar_i64.map(|value| value >= SHARE_PRICE_FLOOR); + let passes_proceeds_floor = pressured_proceeds.map(|value| value >= ISSUE_PROCEEDS_CAP); + let passes_cash_gate = current_cash + .zip(highest_coupon_live_bond_principal) + .map(|(cash, principal)| cash <= i64::from(principal) + 5_000); + let passes_issue_cooldown_gate = Some( + annual_finance_state + .current_issue_age_absolute_counter_delta + .is_none_or(|delta| delta >= ONE_YEAR_ABSOLUTE_COUNTER_SPAN), + ); + let passes_coupon_price_to_book_gate = price_to_book_ratio_basis_points + .zip(minimum_price_to_book_ratio_basis_points) + .map(|(actual, minimum)| actual >= minimum); + let eligible_for_double_tranche_issue = runtime_world_annual_finance_mode_active(state) + == Some(true) + && runtime_world_stock_issue_and_buyback_allowed(state) == Some(true) + && runtime_world_bond_issue_and_repayment_allowed(state) == Some(true) + && annual_finance_state.bond_count >= 2 + && annual_finance_state + .years_since_founding + .is_some_and(|years| years >= 1) + && !runtime_company_annual_creditor_pressure_state(state, company_id)? + .eligible_for_bankruptcy_branch + && !runtime_company_annual_deep_distress_state(state, company_id)? + .eligible_for_bankruptcy_fallback + && !runtime_company_annual_bond_policy_state(state, company_id)? + .eligible_for_bond_issue_branch + && !runtime_company_annual_stock_repurchase_state(state, company_id)? + .eligible_for_single_batch_repurchase + && passes_share_price_floor == Some(true) + && passes_proceeds_floor == Some(true) + && passes_cash_gate == Some(true) + && passes_issue_cooldown_gate == Some(true) + && passes_coupon_price_to_book_gate == Some(true); + Some(RuntimeCompanyAnnualStockIssueState { + company_id, + annual_mode_active: runtime_world_annual_finance_mode_active(state), + stock_issue_and_buyback_allowed: runtime_world_stock_issue_and_buyback_allowed(state), + bond_issue_and_repayment_allowed: runtime_world_bond_issue_and_repayment_allowed(state), + years_since_founding: annual_finance_state.years_since_founding, + live_bond_count: Some(annual_finance_state.bond_count), + initial_issue_batch_size, + trimmed_issue_batch_size: Some(trimmed_issue_batch_size), + share_pressure_basis_points: runtime_round_f64_to_i64( + -(trimmed_issue_batch_size as f64) / annual_finance_state.outstanding_shares as f64 + * 10_000.0, + ), + pressured_support_adjusted_share_price_scalar: + pressured_support_adjusted_share_price_scalar_i64, + pressured_proceeds, + book_value_per_share_floor_applied, + price_to_book_ratio_basis_points, + current_cash, + highest_coupon_live_bond_principal, + highest_coupon_live_bond_rate_basis_points, + current_issue_age_absolute_counter_delta: annual_finance_state + .current_issue_age_absolute_counter_delta, + current_issue_cooldown_floor: Some(ONE_YEAR_ABSOLUTE_COUNTER_SPAN), + minimum_price_to_book_ratio_basis_points, + passes_share_price_floor, + passes_proceeds_floor, + passes_cash_gate, + passes_issue_cooldown_gate, + passes_coupon_price_to_book_gate, + eligible_for_double_tranche_issue, + }) +} diff --git a/crates/rrt-runtime/src/derived/mod.rs b/crates/rrt-runtime/src/derived/mod.rs new file mode 100644 index 0000000..92999cf --- /dev/null +++ b/crates/rrt-runtime/src/derived/mod.rs @@ -0,0 +1,10 @@ +mod company; +mod finance; +mod world; + +pub use company::*; +pub use finance::*; +pub use world::*; + +#[cfg(test)] +mod tests; diff --git a/crates/rrt-runtime/src/derived/tests/bond_metrics.rs b/crates/rrt-runtime/src/derived/tests/bond_metrics.rs new file mode 100644 index 0000000..99c42a8 --- /dev/null +++ b/crates/rrt-runtime/src/derived/tests/bond_metrics.rs @@ -0,0 +1,260 @@ +use super::*; + +#[test] +fn derives_company_credit_rating_from_rehosted_finance_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = + 20.0f64.to_bits(); + year_stat_family_qword_bits[(0x01 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = + 100.0f64.to_bits(); + year_stat_family_qword_bits[(0x09 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = + 0.0f64.to_bits(); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1835, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + issue_38_value: Some(2), + packed_year_word_raw_u16: Some(1835), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1830, + last_bankruptcy_year: 1800, + year_stat_family_qword_bits, + live_bond_slots: vec![RuntimeCompanyBondSlot { + slot_index: 0, + principal: 100_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.05f32.to_bits(), + }], + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let annual_finance_state = + runtime_company_annual_finance_state(&state, 7).expect("annual finance state"); + assert_eq!( + annual_finance_state.trailing_full_year_net_profits, + vec![100, 0, 0, 0] + ); + assert_eq!( + runtime_company_control_transfer_stat_value_f64(&state, 7, 0x12), + Some(20.0) + ); + assert_eq!( + runtime_company_derived_stat_value( + &state, + 7, + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + 0x30, + ), + Some(100.0) + ); + assert_eq!( + runtime_company_derived_stat_value( + &state, + 7, + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + 0x31, + ), + Some(120.0) + ); + let average_live_bond_coupon = + runtime_company_average_live_bond_coupon(&state, 7).expect("average coupon"); + assert!((average_live_bond_coupon - 0.05).abs() < 1e-6); + assert_eq!( + annual_finance_state.live_bond_coupon_burden_total, + Some(5_000) + ); + assert_eq!(runtime_world_prime_rate_baseline(&state), Some(5.0)); + assert_eq!( + runtime_world_issue_opinion_term_sum_raw( + &state, + RUNTIME_WORLD_ISSUE_PRIME_RATE, + None, + Some(7), + None, + ), + Some(0) + ); + assert_eq!( + runtime_world_issue_opinion_term_sum_raw( + &state, + RUNTIME_WORLD_ISSUE_CREDIT_MARKET, + None, + Some(7), + None, + ), + Some(0) + ); + assert_eq!(runtime_world_credit_market_scale(&state), Some(1.0)); + + assert_eq!(runtime_company_credit_rating(&state, 7), Some(10)); + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING, + }, + ), + Some(10) + ); +} + +#[test] +fn computes_weighted_average_live_bond_coupon_from_owned_market_slots() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + live_bond_slots: vec![ + RuntimeCompanyBondSlot { + slot_index: 0, + principal: 100_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.04f32.to_bits(), + }, + RuntimeCompanyBondSlot { + slot_index: 1, + principal: 300_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }, + ], + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let average = runtime_company_average_live_bond_coupon(&state, 7) + .expect("weighted average live bond coupon"); + assert!((average - 0.07).abs() < 1e-6); +} diff --git a/crates/rrt-runtime/src/derived/tests/company.rs b/crates/rrt-runtime/src/derived/tests/company.rs new file mode 100644 index 0000000..59420ae --- /dev/null +++ b/crates/rrt-runtime/src/derived/tests/company.rs @@ -0,0 +1,2519 @@ +use super::*; + +#[test] +fn rejects_duplicate_company_ids() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + absolute_counter_raw_u32: Some(5), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![ + RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + }, + RuntimeCompany { + company_id: 1, + current_cash: 200, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + }, + ], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_partial_world_restore_without_year_lane() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + selected_year_profile_lane: None, + campaign_scenario_enabled: Some(false), + sandbox_enabled: Some(true), + seed_tuple_written_from_raw_lane: Some(true), + absolute_counter_requires_shell_context: Some(true), + absolute_counter_reconstructible_from_save: Some(false), + packed_year_word_raw_u16: None, + partial_year_progress_raw_u8: None, + current_calendar_tuple_word_raw_u32: None, + current_calendar_tuple_word_2_raw_u32: None, + absolute_counter_raw_u32: None, + absolute_counter_mirror_raw_u32: None, + disable_cargo_economy_special_condition_slot: Some(30), + disable_cargo_economy_special_condition_reconstructible_from_save: Some(true), + disable_cargo_economy_special_condition_write_side_grounded: Some(true), + disable_cargo_economy_special_condition_enabled: Some(false), + use_bio_accelerator_cars_enabled: Some(false), + use_wartime_cargos_enabled: Some(false), + disable_train_crashes_enabled: Some(false), + disable_train_crashes_and_breakdowns_enabled: Some(false), + ai_ignore_territories_at_startup_enabled: Some(false), + limited_track_building_amount: None, + economic_status_code: None, + territory_access_cost: None, + linked_site_removal_follow_on_gate_raw_u8: None, + linked_site_removal_follow_on_gate_enabled: None, + auto_show_grade_during_track_lay_raw_u8: None, + starting_building_density_level_raw_u8: None, + post_text_building_density_growth_raw_u8: None, + leftover_simulation_time_accumulator_raw_u32: None, + leftover_simulation_time_accumulator_value_f32_text: None, + selected_year_lane_snapshot_raw_u8: None, + all_steam_locomotives_available_raw_u8: None, + all_steam_locomotives_available_enabled: None, + all_diesel_locomotives_available_raw_u8: None, + all_diesel_locomotives_available_enabled: None, + all_electric_locomotives_available_raw_u8: None, + all_electric_locomotives_available_enabled: None, + cached_available_locomotive_rating_raw_u32: None, + cached_available_locomotive_rating_value_f32_text: None, + issue_37_value: None, + issue_38_value: None, + issue_39_value: None, + issue_3a_value: None, + issue_37_multiplier_raw_u32: None, + issue_37_multiplier_value_f32_text: None, + stock_issue_and_buyback_policy_raw_u8: None, + bond_issue_and_repayment_policy_raw_u8: None, + bankruptcy_policy_raw_u8: None, + dividend_policy_raw_u8: None, + building_density_growth_setting_raw_u32: None, + stock_issue_and_buyback_allowed: None, + bond_issue_and_repayment_allowed: None, + bankruptcy_allowed: None, + dividend_adjustment_allowed: None, + finance_neighborhood_candidates: Vec::new(), + economic_tuning_mirror_raw_u32: None, + economic_tuning_mirror_value_f32_text: None, + economic_tuning_lane_raw_u32: Vec::new(), + economic_tuning_lane_value_f32_text: Vec::new(), + selected_year_bucket_scalar_raw_u32: None, + selected_year_bucket_scalar_value_f32_text: None, + selected_year_bucket_direct_lane_raw_u32: Vec::new(), + selected_year_bucket_direct_lane_value_f32_text: Vec::new(), + selected_year_bucket_complement_lane_raw_u32: Vec::new(), + selected_year_bucket_complement_lane_value_f32_text: Vec::new(), + selected_year_bucket_scaled_companion_lane_raw_u32: Vec::new(), + selected_year_bucket_scaled_companion_lane_value_f32_text: Vec::new(), + selected_year_gap_scalar_raw_u32: None, + selected_year_gap_scalar_value_f32_text: None, + absolute_counter_restore_kind: Some( + "mode-adjusted-selected-year-lane".to_string(), + ), + absolute_counter_adjustment_context: Some( + "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30".to_string(), + ), + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_event_effect_targeting_unknown_company() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + absolute_counter_raw_u32: Some(5), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 7, + trigger_kind: 1, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::Ids { ids: vec![2] }, + delta: 50, + }], + }], + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_template_effect_targeting_unknown_company() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + absolute_counter_raw_u32: Some(5), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 7, + trigger_kind: 1, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AppendEventRecord { + record: Box::new(RuntimeEventRecordTemplate { + record_id: 8, + trigger_kind: 0x0a, + active: true, + marks_collection_dirty: false, + one_shot: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::Ids { ids: vec![2] }, + delta: 50, + }], + }), + }], + }], + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_invalid_packed_event_collection_summary() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: Some(RuntimePackedEventCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 4, + live_record_count: 2, + live_entry_ids: vec![3, 3], + decoded_record_count: 0, + imported_runtime_record_count: 0, + records: vec![ + RuntimePackedEventRecordSummary { + record_index: 0, + live_entry_id: 3, + payload_offset: None, + payload_len: None, + decode_status: "unsupported_framing".to_string(), + payload_family: "unsupported_framing".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: None, + notes: vec!["test".to_string()], + }, + RuntimePackedEventRecordSummary { + record_index: 1, + live_entry_id: 3, + payload_offset: None, + payload_len: None, + decode_status: "unsupported_framing".to_string(), + payload_family: "unsupported_framing".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: None, + notes: vec!["test".to_string()], + }, + ], + }), + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_selected_company_id_that_does_not_exist() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: Some(2), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_selected_company_id_that_is_inactive() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: false, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: Some(1), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_duplicate_train_ids() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: vec![ + RuntimeTrain { + train_id: 7, + owner_company_id: 1, + territory_id: None, + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 7, + owner_company_id: 1, + territory_id: None, + locomotive_name: Some("Orca".to_string()), + active: true, + retired: false, + }, + ], + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_train_with_unknown_owner_company() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: vec![RuntimeTrain { + train_id: 7, + owner_company_id: 2, + territory_id: None, + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }], + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_train_with_unknown_territory() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: vec![RuntimeTrain { + train_id: 7, + owner_company_id: 1, + territory_id: Some(9), + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }], + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 1, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_train_marked_active_and_retired() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: vec![RuntimeTrain { + train_id: 7, + owner_company_id: 1, + territory_id: None, + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: true, + }], + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_duplicate_company_territory_access_pairs() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + company_territory_track_piece_counts: Vec::new(), + company_territory_access: vec![ + RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 7, + }, + RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 7, + }, + ], + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_company_territory_access_with_unknown_company() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + company_territory_track_piece_counts: Vec::new(), + company_territory_access: vec![RuntimeCompanyTerritoryAccess { + company_id: 2, + territory_id: 7, + }], + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_company_territory_access_with_unknown_territory() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + company_territory_track_piece_counts: Vec::new(), + company_territory_access: vec![RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 8, + }], + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_company_with_unknown_linked_chairman_profile() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(9), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn rejects_mismatched_company_chairman_back_links() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(1), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); +} + +#[test] +fn refreshes_chairman_totals_from_company_market_state() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![ + RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }, + RuntimeCompany { + company_id: 2, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }, + ], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 100, + linked_company_id: Some(1), + company_holdings: BTreeMap::from([(1, 2), (2, 3)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 400, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([ + ( + 1, + RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41200000, + ..RuntimeCompanyMarketState::default() + }, + ), + ( + 2, + RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41a00000, + ..RuntimeCompanyMarketState::default() + }, + ), + ]), + ..RuntimeServiceState::default() + }, + }; + + state.refresh_derived_market_state(); + + assert_eq!(state.chairman_profiles[0].holdings_value_total, 80); + assert_eq!(state.chairman_profiles[0].net_worth_total, 180); + assert_eq!(state.chairman_profiles[0].purchasing_power_total, 400); +} + +#[test] +fn refreshes_chairman_purchasing_power_when_cash_changes() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 50, + linked_company_id: Some(1), + company_holdings: BTreeMap::from([(1, 2)]), + holdings_value_total: 20, + net_worth_total: 70, + purchasing_power_total: 130, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41200000, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + state.chairman_profiles[0].current_cash = 80; + + state.refresh_derived_market_state(); + + assert_eq!(state.chairman_profiles[0].holdings_value_total, 20); + assert_eq!(state.chairman_profiles[0].net_worth_total, 100); + assert_eq!(state.chairman_profiles[0].purchasing_power_total, 130); +} + +#[test] +fn refreshes_company_leaf_fields_from_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 275.0f64.to_bits(); + + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: { + let mut terms = vec![0; 0x3b]; + terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100; + terms + }, + company_market_state: BTreeMap::from([( + 1, + RuntimeCompanyMarketState { + cached_share_price_raw_u32: 37.0f32.to_bits(), + issue_opinion_terms_raw_i32: { + let mut terms = vec![0; 0x3b]; + terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 58; + terms + }, + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x32f, + 2620.0f32.to_bits(), + )]), + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + state.refresh_derived_market_state(); + + assert_eq!(state.companies[0].current_cash, 275); + assert_eq!(state.companies[0].book_value_per_share, 2620); + assert_eq!(state.companies[0].prime_rate, Some(6)); + assert_eq!(state.companies[0].investor_confidence, 37); + assert_eq!(state.companies[0].management_attitude, 58); +} + +#[test] +fn seeds_company_periodic_side_latch_state_from_company_market_state() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + }], + selected_company_id: Some(1), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + RuntimeCompanyMarketState { + city_connection_latch: true, + linked_transit_latch: false, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + state.refresh_derived_market_state(); + + assert_eq!( + state + .service_state + .company_periodic_side_latch_state + .get(&1), + Some(&RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: None, + city_connection_latch: true, + linked_transit_latch: false, + }) + ); +} + +#[test] +fn preserves_company_periodic_side_latch_state_without_market_projection() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + }], + selected_company_id: Some(1), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_periodic_side_latch_state: BTreeMap::from([( + 1, + RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: Some(2), + city_connection_latch: true, + linked_transit_latch: false, + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + state.refresh_derived_market_state(); + + assert_eq!( + state + .service_state + .company_periodic_side_latch_state + .get(&1), + Some(&RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: Some(2), + city_connection_latch: true, + linked_transit_latch: false, + }) + ); +} + +#[test] +fn reads_grounded_company_stat_family_slots_from_runtime_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = + 75.0f64.to_bits(); + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 125_000, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 2_620, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + Some(125_000) + ); + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, + }, + ), + Some(2_620) + ); + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: 0x12, + }, + ), + Some(75) + ); + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: 0x2b, + }, + ), + Some(0) + ); + assert_eq!( + runtime_company_stat_value( + &state, + 99, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + None + ); +} + +#[test] +fn reads_book_value_per_share_from_rehosted_direct_company_field_band() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x32f, + 2620.0f32.to_bits(), + )]), + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!(runtime_company_book_value_per_share(&state, 7), Some(2620)); + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, + }, + ), + Some(2620) + ); +} + +#[test] +fn reads_investor_confidence_from_rehosted_company_share_price_cache() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + absolute_counter_raw_u32: Some(5), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + recent_per_share_cache_absolute_counter: 5, + recent_per_share_cached_value_bits: 14.5f64.to_bits(), + recent_per_share_subscore_raw_u32: 12.0f32.to_bits(), + cached_share_price_raw_u32: 37.0f32.to_bits(), + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_company_recent_per_share_subscore(&state, 7), + Some(14.5) + ); + assert_eq!(runtime_company_investor_confidence(&state, 7), Some(37)); + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: 0x13, + }, + ), + Some(37) + ); +} + +#[test] +fn derives_company_management_attitude_from_issue3a_owner_state() { + let mut company_terms = vec![0; 0x3b]; + company_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 12; + let mut chairman_terms = vec![0; 0x3b]; + chairman_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 6; + let mut world_terms = vec![0; 0x3b]; + world_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 40; + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(3), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 3, + name: "Chairman".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(7), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: world_terms, + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + issue_opinion_terms_raw_i32: company_terms, + ..RuntimeCompanyMarketState::default() + }, + )]), + chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(3, chairman_terms)]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!(runtime_company_management_attitude(&state, 7), Some(58)); +} + +#[test] +fn reads_year_relative_company_stat_family_from_saved_market_matrix() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_year_value = |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10.0); + write_year_value(&mut year_stat_family_qword_bits, 0x02, 1, 20.0); + write_year_value(&mut year_stat_family_qword_bits, 0x03, 1, 30.0); + write_year_value(&mut year_stat_family_qword_bits, 0x04, 1, 40.0); + write_year_value(&mut year_stat_family_qword_bits, 0x05, 1, 5.0); + write_year_value(&mut year_stat_family_qword_bits, 0x06, 1, 6.0); + write_year_value(&mut year_stat_family_qword_bits, 0x07, 1, 7.0); + write_year_value(&mut year_stat_family_qword_bits, 0x08, 1, 8.0); + write_year_value(&mut year_stat_family_qword_bits, 0x09, 1, 9.0); + write_year_value(&mut year_stat_family_qword_bits, 0x0a, 1, 10.0); + write_year_value(&mut year_stat_family_qword_bits, 0x0b, 1, 11.0); + write_year_value(&mut year_stat_family_qword_bits, 0x0c, 1, 12.0); + write_year_value(&mut year_stat_family_qword_bits, 0x0d, 1, 13.0); + write_year_value(&mut year_stat_family_qword_bits, 0x0e, 1, 14.0); + write_year_value(&mut year_stat_family_qword_bits, 0x0f, 1, 15.0); + write_year_value(&mut year_stat_family_qword_bits, 0x10, 1, 16.0); + write_year_value(&mut year_stat_family_qword_bits, 0x11, 1, 17.0); + write_year_value(&mut year_stat_family_qword_bits, 0x12, 1, 18.0); + write_year_value(&mut year_stat_family_qword_bits, 0x16, 1, 4.0); + write_year_value(&mut year_stat_family_qword_bits, 0x17, 1, 10.0); + write_year_value(&mut year_stat_family_qword_bits, 0x18, 1, 20.0); + write_year_value(&mut year_stat_family_qword_bits, 0x19, 1, 25.0); + write_year_value(&mut year_stat_family_qword_bits, 0x1a, 1, 50.0); + write_year_value(&mut year_stat_family_qword_bits, 0x1b, 1, 100.0); + write_year_value(&mut year_stat_family_qword_bits, 0x24, 1, 5.0); + + let mut special_stat_family_232a_qword_bits = + vec![0u64; RUNTIME_COMPANY_STAT_SLOT_COUNT as usize]; + special_stat_family_232a_qword_bits[RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize] = + 111.0f64.to_bits(); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 125_000, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 2_620, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + year_stat_family_qword_bits, + special_stat_family_232a_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let prior_year = RuntimeCompanyStatSelector { + family_id: 1844, + slot_id: 0x09, + }; + assert_eq!( + runtime_company_stat_value_f64(&state, 7, prior_year), + Some(9.0) + ); + assert_eq!( + runtime_company_stat_value_f64( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: 1844, + slot_id: 0x2c, + }, + ), + Some(100.0) + ); + assert_eq!( + runtime_company_stat_value_f64( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: 1844, + slot_id: 0x2b, + }, + ), + Some(168.0) + ); + assert_eq!( + runtime_company_stat_value_f64( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: 1844, + slot_id: 0x32, + }, + ), + Some(20.0) + ); + assert_eq!( + runtime_company_stat_value_f64( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: 1844, + slot_id: 0x38, + }, + ), + Some(1.0) + ); + assert_eq!( + runtime_company_stat_value_f64( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + Some(111.0) + ); +} + +#[test] +fn carries_trailing_full_year_finance_lanes_into_annual_finance_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_year_value = |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + for (year_delta, revenue_parts, extra_profit_parts, fuel_cost) in [ + ( + 1, + [60.0, 50.0, 40.0, 30.0], + [10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0], + 18.0, + ), + ( + 2, + [50.0, 45.0, 40.0, 35.0], + [9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0], + 17.0, + ), + ( + 3, + [50.0, 40.0, 35.0, 35.0], + [8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0], + 16.0, + ), + ( + 4, + [45.0, 40.0, 35.0, 30.0], + [7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0], + 15.0, + ), + ] { + for (slot_id, value) in [0x01, 0x02, 0x03, 0x04].into_iter().zip(revenue_parts) { + write_year_value(&mut year_stat_family_qword_bits, slot_id, year_delta, value); + } + for (slot_id, value) in [0x05, 0x06, 0x07, 0x08, 0x0a, 0x0b, 0x0c] + .into_iter() + .zip(extra_profit_parts) + { + write_year_value(&mut year_stat_family_qword_bits, slot_id, year_delta, value); + } + write_year_value( + &mut year_stat_family_qword_bits, + 0x09, + year_delta, + fuel_cost, + ); + } + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 125_000, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 2_620, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let finance_state = + runtime_company_annual_finance_state(&state, 7).expect("annual finance state"); + assert_eq!( + finance_state.trailing_full_year_year_words, + vec![1844, 1843, 1842, 1841] + ); + assert_eq!( + finance_state.trailing_full_year_net_profits, + vec![247, 229, 211, 193] + ); + assert_eq!( + finance_state.trailing_full_year_revenues, + vec![180, 170, 160, 150] + ); + assert_eq!( + finance_state.trailing_full_year_fuel_costs, + vec![18, 17, 16, 15] + ); +} diff --git a/crates/rrt-runtime/src/derived/tests/finance.rs b/crates/rrt-runtime/src/derived/tests/finance.rs new file mode 100644 index 0000000..a69507e --- /dev/null +++ b/crates/rrt-runtime/src/derived/tests/finance.rs @@ -0,0 +1,1565 @@ +use super::*; + +#[test] +fn skips_company_periodic_route_preference_override_without_electric_preference() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + auto_show_grade_during_track_lay_raw_u8: Some(3), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 8, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(8), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 8, + RuntimeCompanyMarketState { + city_connection_latch: true, + linked_transit_latch: false, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_apply_company_periodic_route_preference_override(&mut state, 8), + None + ); + assert_eq!( + state.world_restore.auto_show_grade_during_track_lay_raw_u8, + Some(3) + ); + assert_eq!( + state + .service_state + .active_periodic_route_preference_override, + None + ); + assert_eq!( + state.service_state.last_periodic_route_preference_override, + None + ); +} + +#[test] +fn derives_annual_creditor_pressure_from_rehosted_finance_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x0d, -700_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x12, 0.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 80_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -115_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -110_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + founding_year: 1841, + last_bankruptcy_year: 1832, + cached_share_price_raw_u32: 25.0f32.to_bits(), + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!(runtime_world_annual_finance_mode_active(&state), Some(true)); + assert_eq!(runtime_world_bankruptcy_allowed(&state), Some(true)); + let pressure_state = + runtime_company_annual_creditor_pressure_state(&state, 7).expect("creditor pressure state"); + assert_eq!(pressure_state.recent_bad_net_profit_year_count, 3); + assert_eq!(pressure_state.recent_peak_revenue, Some(100_000)); + assert_eq!( + pressure_state.recent_three_year_net_profit_total, + Some(-65_000) + ); + assert_eq!(pressure_state.pressure_ladder_cash_floor, Some(-600_000)); + assert_eq!( + pressure_state.current_cash_plus_slot_12_total, + Some(-700_000) + ); + assert_eq!(pressure_state.support_adjusted_share_price_floor, Some(20)); + assert_eq!(pressure_state.support_adjusted_share_price_scalar, Some(25)); + assert_eq!(pressure_state.current_fuel_cost, Some(-50_000)); + assert_eq!(pressure_state.current_fuel_cost_floor, Some(-48_000)); + assert!(pressure_state.eligible_for_bankruptcy_branch); +} + +#[test] +fn derives_annual_deep_distress_bankruptcy_fallback_from_rehosted_finance_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, -350_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 9, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 9, + RuntimeCompanyMarketState { + founding_year: 1841, + last_bankruptcy_year: 1840, + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let pressure_state = + runtime_company_annual_deep_distress_state(&state, 9).expect("deep distress state"); + assert_eq!(pressure_state.current_cash, Some(-350_000)); + assert_eq!( + pressure_state.recent_first_three_net_profit_years, + vec![-25_000, -23_000, -21_000] + ); + assert_eq!(pressure_state.deep_distress_cash_floor, Some(-300_000)); + assert_eq!(pressure_state.deep_distress_net_profit_floor, Some(-20_000)); + assert!(pressure_state.eligible_for_bankruptcy_fallback); +} + +#[test] +fn derives_annual_stock_repurchase_state_from_rehosted_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 1_600_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + partial_year_progress_raw_u8: Some(0x0c), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(1), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 12, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(3), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 3, + name: "Jay".to_string(), + active: true, + current_cash: 200, + linked_company_id: Some(12), + company_holdings: BTreeMap::from([(12, 14_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 12, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + cached_share_price_raw_u32: 20.0f32.to_bits(), + founding_year: 1835, + city_connection_latch: true, + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + chairman_personality_raw_u8: BTreeMap::from([(3, 20)]), + ..RuntimeServiceState::default() + }, + }; + + let repurchase_state = + runtime_company_annual_stock_repurchase_state(&state, 12).expect("stock repurchase state"); + assert_eq!(repurchase_state.building_density_growth_setting, Some(1)); + assert_eq!( + repurchase_state.linked_chairman_personality_raw_u8, + Some(20) + ); + assert_eq!(repurchase_state.repurchase_batch_size, Some(1_000)); + assert_eq!(repurchase_state.repurchase_factor_basis_points, Some(432)); + assert_eq!(repurchase_state.current_cash, Some(1_600_000)); + assert_eq!( + repurchase_state.stock_value_gate_cash_floor, + Some(3_456_000) + ); + assert_eq!( + repurchase_state.support_adjusted_share_price_scalar, + Some(20) + ); + assert_eq!(repurchase_state.affordability_cash_floor, Some(103_680)); + assert_eq!(repurchase_state.unassigned_share_pool, Some(5_500)); + assert!(!repurchase_state.eligible_for_single_batch_repurchase); +} + +#[test] +fn derives_annual_bond_policy_state_from_rehosted_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, -400_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + partial_year_progress_raw_u8: Some(0x0c), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 11, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 11, + RuntimeCompanyMarketState { + bond_count: 2, + linked_transit_latch: true, + live_bond_slots: vec![ + RuntimeCompanyBondSlot { + slot_index: 0, + principal: 200_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.09f32.to_bits(), + }, + RuntimeCompanyBondSlot { + slot_index: 1, + principal: 150_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }, + ], + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let bond_state = + runtime_company_annual_bond_policy_state(&state, 11).expect("bond policy state"); + assert_eq!(bond_state.live_bond_count, Some(2)); + assert_eq!(bond_state.live_bond_principal_total, Some(350_000)); + assert_eq!(bond_state.matured_live_bond_count, Some(0)); + assert_eq!(bond_state.matured_live_bond_principal_total, Some(0)); + assert_eq!(bond_state.next_live_bond_maturity_year, None); + assert_eq!(bond_state.live_bond_coupon_burden_total, Some(30_000)); + assert_eq!(bond_state.current_cash, Some(-400_000)); + assert_eq!(bond_state.cash_after_full_repayment, Some(-750_000)); + assert_eq!(bond_state.issue_cash_floor, Some(-30_000)); + assert_eq!(bond_state.issue_principal_step, Some(500_000)); + assert_eq!(bond_state.proposed_issue_bond_count, Some(2)); + assert_eq!(bond_state.proposed_issue_total_principal, Some(1_000_000)); + assert_eq!(bond_state.proposed_issue_years_to_maturity, Some(30)); + assert!(bond_state.eligible_for_bond_issue_branch); +} + +#[test] +fn classifies_annual_bond_principal_flow_relation() { + assert_eq!( + runtime_annual_bond_principal_flow_relation_label(0, 0), + None + ); + assert_eq!( + runtime_annual_bond_principal_flow_relation_label(350_000, 0), + Some("retired_only") + ); + assert_eq!( + runtime_annual_bond_principal_flow_relation_label(0, 500_000), + Some("issued_only") + ); + assert_eq!( + runtime_annual_bond_principal_flow_relation_label(350_000, 1_000_000), + Some("issued_exceeds_retired") + ); + assert_eq!( + runtime_annual_bond_principal_flow_relation_label(500_000, 500_000), + Some("retired_equals_issued") + ); +} + +#[test] +fn maps_annual_bond_news_selector_from_principal_flow_relation() { + assert_eq!( + runtime_annual_finance_news_family_candidate_label( + RuntimeCompanyAnnualFinancePolicyAction::BondIssue, + 500_000, + 500_000, + 0, + 0, + ), + Some("2882") + ); + assert_eq!( + runtime_annual_finance_news_family_candidate_label( + RuntimeCompanyAnnualFinancePolicyAction::BondIssue, + 350_000, + 1_000_000, + 0, + 0, + ), + Some("2883") + ); + assert_eq!( + runtime_annual_finance_news_family_candidate_label( + RuntimeCompanyAnnualFinancePolicyAction::BondIssue, + 900_000, + 500_000, + 0, + 0, + ), + Some("2884") + ); + assert_eq!( + runtime_annual_finance_news_family_candidate_label( + RuntimeCompanyAnnualFinancePolicyAction::BondIssue, + 350_000, + 0, + 0, + 0, + ), + Some("2885") + ); + assert_eq!( + runtime_annual_finance_news_family_candidate_label( + RuntimeCompanyAnnualFinancePolicyAction::BondIssue, + 0, + 500_000, + 0, + 0, + ), + Some("2886") + ); +} + +#[test] +fn annual_bond_policy_stays_eligible_for_repayment_without_new_issue() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 900_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + partial_year_progress_raw_u8: Some(0x0c), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 12, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 12, + RuntimeCompanyMarketState { + bond_count: 2, + live_bond_slots: vec![ + RuntimeCompanyBondSlot { + slot_index: 0, + principal: 200_000, + maturity_year: 1845, + coupon_rate_raw_u32: 0.09f32.to_bits(), + }, + RuntimeCompanyBondSlot { + slot_index: 1, + principal: 150_000, + maturity_year: 1847, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }, + ], + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let bond_state = + runtime_company_annual_bond_policy_state(&state, 12).expect("bond policy state"); + assert_eq!(bond_state.live_bond_principal_total, Some(350_000)); + assert_eq!(bond_state.cash_after_full_repayment, Some(550_000)); + assert_eq!(bond_state.proposed_issue_bond_count, Some(0)); + assert!(bond_state.eligible_for_bond_issue_branch); +} + +#[test] +fn derives_annual_stock_issue_state_from_rehosted_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 250_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + partial_year_progress_raw_u8: Some(0x0c), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + issue_37_value: Some(2), + issue_37_multiplier_raw_u32: Some(1.0f32.to_bits()), + issue_37_multiplier_value_f32_text: Some("1.000000".to_string()), + absolute_counter_raw_u32: Some(885_911_040), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 14, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(8), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 8, + name: "Taylor".to_string(), + active: true, + current_cash: 200, + linked_company_id: Some(14), + company_holdings: BTreeMap::from([(14, 14_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + chairman_personality_raw_u8: BTreeMap::from([(8, 20)]), + company_market_state: BTreeMap::from([( + 14, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 2, + highest_coupon_live_bond_principal: Some(300_000), + current_issue_calendar_word: 0x0101_0725, + current_issue_calendar_word_2: 0x0001_0001, + founding_year: 1840, + cached_share_price_raw_u32: 35.0f32.to_bits(), + recent_per_share_cache_absolute_counter: 885_911_040, + recent_per_share_cached_value_bits: 34.0f64.to_bits(), + city_connection_latch: false, + live_bond_slots: vec![ + RuntimeCompanyBondSlot { + slot_index: 0, + principal: 300_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.11f32.to_bits(), + }, + RuntimeCompanyBondSlot { + slot_index: 1, + principal: 200_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.07f32.to_bits(), + }, + ], + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x32f, + 30.0f32.to_bits(), + )]), + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let stock_issue_state = + runtime_company_annual_stock_issue_state(&state, 14).expect("stock issue state"); + assert_eq!(stock_issue_state.live_bond_count, Some(2)); + assert_eq!(stock_issue_state.initial_issue_batch_size, Some(2_000)); + assert_eq!(stock_issue_state.trimmed_issue_batch_size, Some(2_000)); + assert_eq!(stock_issue_state.share_pressure_basis_points, Some(-1_000)); + assert_eq!( + stock_issue_state.pressured_support_adjusted_share_price_scalar, + Some(35) + ); + assert_eq!(stock_issue_state.pressured_proceeds, Some(70_000)); + assert_eq!( + stock_issue_state.book_value_per_share_floor_applied, + Some(30) + ); + assert_eq!( + stock_issue_state.price_to_book_ratio_basis_points, + Some(11_667) + ); + assert_eq!( + stock_issue_state.highest_coupon_live_bond_rate_basis_points, + Some(1_100) + ); + assert_eq!( + stock_issue_state.minimum_price_to_book_ratio_basis_points, + Some(8_000) + ); + assert_eq!(stock_issue_state.current_cash, Some(250_000)); + assert_eq!( + stock_issue_state.highest_coupon_live_bond_principal, + Some(300_000) + ); + assert_eq!( + stock_issue_state.current_issue_age_absolute_counter_delta, + Some(967_680) + ); + assert_eq!( + stock_issue_state.current_issue_cooldown_floor, + Some(483_840) + ); + assert_eq!(stock_issue_state.passes_share_price_floor, Some(true)); + assert_eq!(stock_issue_state.passes_proceeds_floor, Some(true)); + assert_eq!(stock_issue_state.passes_cash_gate, Some(true)); + assert_eq!(stock_issue_state.passes_issue_cooldown_gate, Some(true)); + assert_eq!( + stock_issue_state.passes_coupon_price_to_book_gate, + Some(true) + ); + assert!(stock_issue_state.eligible_for_double_tranche_issue); +} + +#[test] +fn derives_annual_dividend_policy_state_from_rehosted_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + dividend_policy_raw_u8: Some(0), + dividend_adjustment_allowed: Some(true), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(1), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 15, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 3, + name: "Chairman Three".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(15), + company_holdings: BTreeMap::from([(15, 9_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 15, + RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1840, + last_dividend_year: 1844, + year_stat_family_qword_bits, + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x33f, + 0.4f32.to_bits(), + )]), + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let dividend_state = runtime_company_annual_dividend_policy_state(&state, 15) + .expect("annual dividend policy state"); + assert_eq!(dividend_state.years_since_last_dividend, Some(1)); + assert_eq!(dividend_state.years_since_founding, Some(5)); + assert_eq!(dividend_state.outstanding_shares, Some(10_000)); + assert_eq!(dividend_state.unassigned_share_pool, Some(500)); + assert_eq!( + dividend_state.weighted_recent_net_profit_total, + Some(600_000) + ); + assert_eq!( + dividend_state.weighted_recent_net_profit_average, + Some(100_000) + ); + assert_eq!(dividend_state.current_cash, Some(300_000)); + assert!(dividend_state.tiny_unassigned_share_cash_supplement_branch); + assert_eq!( + dividend_state.tentative_target_dividend_per_share_tenths, + Some(133) + ); + assert_eq!(dividend_state.current_dividend_per_share_tenths, Some(4)); + assert_eq!( + dividend_state.growth_adjusted_current_dividend_per_share_tenths, + Some(3) + ); + assert_eq!( + dividend_state.board_approved_dividend_rate_ceiling_tenths, + Some(18) + ); + assert_eq!(dividend_state.proposed_dividend_per_share_tenths, Some(18)); + assert!(dividend_state.eligible_for_dividend_adjustment_branch); +} + +#[test] +fn derives_annual_finance_policy_action_from_branch_priority_order() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + dividend_policy_raw_u8: Some(0), + dividend_adjustment_allowed: Some(true), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(1), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 16, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(4), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(16), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 4, + name: "Chairman Four".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(16), + company_holdings: BTreeMap::from([(16, 9_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + chairman_personality_raw_u8: BTreeMap::from([(4, 20)]), + company_market_state: BTreeMap::from([( + 16, + RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1840, + last_dividend_year: 1844, + city_connection_latch: true, + year_stat_family_qword_bits, + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x33f, + 0.4f32.to_bits(), + )]), + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let policy_state = runtime_company_annual_finance_policy_state(&state, 16) + .expect("annual finance policy state"); + assert_eq!( + policy_state.action, + RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment + ); + assert!(!policy_state.stock_repurchase_eligible); + assert!(!policy_state.stock_issue_eligible); + assert!(policy_state.dividend_adjustment_eligible); +} + +#[test] +fn reads_company_market_metrics_from_annual_finance_reader() { + let current_issue_calendar_word = 0x0101_0726; + let current_issue_calendar_word_2 = 0x0001_0001; + let prior_issue_calendar_word = 0x0101_0725; + let prior_issue_calendar_word_2 = 0x0001_0001; + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + absolute_counter_raw_u32: Some(885_427_260), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(7), + company_holdings: BTreeMap::from([(7, 12_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 2, + largest_live_bond_principal: Some(500_000), + highest_coupon_live_bond_principal: Some(300_000), + cached_share_price_raw_u32: 0x42200000, + chairman_salary_baseline: 18, + chairman_salary_current: 27, + chairman_bonus_year: 1843, + chairman_bonus_amount: 625, + founding_year: 1832, + last_bankruptcy_year: 0, + last_dividend_year: 1842, + current_issue_calendar_word, + current_issue_calendar_word_2, + prior_issue_calendar_word, + prior_issue_calendar_word_2, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::OutstandingShares + ), + Some(20_000) + ); + assert_eq!( + runtime_company_market_metric_value(&state, 7, RuntimeCompanyMarketMetric::BondCount), + Some(2) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::LargestLiveBondPrincipal + ), + Some(500_000) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::HighestCouponLiveBondPrincipal + ), + Some(300_000) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::AssignedSharePool + ), + Some(12_000) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::UnassignedSharePool + ), + Some(8_000) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::CachedSharePrice + ), + Some(40) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::CurrentIssueAbsoluteCounter + ), + Some(885_427_200) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::PriorIssueAbsoluteCounter + ), + Some(884_943_360) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::CurrentIssueAgeAbsoluteCounterDelta + ), + Some(60) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::CurrentIssueCalendarWord + ), + Some(i64::from(current_issue_calendar_word)) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::CurrentIssueCalendarWord2 + ), + Some(i64::from(current_issue_calendar_word_2)) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::PriorIssueCalendarWord + ), + Some(i64::from(prior_issue_calendar_word)) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::PriorIssueCalendarWord2 + ), + Some(i64::from(prior_issue_calendar_word_2)) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::ChairmanSalaryCurrent + ), + Some(27) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 7, + RuntimeCompanyMarketMetric::ChairmanBonusAmount + ), + Some(625) + ); + assert_eq!( + runtime_company_market_metric_value( + &state, + 99, + RuntimeCompanyMarketMetric::OutstandingShares + ), + None + ); +} + +#[test] +fn derives_elapsed_company_finance_years_from_calendar_and_saved_market_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1844, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 3, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 3, + RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1838, + last_bankruptcy_year: 1841, + last_dividend_year: 1843, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let finance_state = + runtime_company_annual_finance_state(&state, 3).expect("finance state should derive"); + assert_eq!(finance_state.years_since_founding, Some(6)); + assert_eq!(finance_state.years_since_last_bankruptcy, Some(3)); + assert_eq!(finance_state.years_since_last_dividend, Some(1)); +} diff --git a/crates/rrt-runtime/src/derived/tests/issue_state.rs b/crates/rrt-runtime/src/derived/tests/issue_state.rs new file mode 100644 index 0000000..4d68c47 --- /dev/null +++ b/crates/rrt-runtime/src/derived/tests/issue_state.rs @@ -0,0 +1,490 @@ +use super::*; + +#[test] +fn reads_grounded_world_issue_state_from_runtime_restore_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(3), + issue_38_value: Some(1), + issue_39_value: Some(2), + issue_3a_value: Some(4), + issue_37_multiplier_raw_u32: Some(0x3d75c28f), + issue_37_multiplier_value_f32_text: Some("0.060000".to_string()), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let issue = runtime_world_issue_state(&state, RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE) + .expect("grounded issue 0x37 state"); + assert_eq!(issue.issue_id, RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE); + assert_eq!(issue.raw_value_u32, 3); + assert_eq!(issue.multiplier_raw_u32, Some(0x3d75c28f)); + assert_eq!(issue.multiplier_value_f32_text.as_deref(), Some("0.060000")); + assert_eq!( + runtime_world_issue_state(&state, RUNTIME_WORLD_ISSUE_CREDIT_MARKET) + .expect("grounded issue 0x38 state") + .raw_value_u32, + 1 + ); + assert_eq!( + runtime_world_issue_state(&state, RUNTIME_WORLD_ISSUE_PRIME_RATE) + .expect("grounded issue 0x39 state") + .raw_value_u32, + 2 + ); + assert_eq!( + runtime_world_issue_state(&state, RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE) + .expect("grounded issue 0x3a state") + .raw_value_u32, + 4 + ); + assert_eq!(runtime_world_issue_state(&state, 0x40), None); + assert_eq!(runtime_world_absolute_counter(&state), None); +} + +#[test] +fn sums_save_native_issue_opinion_terms_with_linked_company_fallback() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(3), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 3, + name: "Chairman".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(7), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: (0..0x3b).map(|value| value as i32).collect(), + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + issue_opinion_terms_raw_i32: (0..0x3b) + .map(|value| (value as i32) * 2) + .collect(), + ..RuntimeCompanyMarketState::default() + }, + )]), + chairman_issue_opinion_terms_raw_i32: BTreeMap::from([( + 3, + (0..0x3b).map(|value| (value as i32) * 3).collect(), + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_world_issue_opinion_term_sum_raw(&state, 0x39, Some(3), None, None), + Some(57 + 114 + 171) + ); + assert_eq!( + runtime_world_issue_opinion_term_sum_raw(&state, 0x39, None, Some(7), None), + Some(57 + 114) + ); +} + +#[test] +fn computes_save_native_issue_opinion_multiplier_with_floor() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 5, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 9, + name: "Chairman".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(5), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: { + let mut values = vec![0; 0x3b]; + values[0x38] = -150; + values + }, + company_market_state: BTreeMap::from([( + 5, + RuntimeCompanyMarketState { + issue_opinion_terms_raw_i32: { + let mut values = vec![0; 0x3b]; + values[0x38] = -60; + values + }, + ..RuntimeCompanyMarketState::default() + }, + )]), + chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(9, { + let mut values = vec![0; 0x3b]; + values[0x38] = 50; + values + })]), + ..RuntimeServiceState::default() + }, + }; + + let multiplier = runtime_world_issue_opinion_multiplier(&state, 0x38, Some(9), None, None) + .expect("issue multiplier"); + assert!((multiplier - 0.01).abs() < f64::EPSILON); +} + +#[test] +fn reads_grounded_world_absolute_counter_from_runtime_restore_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + absolute_counter_raw_u32: Some(5), + absolute_counter_mirror_raw_u32: Some(5), + packed_year_word_raw_u16: Some(0x0210), + partial_year_progress_raw_u8: Some(8), + current_calendar_tuple_word_raw_u32: Some(0x0108_0210), + current_calendar_tuple_word_2_raw_u32: Some(0x35e6_3160), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert_eq!(runtime_world_absolute_counter(&state), Some(5)); + assert_eq!( + runtime_world_partial_year_weight_numerator(&state), + Some(35) + ); +} + +#[test] +fn derives_prime_rate_baseline_from_saved_world_raw_word() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert_eq!(runtime_world_prime_rate_baseline(&state), Some(5.0)); +} + +#[test] +fn derives_company_prime_rate_from_issue39_owner_state() { + let mut company_terms = vec![0; 0x3b]; + company_terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 50; + let mut chairman_terms = vec![0; 0x3b]; + chairman_terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 25; + let mut world_terms = vec![0; 0x3b]; + world_terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100; + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(3), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 3, + name: "Chairman".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(7), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: world_terms, + company_market_state: BTreeMap::from([( + 7, + RuntimeCompanyMarketState { + issue_opinion_terms_raw_i32: company_terms, + ..RuntimeCompanyMarketState::default() + }, + )]), + chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(3, chairman_terms)]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!(runtime_company_prime_rate(&state, 7), Some(7)); +} diff --git a/crates/rrt-runtime/src/derived/tests/mod.rs b/crates/rrt-runtime/src/derived/tests/mod.rs new file mode 100644 index 0000000..a9f5d15 --- /dev/null +++ b/crates/rrt-runtime/src/derived/tests/mod.rs @@ -0,0 +1,15 @@ +use super::*; +use crate::event::effects::*; +use crate::event::metrics::*; +use crate::event::packed::*; +use crate::event::records::*; +use crate::event::targets::*; +use crate::state::*; +use std::collections::BTreeMap; + +mod bond_metrics; +mod company; +mod finance; +mod issue_state; +mod selected_year; +mod share_price; diff --git a/crates/rrt-runtime/src/derived/tests/selected_year.rs b/crates/rrt-runtime/src/derived/tests/selected_year.rs new file mode 100644 index 0000000..09ba12a --- /dev/null +++ b/crates/rrt-runtime/src/derived/tests/selected_year.rs @@ -0,0 +1,213 @@ +use super::*; + +#[test] +fn decodes_and_packs_company_issue_calendar_tuple() { + let tuple = runtime_decode_packed_calendar_tuple(0x0101_0726, 0x0001_0001); + assert_eq!( + tuple, + RuntimePackedCalendarTuple { + year_word: 0x0726, + month_1_based: 1, + week_1_based: 1, + day_1_based: 1, + hour_0_based: 0, + quarter_day_1_based: 1, + minute_0_based: 0, + } + ); + assert_eq!( + runtime_pack_packed_calendar_tuple_to_absolute_counter(tuple), + Some(885_427_200) + ); + assert_eq!( + runtime_pack_packed_calendar_tuple_to_absolute_counter(RuntimePackedCalendarTuple { + year_word: 1830, + month_1_based: 13, + week_1_based: 1, + day_1_based: 1, + hour_0_based: 0, + quarter_day_1_based: 1, + minute_0_based: 0, + }), + None + ); +} + +#[test] +fn derives_and_encodes_packed_calendar_tuple_from_runtime_calendar() { + let tuple = runtime_derive_packed_calendar_tuple_from_calendar_point(CalendarPoint { + year: 1830, + month_slot: 11, + phase_slot: 27, + tick_slot: 179, + }) + .expect("runtime calendar tuple"); + assert_eq!( + tuple, + RuntimePackedCalendarTuple { + year_word: 1830, + month_1_based: 12, + week_1_based: 4, + day_1_based: 28, + hour_0_based: 23, + quarter_day_1_based: 4, + minute_0_based: 52, + } + ); + assert_eq!( + runtime_encode_packed_calendar_tuple(tuple), + (0x040c_0726, 0x3404_171c) + ); +} + +#[test] +fn derives_selected_year_gap_scalar_from_year_word() { + assert_eq!( + runtime_world_selected_year_bucket_scalar_from_year_word(1830), + Some(25.0) + ); + assert_eq!( + runtime_world_selected_year_bucket_scalar_from_year_word(1835), + Some(31.5) + ); + assert_eq!( + runtime_world_selected_year_bucket_scalar_from_year_word(2000), + Some(123.0) + ); + let bands = runtime_selected_year_bucket_bands_from_scalar(25.0) + .expect("selected-year bucket companion bands"); + assert!((bands.direct[0] - 22.5).abs() < 1e-6); + assert!((bands.direct[1] - 26.25).abs() < 1e-5); + assert!((bands.direct[2] - 17.5).abs() < 1e-6); + assert!((bands.complement[0] - 0.999121).abs() < 1e-6); + assert!((bands.scaled_companion[0] - 139.16667).abs() < 1e-4); + assert_eq!( + runtime_world_selected_year_gap_scalar_from_year_word(1830), + Some((1.0f32 / 3.0).clamp(1.0 / 3.0, 1.0)) + ); + assert_eq!( + runtime_world_selected_year_gap_scalar_from_year_word(1900), + Some((50.0f32 / 150.0).clamp(1.0 / 3.0, 1.0)) + ); + assert_eq!( + runtime_world_selected_year_gap_scalar_from_year_word(2000), + Some(1.0) + ); +} + +#[test] +fn refreshes_selected_year_gap_scalar_from_world_restore_calendar() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1900), + economic_tuning_lane_raw_u32: vec![0x3f400000], + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + state.refresh_derived_world_state(); + + assert_eq!( + state.world_restore.economic_tuning_mirror_raw_u32, + Some(0x3f400000) + ); + assert_eq!( + state + .world_restore + .economic_tuning_mirror_value_f32_text + .as_deref(), + Some("0.750000") + ); + assert_eq!( + state.world_restore.selected_year_bucket_scalar_raw_u32, + Some(70.0f32.to_bits()) + ); + assert_eq!( + state + .world_restore + .selected_year_bucket_scalar_value_f32_text + .as_deref(), + Some("70.000000") + ); + assert_eq!( + state + .world_restore + .selected_year_bucket_direct_lane_value_f32_text, + vec![ + "63.000000".to_string(), + "73.500000".to_string(), + "49.000000".to_string() + ] + ); + assert_eq!( + state + .world_restore + .selected_year_bucket_complement_lane_value_f32_text, + vec![ + "0.998400".to_string(), + "0.998213".to_string(), + "0.998649".to_string() + ] + ); + assert_eq!( + state + .world_restore + .selected_year_bucket_scaled_companion_lane_value_f32_text, + vec![ + "64.166672".to_string(), + "58.214291".to_string(), + "76.071426".to_string() + ] + ); + assert_eq!( + state.world_restore.selected_year_gap_scalar_raw_u32, + Some(((50.0f32 / 150.0).clamp(1.0 / 3.0, 1.0)).to_bits()) + ); + assert_eq!( + state + .world_restore + .selected_year_gap_scalar_value_f32_text + .as_deref(), + Some("0.333333") + ); +} diff --git a/crates/rrt-runtime/src/derived/tests/share_price.rs b/crates/rrt-runtime/src/derived/tests/share_price.rs new file mode 100644 index 0000000..5d3418d --- /dev/null +++ b/crates/rrt-runtime/src/derived/tests/share_price.rs @@ -0,0 +1,598 @@ +use super::*; + +#[test] +fn derives_company_unassigned_share_pool_from_market_state_and_holdings() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 4, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(4), + company_holdings: BTreeMap::from([(4, 8_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 0, + linked_company_id: None, + company_holdings: BTreeMap::from([(4, 7_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + ], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 4, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_company_unassigned_share_pool(&state, 4), + Some(4_500) + ); + assert_eq!(runtime_company_unassigned_share_pool(&state, 99), None); +} + +#[test] +fn derives_company_annual_finance_state_from_owned_runtime_market_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 4, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(4), + company_holdings: BTreeMap::from([(4, 8_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 0, + linked_company_id: None, + company_holdings: BTreeMap::from([(4, 7_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + ], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 4, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 3, + largest_live_bond_principal: Some(650_000), + highest_coupon_live_bond_principal: Some(500_000), + cached_share_price_raw_u32: 0x42200000, + chairman_salary_baseline: 24, + chairman_salary_current: 30, + chairman_bonus_year: 1842, + chairman_bonus_amount: 750, + founding_year: 1831, + last_bankruptcy_year: 0, + last_dividend_year: 1841, + current_issue_calendar_word: 5, + current_issue_calendar_word_2: 6, + prior_issue_calendar_word: 4, + prior_issue_calendar_word_2: 5, + city_connection_latch: true, + linked_transit_latch: false, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!(runtime_company_assigned_share_pool(&state, 4), Some(15_500)); + assert_eq!( + runtime_company_annual_finance_state(&state, 4), + Some(RuntimeCompanyAnnualFinanceState { + company_id: 4, + outstanding_shares: 20_000, + bond_count: 3, + largest_live_bond_principal: Some(650_000), + highest_coupon_live_bond_principal: Some(500_000), + live_bond_coupon_burden_total: Some(0), + assigned_share_pool: 15_500, + unassigned_share_pool: 4_500, + cached_share_price: Some(40), + chairman_salary_baseline: 24, + chairman_salary_current: 30, + chairman_bonus_year: 1842, + chairman_bonus_amount: 750, + founding_year: 1831, + last_bankruptcy_year: 0, + last_dividend_year: 1841, + years_since_founding: None, + years_since_last_bankruptcy: None, + years_since_last_dividend: None, + current_partial_year_weight_numerator: None, + trailing_full_year_year_words: Vec::new(), + trailing_full_year_net_profits: Vec::new(), + trailing_full_year_revenues: Vec::new(), + trailing_full_year_fuel_costs: Vec::new(), + current_issue_absolute_counter: None, + prior_issue_absolute_counter: None, + current_issue_age_absolute_counter_delta: None, + current_issue_calendar_word: 5, + current_issue_calendar_word_2: 6, + prior_issue_calendar_word: 4, + prior_issue_calendar_word_2: 5, + preferred_locomotive_engine_type_raw_u8: None, + city_connection_latch: true, + linked_transit_latch: false, + }) + ); + assert_eq!(runtime_company_assigned_share_pool(&state, 99), None); + assert_eq!(runtime_company_annual_finance_state(&state, 99), None); +} + +#[test] +fn derives_company_periodic_service_state_from_side_latches_and_world_route_preference() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + auto_show_grade_during_track_lay_raw_u8: Some(1), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 4, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(4), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 4, + RuntimeCompanyMarketState { + city_connection_latch: true, + linked_transit_latch: false, + ..RuntimeCompanyMarketState::default() + }, + )]), + company_periodic_side_latch_state: BTreeMap::from([( + 4, + RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: Some(2), + city_connection_latch: false, + linked_transit_latch: true, + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_company_periodic_service_state(&state, 4), + Some(RuntimeCompanyPeriodicServiceState { + company_id: 4, + preferred_locomotive_engine_type_raw_u8: Some(2), + city_connection_latch: false, + linked_transit_latch: true, + base_route_preference_raw_u8: Some(1), + effective_route_preference_raw_u8: Some(2), + electric_route_preference_override_active: true, + effective_route_quality_multiplier_basis_points: 180, + }) + ); + assert_eq!(runtime_company_periodic_service_state(&state, 99), None); +} + +#[test] +fn periodic_service_state_falls_back_to_market_latches_and_world_base_route_preference() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + auto_show_grade_during_track_lay_raw_u8: Some(3), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 8, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(8), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 8, + RuntimeCompanyMarketState { + city_connection_latch: true, + linked_transit_latch: false, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_company_periodic_service_state(&state, 8), + Some(RuntimeCompanyPeriodicServiceState { + company_id: 8, + preferred_locomotive_engine_type_raw_u8: None, + city_connection_latch: true, + linked_transit_latch: false, + base_route_preference_raw_u8: Some(3), + effective_route_preference_raw_u8: Some(3), + electric_route_preference_override_active: false, + effective_route_quality_multiplier_basis_points: 140, + }) + ); +} + +#[test] +fn applies_and_restores_company_periodic_route_preference_override() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + auto_show_grade_during_track_lay_raw_u8: Some(1), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 4, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(4), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 4, + RuntimeCompanyMarketState { + city_connection_latch: true, + linked_transit_latch: false, + ..RuntimeCompanyMarketState::default() + }, + )]), + company_periodic_side_latch_state: BTreeMap::from([( + 4, + RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: Some(2), + city_connection_latch: true, + linked_transit_latch: false, + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let applied = runtime_apply_company_periodic_route_preference_override(&mut state, 4) + .expect("electric override should apply"); + assert_eq!( + applied, + RuntimeWorldRoutePreferenceOverrideState { + company_id: 4, + base_route_preference_raw_u8: Some(1), + effective_route_preference_raw_u8: Some(2), + electric_route_preference_override_active: true, + } + ); + assert_eq!( + state.world_restore.auto_show_grade_during_track_lay_raw_u8, + Some(2) + ); + assert_eq!( + state + .service_state + .active_periodic_route_preference_override, + Some(applied.clone()) + ); + assert_eq!( + state.service_state.last_periodic_route_preference_override, + Some(applied.clone()) + ); + assert_eq!( + state + .service_state + .periodic_route_preference_override_apply_count, + 1 + ); + assert_eq!( + state + .service_state + .periodic_route_preference_override_restore_count, + 0 + ); + + let restored = runtime_restore_company_periodic_route_preference_override(&mut state) + .expect("override should restore"); + assert_eq!(restored, applied); + assert_eq!( + state.world_restore.auto_show_grade_during_track_lay_raw_u8, + Some(1) + ); + assert_eq!( + state + .service_state + .active_periodic_route_preference_override, + None + ); + assert_eq!( + state.service_state.last_periodic_route_preference_override, + Some(restored) + ); + assert_eq!( + state + .service_state + .periodic_route_preference_override_apply_count, + 1 + ); + assert_eq!( + state + .service_state + .periodic_route_preference_override_restore_count, + 1 + ); +} diff --git a/crates/rrt-runtime/src/derived/world/bond_metrics.rs b/crates/rrt-runtime/src/derived/world/bond_metrics.rs new file mode 100644 index 0000000..6dd8643 --- /dev/null +++ b/crates/rrt-runtime/src/derived/world/bond_metrics.rs @@ -0,0 +1,73 @@ +use crate::state::RuntimeState; + +pub fn runtime_company_highest_live_bond_coupon_rate( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + market_state + .live_bond_slots + .iter() + .filter_map(|slot| { + let value = f32::from_bits(slot.coupon_rate_raw_u32) as f64; + value.is_finite().then_some(value) + }) + .max_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal)) +} + +pub fn runtime_company_total_live_bond_principal( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + Some( + market_state + .live_bond_slots + .iter() + .map(|slot| slot.principal) + .sum(), + ) +} + +pub fn runtime_company_matured_live_bond_count( + state: &RuntimeState, + company_id: u32, + current_year_word: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + Some( + market_state + .live_bond_slots + .iter() + .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) + .count() as u32, + ) +} + +pub fn runtime_company_matured_live_bond_principal_total( + state: &RuntimeState, + company_id: u32, + current_year_word: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + Some( + market_state + .live_bond_slots + .iter() + .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) + .map(|slot| slot.principal) + .sum(), + ) +} + +pub fn runtime_company_next_live_bond_maturity_year( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + market_state + .live_bond_slots + .iter() + .filter_map(|slot| (slot.maturity_year != 0).then_some(slot.maturity_year)) + .min() +} diff --git a/crates/rrt-runtime/src/derived/world/calendar.rs b/crates/rrt-runtime/src/derived/world/calendar.rs new file mode 100644 index 0000000..16c995c --- /dev/null +++ b/crates/rrt-runtime/src/derived/world/calendar.rs @@ -0,0 +1,92 @@ +use crate::event::metrics::RuntimePackedCalendarTuple; +use crate::{CalendarPoint, RuntimeState}; + +pub fn runtime_world_absolute_counter(state: &RuntimeState) -> Option { + state.world_restore.absolute_counter_raw_u32 +} + +pub fn runtime_world_partial_year_weight_numerator(state: &RuntimeState) -> Option { + Some(i64::from(state.world_restore.partial_year_progress_raw_u8?) * 5 - 5) +} + +pub fn runtime_decode_packed_calendar_tuple( + word_0: u32, + word_1: u32, +) -> RuntimePackedCalendarTuple { + let bytes_0 = word_0.to_le_bytes(); + let bytes_1 = word_1.to_le_bytes(); + RuntimePackedCalendarTuple { + year_word: u16::from_le_bytes([bytes_0[0], bytes_0[1]]), + month_1_based: bytes_0[2], + week_1_based: bytes_0[3], + day_1_based: bytes_1[0], + hour_0_based: bytes_1[1], + quarter_day_1_based: bytes_1[2], + minute_0_based: bytes_1[3], + } +} + +pub fn runtime_encode_packed_calendar_tuple(tuple: RuntimePackedCalendarTuple) -> (u32, u32) { + let year_bytes = tuple.year_word.to_le_bytes(); + let word_0 = u32::from_le_bytes([ + year_bytes[0], + year_bytes[1], + tuple.month_1_based, + tuple.week_1_based, + ]); + let word_1 = u32::from_le_bytes([ + tuple.day_1_based, + tuple.hour_0_based, + tuple.quarter_day_1_based, + tuple.minute_0_based, + ]); + (word_0, word_1) +} + +pub fn runtime_derive_packed_calendar_tuple_from_calendar_point( + calendar: CalendarPoint, +) -> Option { + let year_word = u16::try_from(calendar.year).ok()?; + let month_1_based = u8::try_from(calendar.month_slot.checked_add(1)?).ok()?; + let day_1_based = u8::try_from(calendar.phase_slot.checked_add(1)?).ok()?; + let week_1_based = u8::try_from(calendar.phase_slot / 7 + 1).ok()?; + let total_minutes = calendar.tick_slot.checked_mul(1440)? / crate::calendar::TICKS_PER_PHASE; + let hour_0_based = u8::try_from(total_minutes / 60).ok()?; + let minute_0_based = u8::try_from(total_minutes % 60).ok()?; + let quarter_day_1_based = u8::try_from((u32::from(hour_0_based) / 6) + 1).ok()?; + Some(RuntimePackedCalendarTuple { + year_word, + month_1_based, + week_1_based, + day_1_based, + hour_0_based, + quarter_day_1_based, + minute_0_based, + }) +} + +pub fn runtime_pack_packed_calendar_tuple_to_absolute_counter( + tuple: RuntimePackedCalendarTuple, +) -> Option { + if !(1..=12).contains(&tuple.month_1_based) { + return None; + } + if !(1..=28).contains(&tuple.day_1_based) { + return None; + } + if tuple.hour_0_based >= 24 { + return None; + } + if tuple.minute_0_based >= 60 { + return None; + } + + let year = u64::from(tuple.year_word); + let month = u64::from(tuple.month_1_based); + let day = u64::from(tuple.day_1_based); + let hour = u64::from(tuple.hour_0_based); + let minute = u64::from(tuple.minute_0_based); + let absolute_counter = + ((((year * 12 + month) * 28 + day).checked_sub(29)? * 24 + hour) * 60) + minute; + u32::try_from(absolute_counter).ok() +} diff --git a/crates/rrt-runtime/src/derived/world/issue_state.rs b/crates/rrt-runtime/src/derived/world/issue_state.rs new file mode 100644 index 0000000..26c4914 --- /dev/null +++ b/crates/rrt-runtime/src/derived/world/issue_state.rs @@ -0,0 +1,206 @@ +use crate::derived::{ + runtime_company_support_adjusted_share_price_scalar, runtime_round_f64_to_i64, +}; +use crate::event::metrics::{ + RUNTIME_WORLD_ISSUE_CREDIT_MARKET, RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE, + RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE, RUNTIME_WORLD_ISSUE_PRIME_RATE, + RuntimeWorldIssueState, +}; +use crate::state::RuntimeState; + +pub fn runtime_company_investor_confidence(state: &RuntimeState, company_id: u32) -> Option { + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + if company.investor_confidence != 0 { + return Some(company.investor_confidence); + } + runtime_company_support_adjusted_share_price_scalar(state, company_id) + .and_then(runtime_round_f64_to_i64) +} + +pub fn runtime_company_management_attitude(state: &RuntimeState, company_id: u32) -> Option { + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + if company.management_attitude != 0 { + return Some(company.management_attitude); + } + runtime_world_issue_opinion_term_sum_raw( + state, + RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE, + company.linked_chairman_profile_id, + Some(company_id), + None, + ) +} + +pub fn runtime_world_issue_opinion_term_sum_raw( + state: &RuntimeState, + issue_id: u32, + chairman_profile_id: Option, + company_id: Option, + territory_id: Option, +) -> Option { + let mut total = i64::from( + *state + .service_state + .world_issue_opinion_base_terms_raw_i32 + .get(issue_id as usize)?, + ); + let mut resolved_company_id = company_id; + if let Some(profile_id) = chairman_profile_id { + if let Some(profile_terms) = state + .service_state + .chairman_issue_opinion_terms_raw_i32 + .get(&profile_id) + { + if let Some(value) = profile_terms.get(issue_id as usize) { + total = total.checked_add(i64::from(*value))?; + } + } + if resolved_company_id.is_none() { + resolved_company_id = state + .chairman_profiles + .iter() + .find(|profile| profile.profile_id == profile_id) + .and_then(|profile| profile.linked_company_id); + } + } + if let Some(company_id) = resolved_company_id { + if let Some(company_terms) = state + .service_state + .company_market_state + .get(&company_id) + .map(|market_state| &market_state.issue_opinion_terms_raw_i32) + { + if let Some(value) = company_terms.get(issue_id as usize) { + total = total.checked_add(i64::from(*value))?; + } + } + } + if territory_id.is_some() { + return None; + } + Some(total) +} + +pub fn runtime_world_issue_opinion_multiplier( + state: &RuntimeState, + issue_id: u32, + chairman_profile_id: Option, + company_id: Option, + territory_id: Option, +) -> Option { + let normalize = |raw: i32| (i64::from(raw.max(-99)) as f64) / 100.0 + 1.0; + let base_raw = *state + .service_state + .world_issue_opinion_base_terms_raw_i32 + .get(issue_id as usize)?; + let mut multiplier = normalize(base_raw); + let mut resolved_company_id = company_id; + if let Some(profile_id) = chairman_profile_id { + if let Some(profile_terms) = state + .service_state + .chairman_issue_opinion_terms_raw_i32 + .get(&profile_id) + { + if let Some(value) = profile_terms.get(issue_id as usize) { + multiplier *= normalize(*value); + } + } + if resolved_company_id.is_none() { + resolved_company_id = state + .chairman_profiles + .iter() + .find(|profile| profile.profile_id == profile_id) + .and_then(|profile| profile.linked_company_id); + } + } + if let Some(company_id) = resolved_company_id { + if let Some(company_terms) = state + .service_state + .company_market_state + .get(&company_id) + .map(|market_state| &market_state.issue_opinion_terms_raw_i32) + { + if let Some(value) = company_terms.get(issue_id as usize) { + multiplier *= normalize(*value); + } + } + } + if territory_id.is_some() { + return None; + } + Some(multiplier.max(0.01)) +} + +pub fn runtime_world_issue_state( + state: &RuntimeState, + issue_id: u32, +) -> Option { + match issue_id { + RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE => Some(RuntimeWorldIssueState { + issue_id, + raw_value_u32: state.world_restore.issue_37_value?, + multiplier_raw_u32: state.world_restore.issue_37_multiplier_raw_u32, + multiplier_value_f32_text: state + .world_restore + .issue_37_multiplier_value_f32_text + .clone(), + }), + RUNTIME_WORLD_ISSUE_CREDIT_MARKET => Some(RuntimeWorldIssueState { + issue_id, + raw_value_u32: state.world_restore.issue_38_value?, + multiplier_raw_u32: None, + multiplier_value_f32_text: None, + }), + RUNTIME_WORLD_ISSUE_PRIME_RATE => Some(RuntimeWorldIssueState { + issue_id, + raw_value_u32: state.world_restore.issue_39_value?, + multiplier_raw_u32: None, + multiplier_value_f32_text: None, + }), + RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE => Some(RuntimeWorldIssueState { + issue_id, + raw_value_u32: state.world_restore.issue_3a_value?, + multiplier_raw_u32: None, + multiplier_value_f32_text: None, + }), + _ => None, + } +} + +pub fn runtime_world_prime_rate_baseline(state: &RuntimeState) -> Option { + let raw = state.world_restore.issue_37_value?; + let value = f32::from_bits(raw) as f64; + if !value.is_finite() { + return None; + } + let scaled = (value + 0.001) * 100.0; + if !scaled.is_finite() { + return None; + } + Some(scaled.round() / 100.0) +} + +pub fn runtime_company_prime_rate(state: &RuntimeState, company_id: u32) -> Option { + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + if let Some(prime_rate) = company.prime_rate { + return Some(prime_rate); + } + let baseline = runtime_world_prime_rate_baseline(state)?; + let raw_issue_sum = runtime_world_issue_opinion_term_sum_raw( + state, + RUNTIME_WORLD_ISSUE_PRIME_RATE, + company.linked_chairman_profile_id, + Some(company_id), + None, + )?; + runtime_round_f64_to_i64(baseline + (raw_issue_sum as f64) * 0.01) +} diff --git a/crates/rrt-runtime/src/derived/world/mod.rs b/crates/rrt-runtime/src/derived/world/mod.rs new file mode 100644 index 0000000..ab3acf6 --- /dev/null +++ b/crates/rrt-runtime/src/derived/world/mod.rs @@ -0,0 +1,13 @@ +mod bond_metrics; +mod calendar; +mod issue_state; +mod selected_year; +mod share_price; +mod stat_readers; + +pub use bond_metrics::*; +pub use calendar::*; +pub use issue_state::*; +pub use selected_year::*; +pub use share_price::*; +pub use stat_readers::*; diff --git a/crates/rrt-runtime/src/derived/world/selected_year.rs b/crates/rrt-runtime/src/derived/world/selected_year.rs new file mode 100644 index 0000000..e4a8a90 --- /dev/null +++ b/crates/rrt-runtime/src/derived/world/selected_year.rs @@ -0,0 +1,120 @@ +use std::sync::OnceLock; + +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub(super) struct CheckedInSelectedYearBucketLadderArtifact { + direct_lane_multipliers: Vec, + complement_formula: CheckedInSelectedYearBucketComplementFormula, + scaled_companion_formula: CheckedInSelectedYearBucketScaledCompanionFormula, + entries: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub(super) struct CheckedInSelectedYearBucketLadderEntry { + year: u32, + value: f32, +} + +#[derive(Debug, Clone, Deserialize)] +pub(super) struct CheckedInSelectedYearBucketComplementFormula { + divisor: f32, + multiplier: f32, + bias: f32, + scale: f32, + floor: f32, + build_106_multiplier: f32, + cap: f32, +} + +#[derive(Debug, Clone, Deserialize)] +pub(super) struct CheckedInSelectedYearBucketScaledCompanionFormula { + numerator: f32, + multiplier: f32, + bias: f32, + scale: f32, +} + +#[derive(Debug, Clone)] +pub(crate) struct RuntimeSelectedYearBucketBands { + pub(crate) direct: [f32; 3], + pub(crate) complement: [f32; 3], + pub(crate) scaled_companion: [f32; 3], +} + +pub(super) fn checked_in_selected_year_bucket_ladder() +-> &'static CheckedInSelectedYearBucketLadderArtifact { + static LADDER: OnceLock = OnceLock::new(); + LADDER.get_or_init(|| { + serde_json::from_str::(include_str!( + "../../../../../artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json" + )) + .expect("checked-in selected-year bucket ladder should parse") + }) +} + +pub fn runtime_world_selected_year_bucket_scalar_from_year_word(year_word: u32) -> Option { + let ladder = &checked_in_selected_year_bucket_ladder().entries; + if ladder.is_empty() { + return None; + } + if year_word <= ladder[0].year { + return Some(ladder[0].value); + } + for window in ladder.windows(2) { + let start = &window[0]; + let end = &window[1]; + if year_word <= end.year { + if year_word <= start.year || end.year == start.year { + return Some(start.value); + } + let span = (end.year - start.year) as f32; + let progress = (year_word - start.year) as f32 / span; + return Some(start.value + (end.value - start.value) * progress); + } + } + ladder.last().map(|entry| entry.value) +} + +pub(crate) fn runtime_selected_year_bucket_bands_from_scalar( + scalar: f32, +) -> Option { + let artifact = checked_in_selected_year_bucket_ladder(); + if artifact.direct_lane_multipliers.len() != 3 { + return None; + } + let direct = [ + scalar * artifact.direct_lane_multipliers[0], + scalar * artifact.direct_lane_multipliers[1], + scalar * artifact.direct_lane_multipliers[2], + ]; + let mut complement = [0.0f32; 3]; + let mut scaled_companion = [0.0f32; 3]; + for (index, direct_value) in direct.iter().copied().enumerate() { + let mut x = (((direct_value / artifact.complement_formula.divisor) + * artifact.complement_formula.multiplier) + + artifact.complement_formula.bias) + * artifact.complement_formula.scale; + x = x.max(artifact.complement_formula.floor); + x *= artifact.complement_formula.build_106_multiplier; + x = x.min(artifact.complement_formula.cap); + complement[index] = 1.0 - x; + scaled_companion[index] = (((artifact.scaled_companion_formula.numerator / direct_value) + * artifact.scaled_companion_formula.multiplier) + + artifact.scaled_companion_formula.bias) + * artifact.scaled_companion_formula.scale; + } + Some(RuntimeSelectedYearBucketBands { + direct, + complement, + scaled_companion, + }) +} + +pub fn runtime_world_selected_year_gap_scalar_from_year_word(year_word: u32) -> Option { + let normalized = (year_word as f64 - 1850.0) / 150.0; + if !normalized.is_finite() { + return None; + } + Some(normalized.clamp(1.0 / 3.0, 1.0) as f32) +} diff --git a/crates/rrt-runtime/src/derived/world/share_price.rs b/crates/rrt-runtime/src/derived/world/share_price.rs new file mode 100644 index 0000000..2cb276b --- /dev/null +++ b/crates/rrt-runtime/src/derived/world/share_price.rs @@ -0,0 +1,94 @@ +use crate::derived::{ + derive_runtime_company_elapsed_years, runtime_decode_saved_f32_value, + runtime_decode_saved_f64_bits, runtime_world_absolute_counter, + runtime_world_issue_opinion_multiplier, +}; +use crate::event::metrics::RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE; +use crate::state::RuntimeState; + +pub(crate) fn runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( + state: &RuntimeState, + company_id: u32, + share_pressure_shares: i64, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + if let Some(cached_value) = + runtime_decode_saved_f32_value(market_state.cached_share_price_raw_u32) + { + if share_pressure_shares == 0 { + return Some(cached_value.max(0.0001)); + } + } + + if market_state.outstanding_shares == 0 { + return Some(0.0010000000474974513); + } + + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + let mut recent_per_share = runtime_company_recent_per_share_subscore(state, company_id)?; + let young_company_support = + runtime_decode_saved_f32_value(market_state.young_company_support_scalar_raw_u32)?; + if recent_per_share < young_company_support { + let years_since_founding = + derive_runtime_company_elapsed_years(state.calendar.year, market_state.founding_year) + .unwrap_or(u32::MAX); + if years_since_founding <= 5 { + let elapsed_support_ticks = runtime_world_absolute_counter(state) + .unwrap_or(0) + .saturating_sub(market_state.support_progress_word); + let interpolation = (elapsed_support_ticks / 50).min(50) as f64; + recent_per_share = ((50.0 - interpolation) * young_company_support + + (50.0 + interpolation) * recent_per_share) + / 100.0; + } + } + + let mutable_support = + runtime_decode_saved_f32_value(market_state.mutable_support_scalar_raw_u32)?; + let share_pressure = + (share_pressure_shares as f64 / market_state.outstanding_shares as f64).clamp(-0.2, 0.2); + let effective_mutable_support = mutable_support + share_pressure; + let share_count_growth_ratio = ((market_state.outstanding_shares as f64 + + 1.4 + * effective_mutable_support + * ((market_state.outstanding_shares as f64 / 20_000.0).powf(0.33))) + / market_state.outstanding_shares as f64) + .clamp(0.3, 6.0); + + let investor_multiplier = runtime_world_issue_opinion_multiplier( + state, + RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE, + company.linked_chairman_profile_id, + Some(company_id), + None, + )?; + + Some(((recent_per_share * share_count_growth_ratio * investor_multiplier) + 1.0).max(0.0001)) +} + +pub fn runtime_company_recent_per_share_subscore( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + if runtime_world_absolute_counter(state) + .is_some_and(|counter| counter == market_state.recent_per_share_cache_absolute_counter) + { + if let Some(cached_value) = + runtime_decode_saved_f64_bits(market_state.recent_per_share_cached_value_bits) + { + return Some(cached_value); + } + } + runtime_decode_saved_f32_value(market_state.recent_per_share_subscore_raw_u32) +} + +pub fn runtime_company_support_adjusted_share_price_scalar( + state: &RuntimeState, + company_id: u32, +) -> Option { + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(state, company_id, 0) +} diff --git a/crates/rrt-runtime/src/derived/world/stat_readers.rs b/crates/rrt-runtime/src/derived/world/stat_readers.rs new file mode 100644 index 0000000..bdc447b --- /dev/null +++ b/crates/rrt-runtime/src/derived/world/stat_readers.rs @@ -0,0 +1,356 @@ +use crate::derived::{ + runtime_company_credit_rating, runtime_company_support_adjusted_share_price_scalar, + runtime_decode_saved_f64_bits, runtime_round_f64_to_i64, +}; +use crate::event::metrics::{ + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A, + RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, RUNTIME_COMPANY_STAT_SLOT_COUNT, + RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN, RuntimeCompanyStatSelector, +}; +use crate::state::RuntimeState; + +pub fn runtime_company_stat_value( + state: &RuntimeState, + company_id: u32, + selector: RuntimeCompanyStatSelector, +) -> Option { + runtime_company_stat_value_f64(state, company_id, selector).and_then(runtime_round_f64_to_i64) +} + +pub fn runtime_company_stat_value_f64( + state: &RuntimeState, + company_id: u32, + selector: RuntimeCompanyStatSelector, +) -> Option { + if selector.slot_id >= RUNTIME_COMPANY_STAT_SLOT_COUNT { + return runtime_company_derived_stat_value( + state, + company_id, + selector.family_id, + selector.slot_id, + ); + } + match selector.family_id { + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER => { + runtime_company_control_transfer_stat_value_f64(state, company_id, selector.slot_id) + } + RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A => { + runtime_company_special_stat_family_232a_value_f64(state, company_id, selector.slot_id) + } + family_id => { + runtime_company_year_stat_value_f64(state, company_id, family_id, selector.slot_id) + } + } +} + +pub(super) fn runtime_company_current_stat_value_f64( + state: &RuntimeState, + company_id: u32, + slot_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + let index = slot_id.checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? as usize; + runtime_decode_saved_f64_bits(*market_state.year_stat_family_qword_bits.get(index)?) +} + +pub(crate) fn runtime_company_direct_float_field_value_f64( + state: &RuntimeState, + company_id: u32, + field_offset: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + let raw_u32 = *market_state + .direct_control_transfer_float_fields_raw_u32 + .get(&field_offset)?; + let value = f32::from_bits(raw_u32) as f64; + if !value.is_finite() { + return None; + } + Some(value) +} + +pub(super) fn runtime_company_direct_i32_field_value_f64( + state: &RuntimeState, + company_id: u32, + field_offset: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + Some(i64::from( + *market_state + .direct_control_transfer_int_fields_raw_u32 + .get(&field_offset)? as i32, + ) as f64) +} + +pub fn runtime_company_book_value_per_share(state: &RuntimeState, company_id: u32) -> Option { + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + if company.book_value_per_share != 0 { + return Some(company.book_value_per_share); + } + runtime_company_direct_float_field_value_f64(state, company_id, 0x32f) + .and_then(runtime_round_f64_to_i64) +} + +pub fn runtime_decode_saved_f32_value(raw_u32: u32) -> Option { + let value = f32::from_bits(raw_u32) as f64; + if !value.is_finite() { + return None; + } + Some(value) +} + +pub(crate) fn runtime_company_control_transfer_stat_value_f64( + state: &RuntimeState, + company_id: u32, + slot_id: u32, +) -> Option { + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + match slot_id { + 0x00..=0x12 if slot_id != RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH => { + runtime_company_current_stat_value_f64(state, company_id, slot_id) + } + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH => { + if company.current_cash != 0 { + Some(company.current_cash as f64) + } else { + runtime_company_current_stat_value_f64(state, company_id, slot_id) + } + } + 0x14 => runtime_company_control_transfer_stat_value_f64(state, company_id, 0x31) + .zip(state.service_state.company_market_state.get(&company_id)) + .map(|(value, market_state)| { + if market_state.outstanding_shares == 0 { + 0.0 + } else { + value / market_state.outstanding_shares as f64 + } + }), + 0x15 => runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: 0x2c, + }, + ) + .zip(state.service_state.company_market_state.get(&company_id)) + .map(|(value, market_state)| { + if market_state.outstanding_shares == 0 { + 0.0 + } else { + value / market_state.outstanding_shares as f64 + } + }), + 0x16 => runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: 0x2b, + }, + ) + .zip(state.service_state.company_market_state.get(&company_id)) + .map(|(value, market_state)| { + if market_state.outstanding_shares == 0 { + 0.0 + } else { + value / market_state.outstanding_shares as f64 + } + }), + 0x13 => runtime_company_support_adjusted_share_price_scalar(state, company_id), + 0x17 => runtime_company_direct_float_field_value_f64(state, company_id, 0x4b), + RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING => { + runtime_company_credit_rating(state, company_id).map(|value| value as f64) + } + 0x1a => runtime_company_direct_float_field_value_f64(state, company_id, 0x53), + 0x1b => runtime_company_direct_float_field_value_f64(state, company_id, 0x323), + 0x1c => runtime_company_direct_float_field_value_f64(state, company_id, 0x327), + RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE => { + runtime_company_book_value_per_share(state, company_id).map(|value| value as f64) + } + 0x1e => runtime_company_direct_float_field_value_f64(state, company_id, 0x333), + 0x1f => runtime_company_direct_float_field_value_f64(state, company_id, 0x33b), + 0x20 => runtime_company_direct_float_field_value_f64(state, company_id, 0x33f), + 0x21 => runtime_company_direct_float_field_value_f64(state, company_id, 0x327).and_then( + |denominator| { + let numerator = + runtime_company_direct_float_field_value_f64(state, company_id, 0x32b)?; + Some(if denominator <= 0.0 { + numerator + } else { + numerator / denominator + }) + }, + ), + 0x22 => runtime_company_direct_float_field_value_f64(state, company_id, 0x333).and_then( + |denominator| { + let numerator = + runtime_company_direct_float_field_value_f64(state, company_id, 0x337)?; + Some(if denominator <= 0.0 { + numerator + } else { + numerator / denominator + }) + }, + ), + 0x23 => runtime_company_direct_float_field_value_f64(state, company_id, 0x33f).and_then( + |denominator| { + let numerator = + runtime_company_direct_float_field_value_f64(state, company_id, 0x343)?; + Some(if denominator <= 0.0 { + numerator + } else { + numerator / denominator + }) + }, + ), + 0x26 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x34b), + 0x27 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x14f), + 0x28 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d0b), + 0x29 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d0f), + 0x2a => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d13), + _ => None, + } +} + +pub(super) fn runtime_company_special_stat_family_232a_value_f64( + state: &RuntimeState, + company_id: u32, + slot_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + let value = runtime_decode_saved_f64_bits( + *market_state + .special_stat_family_232a_qword_bits + .get(slot_id as usize)?, + )?; + if (0x13..=0x1b).contains(&slot_id) { + Some(value + runtime_company_control_transfer_stat_value_f64(state, company_id, slot_id)?) + } else { + Some(value) + } +} + +pub(super) fn runtime_company_year_stat_value_f64( + state: &RuntimeState, + company_id: u32, + family_id: u32, + slot_id: u32, +) -> Option { + let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); + let year_delta = current_year_word.checked_sub(family_id)?; + if year_delta == 0 || year_delta >= RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN { + return None; + } + let market_state = state.service_state.company_market_state.get(&company_id)?; + let index = slot_id + .checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? + .checked_add(year_delta)? as usize; + runtime_decode_saved_f64_bits(*market_state.year_stat_family_qword_bits.get(index)?) +} + +pub fn runtime_company_derived_stat_value( + state: &RuntimeState, + company_id: u32, + family_id: u32, + slot_id: u32, +) -> Option { + let stat = |slot_id| { + runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { family_id, slot_id }, + ) + }; + let rounded_stat = |slot_id| stat(slot_id).and_then(runtime_round_f64_to_i64); + match slot_id { + 0x2b => Some(stat(0x2d)? + stat(0x2c)?), + 0x2c => Some(stat(0x04)? + stat(0x03)? + stat(0x02)? + stat(0x01)?), + 0x2d => Some(stat(0x2f)? + stat(0x2e)?), + 0x2e => Some( + stat(0x0c)? + + stat(0x0b)? + + stat(0x0a)? + + stat(0x08)? + + stat(0x07)? + + stat(0x06)? + + stat(0x05)?, + ), + 0x2f => stat(0x09), + 0x30 => Some(stat(0x11)? + stat(0x10)? + stat(0x0f)? + stat(0x0e)? + stat(0x0d)?), + 0x31 => Some(stat(0x30)? + stat(0x12)?), + 0x32 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x24)?), + 0x33 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x16)?), + 0x34 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x17)?), + 0x35 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x18)?), + 0x36 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x19)?), + 0x37 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x1a)?), + 0x38 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x1b)?), + _ => None, + } +} + +pub(super) fn runtime_divide_by_rounded_stat_i64(numerator: f64, denominator: i64) -> Option { + if denominator == 0 { + return Some(0.0); + } + Some(numerator / denominator as f64) +} + +pub fn runtime_company_trailing_full_year_stat_series( + state: &RuntimeState, + company_id: u32, + slot_id: u32, + full_year_count: u32, +) -> Option<(Vec, Vec)> { + let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); + let mut year_words = Vec::with_capacity(full_year_count as usize); + let mut values = Vec::with_capacity(full_year_count as usize); + for year_offset in 1..=full_year_count { + let family_id = current_year_word.checked_sub(year_offset)?; + let value = runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { family_id, slot_id }, + ) + .and_then(runtime_round_f64_to_i64)?; + year_words.push(family_id); + values.push(value); + } + Some((year_words, values)) +} + +pub fn runtime_company_year_or_control_transfer_metric_value( + state: &RuntimeState, + company_id: u32, + year_word: u32, + slot_id: u32, +) -> Option { + let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); + if year_word == current_year_word { + runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id, + }, + ) + } else { + runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { + family_id: year_word, + slot_id, + }, + ) + } +} diff --git a/crates/rrt-runtime/src/documents/blockers.rs b/crates/rrt-runtime/src/documents/blockers.rs new file mode 100644 index 0000000..6a9a7fa --- /dev/null +++ b/crates/rrt-runtime/src/documents/blockers.rs @@ -0,0 +1,404 @@ +use crate::documents::lowering::{ + chairman_target_import_blocker, company_target_import_blocker, lowered_record_decoded_actions, + lowered_record_decoded_conditions, player_target_import_blocker, +}; +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::event::conditions::RuntimeCondition; +use crate::event::effects::RuntimeEffect; +use crate::event::targets::RuntimeChairmanTarget; +use crate::event::targets::{RuntimeCompanyTarget, RuntimePlayerTarget, RuntimeTerritoryTarget}; +use crate::inspect::smp::events::{ + SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventRecordSummary, +}; + +pub(super) fn packed_record_import_blocker( + record: &SmpLoadedPackedEventRecordSummary, + runtime_context: &RuntimeImportContext, +) -> Option { + if record.decoded_actions.iter().any(|effect| { + runtime_effect_uses_condition_true_company(effect) + || runtime_effect_uses_condition_true_player(effect) + }) && record.negative_sentinel_scope.is_none() + { + return Some(RuntimeImportBlocker::MissingConditionContext); + } + let lowered_conditions = match lowered_record_decoded_conditions(record, runtime_context) { + Ok(conditions) => conditions, + Err(blocker) => return Some(blocker), + }; + if let Some(blocker) = lowered_conditions + .iter() + .find_map(|condition| runtime_condition_import_blocker(condition, runtime_context)) + { + return Some(blocker); + } + let lowered_effects = match lowered_record_decoded_actions(record, runtime_context) { + Ok(effects) => effects, + Err(blocker) => return Some(blocker), + }; + lowered_effects + .iter() + .find_map(|effect| runtime_effect_import_blocker(effect, runtime_context)) +} + +pub(super) fn runtime_condition_import_blocker( + condition: &RuntimeCondition, + runtime_context: &RuntimeImportContext, +) -> Option { + match condition { + RuntimeCondition::WorldVariableThreshold { .. } => None, + RuntimeCondition::CompanyNumericThreshold { target, .. } => { + company_target_import_blocker(target, runtime_context) + } + RuntimeCondition::CompanyVariableThreshold { target, .. } => { + company_target_import_blocker(target, runtime_context) + } + RuntimeCondition::PlayerVariableThreshold { target, .. } => { + player_target_import_blocker(target, runtime_context) + } + RuntimeCondition::ChairmanNumericThreshold { target, .. } => { + chairman_target_import_blocker(target, runtime_context) + } + RuntimeCondition::TerritoryNumericThreshold { target, .. } => { + territory_target_import_blocker(target, runtime_context) + } + RuntimeCondition::TerritoryVariableThreshold { target, .. } => { + territory_target_import_blocker(target, runtime_context) + } + RuntimeCondition::CompanyTerritoryNumericThreshold { + target, territory, .. + } => company_target_import_blocker(target, runtime_context) + .or_else(|| territory_target_import_blocker(territory, runtime_context)), + RuntimeCondition::SpecialConditionThreshold { .. } + | RuntimeCondition::CandidateAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionSlotThreshold { .. } + | RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::FactoryProductionTotalThreshold { .. } + | RuntimeCondition::FarmMineProductionTotalThreshold { .. } + | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } + | RuntimeCondition::EconomicStatusCodeThreshold { .. } + | RuntimeCondition::WorldFlagEquals { .. } => None, + } +} + +pub(super) fn territory_target_import_blocker( + target: &RuntimeTerritoryTarget, + runtime_context: &RuntimeImportContext, +) -> Option { + if !runtime_context.has_territory_context { + return Some(RuntimeImportBlocker::MissingTerritoryContext); + } + match target { + RuntimeTerritoryTarget::AllTerritories => None, + RuntimeTerritoryTarget::Ids { ids } => { + if ids.is_empty() { + Some(RuntimeImportBlocker::NamedTerritoryBinding) + } else if !territory_ids_match_known_context(ids, runtime_context) { + Some(RuntimeImportBlocker::NamedTerritoryBinding) + } else { + None + } + } + } +} + +pub(super) fn runtime_import_blocker_outcome(blocker: RuntimeImportBlocker) -> &'static str { + match blocker { + RuntimeImportBlocker::MissingCompanyContext => "blocked_missing_company_context", + RuntimeImportBlocker::MissingSelectionContext => "blocked_missing_selection_context", + RuntimeImportBlocker::MissingCompanyRoleContext => "blocked_missing_company_role_context", + RuntimeImportBlocker::MissingPlayerContext => "blocked_missing_player_context", + RuntimeImportBlocker::MissingPlayerSelectionContext => { + "blocked_missing_player_selection_context" + } + RuntimeImportBlocker::MissingPlayerRoleContext => "blocked_missing_player_role_context", + RuntimeImportBlocker::MissingChairmanContext => "blocked_missing_chairman_context", + RuntimeImportBlocker::ChairmanTargetScope => "blocked_chairman_target_scope", + RuntimeImportBlocker::MissingConditionContext => "blocked_missing_condition_context", + RuntimeImportBlocker::MissingPlayerConditionContext => { + "blocked_missing_player_condition_context" + } + RuntimeImportBlocker::CompanyConditionScopeDisabled => { + "blocked_company_condition_scope_disabled" + } + RuntimeImportBlocker::MissingTerritoryContext => "blocked_missing_territory_context", + RuntimeImportBlocker::NamedTerritoryBinding => "blocked_named_territory_binding", + RuntimeImportBlocker::UnmappedOrdinaryCondition => "blocked_unmapped_ordinary_condition", + RuntimeImportBlocker::UnmappedWorldCondition => "blocked_unmapped_world_condition", + RuntimeImportBlocker::EvidenceBlockedDescriptor => "blocked_evidence_blocked_descriptor", + RuntimeImportBlocker::MissingTrainContext => "blocked_missing_train_context", + RuntimeImportBlocker::MissingTrainTerritoryContext => { + "blocked_missing_train_territory_context" + } + RuntimeImportBlocker::MissingLocomotiveCatalogContext => { + "blocked_missing_locomotive_catalog_context" + } + } +} + +pub(super) fn territory_ids_match_known_context( + ids: &[u32], + runtime_context: &RuntimeImportContext, +) -> bool { + ids.iter() + .all(|territory_id| runtime_context.known_territory_ids.contains(territory_id)) +} + +pub(super) fn real_grouped_row_is_unsupported_territory_access_variant( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + row.descriptor_id == 3 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0) +} + +pub(super) fn real_grouped_row_is_unsupported_chairman_target_scope( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + matches!(row.grouped_target_subject.as_deref(), Some("chairman")) + && matches!(row.descriptor_id, 1 | 14) + && row.notes.iter().any(|note| { + note.starts_with("chairman row uses unsupported grouped target scope ordinal ") + }) +} + +pub(super) fn real_grouped_row_is_unsupported_territory_access_scope( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + row.descriptor_id == 3 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + && row + .notes + .iter() + .any(|note| note == "territory access row is missing company or territory scope") +} + +pub(super) fn real_grouped_row_is_unsupported_confiscation_variant( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + row.descriptor_id == 9 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0) +} + +pub(super) fn real_grouped_row_is_unsupported_retire_train_variant( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + row.descriptor_id == 15 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0) +} + +pub(super) fn real_grouped_row_is_unsupported_retire_train_scope( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + row.descriptor_id == 15 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + && row + .notes + .iter() + .any(|note| note == "retire train row is missing company and territory scope") +} + +pub(super) fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { + match effect { + RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyVariable { target, .. } + | RuntimeEffect::SetCompanyGovernanceScalar { target, .. } + | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } + | RuntimeEffect::ConfiscateCompanyAssets { target } + | RuntimeEffect::DeactivateCompany { target } + | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } + | RuntimeEffect::AdjustCompanyCash { target, .. } + | RuntimeEffect::AdjustCompanyDebt { target, .. } => { + matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) + } + RuntimeEffect::RetireTrains { company_target, .. } => matches!( + company_target, + Some(RuntimeCompanyTarget::ConditionTrueCompany) + ), + RuntimeEffect::AppendEventRecord { record } => record + .effects + .iter() + .any(runtime_effect_uses_condition_true_company), + RuntimeEffect::SetWorldFlag { .. } + | RuntimeEffect::SetWorldVariable { .. } + | RuntimeEffect::SetWorldScalarOverride { .. } + | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } + | RuntimeEffect::SetEconomicStatusCode { .. } + | RuntimeEffect::SetPlayerCash { .. } + | RuntimeEffect::SetPlayerVariable { .. } + | RuntimeEffect::SetChairmanCash { .. } + | RuntimeEffect::DeactivatePlayer { .. } + | RuntimeEffect::DeactivateChairman { .. } + | RuntimeEffect::SetCandidateAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. } + | RuntimeEffect::SetNamedLocomotiveCost { .. } + | RuntimeEffect::SetCargoPriceOverride { .. } + | RuntimeEffect::SetCargoProductionOverride { .. } + | RuntimeEffect::SetCargoProductionSlot { .. } + | RuntimeEffect::SetTerritoryVariable { .. } + | RuntimeEffect::SetTerritoryAccessCost { .. } + | RuntimeEffect::SetSpecialCondition { .. } + | RuntimeEffect::ActivateEventRecord { .. } + | RuntimeEffect::DeactivateEventRecord { .. } + | RuntimeEffect::RemoveEventRecord { .. } => false, + } +} + +pub(super) fn runtime_effect_uses_condition_true_player(effect: &RuntimeEffect) -> bool { + match effect { + RuntimeEffect::SetPlayerCash { target, .. } + | RuntimeEffect::SetPlayerVariable { target, .. } => { + matches!(target, RuntimePlayerTarget::ConditionTruePlayer) + } + RuntimeEffect::DeactivatePlayer { target } => { + matches!(target, RuntimePlayerTarget::ConditionTruePlayer) + } + RuntimeEffect::SetChairmanCash { .. } | RuntimeEffect::DeactivateChairman { .. } => false, + RuntimeEffect::AppendEventRecord { record } => record + .effects + .iter() + .any(runtime_effect_uses_condition_true_player), + _ => false, + } +} + +pub(super) fn record_uses_condition_true_chairman( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .decoded_actions + .iter() + .any(runtime_effect_uses_condition_true_chairman) +} + +pub(super) fn runtime_effect_uses_condition_true_chairman(effect: &RuntimeEffect) -> bool { + match effect { + RuntimeEffect::SetChairmanCash { target, .. } + | RuntimeEffect::DeactivateChairman { target } => { + matches!(target, RuntimeChairmanTarget::ConditionTrueChairman) + } + RuntimeEffect::AppendEventRecord { record } => record + .effects + .iter() + .any(runtime_effect_uses_condition_true_chairman), + _ => false, + } +} + +pub(super) fn runtime_effect_import_blocker( + effect: &RuntimeEffect, + runtime_context: &RuntimeImportContext, +) -> Option { + match effect { + RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyVariable { target, .. } + | RuntimeEffect::SetCompanyGovernanceScalar { target, .. } + | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } + | RuntimeEffect::ConfiscateCompanyAssets { target } + | RuntimeEffect::DeactivateCompany { target } + | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } + | RuntimeEffect::AdjustCompanyCash { target, .. } + | RuntimeEffect::AdjustCompanyDebt { target, .. } => { + if matches!(effect, RuntimeEffect::ConfiscateCompanyAssets { .. }) + && !runtime_context.has_train_context + { + Some(RuntimeImportBlocker::MissingTrainContext) + } else if let RuntimeEffect::SetCompanyTerritoryAccess { territory, .. } = effect { + company_target_import_blocker(target, runtime_context) + .or_else(|| territory_target_import_blocker(territory, runtime_context)) + } else { + company_target_import_blocker(target, runtime_context) + } + } + RuntimeEffect::SetPlayerCash { target, .. } + | RuntimeEffect::SetPlayerVariable { target, .. } + | RuntimeEffect::DeactivatePlayer { target } => { + player_target_import_blocker(target, runtime_context) + } + RuntimeEffect::SetTerritoryVariable { target, .. } => { + territory_target_import_blocker(target, runtime_context) + } + RuntimeEffect::SetChairmanCash { target, .. } + | RuntimeEffect::DeactivateChairman { target } => { + chairman_target_import_blocker(target, runtime_context) + } + RuntimeEffect::RetireTrains { + company_target, + territory_target, + .. + } => { + if !runtime_context.has_train_context { + return Some(RuntimeImportBlocker::MissingTrainContext); + } + if territory_target.is_some() && !runtime_context.has_train_territory_context { + return Some(RuntimeImportBlocker::MissingTrainTerritoryContext); + } + company_target + .as_ref() + .and_then(|target| company_target_import_blocker(target, runtime_context)) + .or_else(|| { + territory_target + .as_ref() + .and_then(|target| territory_target_import_blocker(target, runtime_context)) + }) + } + RuntimeEffect::AppendEventRecord { record } => record + .effects + .iter() + .find_map(|nested| runtime_effect_import_blocker(nested, runtime_context)), + RuntimeEffect::SetWorldFlag { .. } + | RuntimeEffect::SetWorldVariable { .. } + | RuntimeEffect::SetWorldScalarOverride { .. } + | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } + | RuntimeEffect::SetEconomicStatusCode { .. } + | RuntimeEffect::SetCandidateAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. } + | RuntimeEffect::SetNamedLocomotiveCost { .. } + | RuntimeEffect::SetCargoPriceOverride { .. } + | RuntimeEffect::SetCargoProductionOverride { .. } + | RuntimeEffect::SetCargoProductionSlot { .. } + | RuntimeEffect::SetTerritoryAccessCost { .. } + | RuntimeEffect::SetSpecialCondition { .. } + | RuntimeEffect::ActivateEventRecord { .. } + | RuntimeEffect::DeactivateEventRecord { .. } + | RuntimeEffect::RemoveEventRecord { .. } => None, + } +} + +pub(super) fn classify_real_grouped_company_targets( + record: &SmpLoadedPackedEventRecordSummary, +) -> Vec> { + let Some(control) = &record.compact_control else { + return Vec::new(); + }; + + control + .grouped_target_scope_ordinals_0x7fb + .iter() + .enumerate() + .map(|(group_index, ordinal)| { + if !record + .grouped_effect_rows + .iter() + .any(|row| row.group_index == group_index) + { + return None; + } + classify_real_grouped_company_target(*ordinal) + }) + .collect() +} + +pub(super) fn classify_real_grouped_company_target(ordinal: u8) -> Option { + match ordinal { + 0 => Some(RuntimeCompanyTarget::ConditionTrueCompany), + 1 => Some(RuntimeCompanyTarget::SelectedCompany), + 2 => Some(RuntimeCompanyTarget::HumanCompanies), + 3 => Some(RuntimeCompanyTarget::AiCompanies), + _ => None, + } +} diff --git a/crates/rrt-runtime/src/documents/io.rs b/crates/rrt-runtime/src/documents/io.rs new file mode 100644 index 0000000..aef6cad --- /dev/null +++ b/crates/rrt-runtime/src/documents/io.rs @@ -0,0 +1,179 @@ +use std::path::{Path, PathBuf}; + +use crate::documents::{ + RuntimeOverlayImportDocument, RuntimeSaveSliceDocument, RuntimeStateInput, + RuntimeStateInputDocument, build_runtime_state_input_from_overlay, + build_runtime_state_input_from_save_slice, +}; +use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; +use crate::state::RuntimeState; +use crate::validation::{ + validate_runtime_overlay_import_document, validate_runtime_save_slice_document, + validate_runtime_state_input_document, +}; + +pub fn load_runtime_save_slice_document( + path: &Path, +) -> Result> { + let text = std::fs::read_to_string(path)?; + let document: RuntimeSaveSliceDocument = serde_json::from_str(&text)?; + Ok(document) +} + +pub fn load_runtime_overlay_import_document( + path: &Path, +) -> Result> { + let text = std::fs::read_to_string(path)?; + let document: RuntimeOverlayImportDocument = serde_json::from_str(&text)?; + Ok(document) +} + +pub fn save_runtime_save_slice_document( + path: &Path, + document: &RuntimeSaveSliceDocument, +) -> Result<(), Box> { + validate_runtime_save_slice_document(document) + .map_err(|err| format!("invalid runtime save slice document: {err}"))?; + let bytes = serde_json::to_vec_pretty(document)?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, bytes)?; + Ok(()) +} + +pub fn save_runtime_overlay_import_document( + path: &Path, + document: &RuntimeOverlayImportDocument, +) -> Result<(), Box> { + validate_runtime_overlay_import_document(document) + .map_err(|err| format!("invalid runtime overlay import document: {err}"))?; + let bytes = serde_json::to_vec_pretty(document)?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, bytes)?; + Ok(()) +} + +pub fn load_runtime_state_input( + path: &Path, +) -> Result> { + let text = std::fs::read_to_string(path)?; + load_runtime_state_input_from_str_with_base( + &text, + path.file_stem() + .and_then(|stem| stem.to_str()) + .unwrap_or("runtime-state"), + path.parent().unwrap_or_else(|| Path::new(".")), + ) +} + +pub fn load_runtime_state_input_from_str( + text: &str, + fallback_id: &str, +) -> Result> { + load_runtime_state_input_from_str_with_base(text, fallback_id, Path::new(".")) +} + +pub(super) fn load_runtime_state_input_from_str_with_base( + text: &str, + fallback_id: &str, + base_dir: &Path, +) -> Result> { + if let Ok(document) = serde_json::from_str::(text) { + validate_runtime_state_input_document(&document) + .map_err(|err| format!("invalid runtime state input document: {err}"))?; + return Ok(RuntimeStateInput { + input_id: document.input_id, + description: document.source.description, + state: document.state, + }); + } + + if let Ok(document) = serde_json::from_str::(text) { + validate_runtime_save_slice_document(&document) + .map_err(|err| format!("invalid runtime save slice document: {err}"))?; + let mut description_parts = Vec::new(); + if let Some(description) = document.source.description { + description_parts.push(description); + } + if let Some(filename) = document.source.original_save_filename { + description_parts.push(format!("source save {filename}")); + } + let input = build_runtime_state_input_from_save_slice( + &document.save_slice, + &document.save_slice_id, + if description_parts.is_empty() { + None + } else { + Some(description_parts.join(" | ")) + }, + )?; + return Ok(input); + } + + if let Ok(document) = serde_json::from_str::(text) { + validate_runtime_overlay_import_document(&document) + .map_err(|err| format!("invalid runtime overlay import document: {err}"))?; + let base_snapshot_path = resolve_document_path(base_dir, &document.base_snapshot_path); + let save_slice_path = resolve_document_path(base_dir, &document.save_slice_path); + + let snapshot = load_runtime_snapshot_document(&base_snapshot_path)?; + validate_runtime_snapshot_document(&snapshot).map_err(|err| { + format!( + "invalid runtime snapshot {}: {err}", + base_snapshot_path.display() + ) + })?; + let save_slice_document = load_runtime_save_slice_document(&save_slice_path)?; + validate_runtime_save_slice_document(&save_slice_document).map_err(|err| { + format!( + "invalid runtime save slice document {}: {err}", + save_slice_path.display() + ) + })?; + + let mut description_parts = Vec::new(); + if let Some(description) = document.source.description { + description_parts.push(description); + } + if let Some(description) = snapshot.source.description { + description_parts.push(format!("base snapshot {description}")); + } + if let Some(description) = save_slice_document.source.description { + description_parts.push(format!("save slice {description}")); + } + + return build_runtime_state_input_from_overlay( + &snapshot.state, + &save_slice_document.save_slice, + &document.import_id, + if description_parts.is_empty() { + None + } else { + Some(description_parts.join(" | ")) + }, + ) + .map_err(Into::into); + } + + let state: RuntimeState = serde_json::from_str(text)?; + state + .validate() + .map_err(|err| format!("invalid runtime state: {err}"))?; + Ok(RuntimeStateInput { + input_id: fallback_id.to_string(), + description: None, + state, + }) +} + +pub(super) fn resolve_document_path(base_dir: &Path, path: &str) -> PathBuf { + let candidate = PathBuf::from(path); + if candidate.is_absolute() { + candidate + } else { + base_dir.join(candidate) + } +} diff --git a/crates/rrt-runtime/src/documents/lowering/condition_targets/blockers.rs b/crates/rrt-runtime/src/documents/lowering/condition_targets/blockers.rs new file mode 100644 index 0000000..e74ee89 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/condition_targets/blockers.rs @@ -0,0 +1,118 @@ +use super::super::import_outcome::record_has_world_state_condition_rows; +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::event::conditions::RuntimeCondition; +use crate::event::targets::RuntimeChairmanTarget; +use crate::inspect::smp::events::SmpLoadedPackedEventRecordSummary; + +pub(crate) fn packed_record_condition_scope_import_blocker( + record: &SmpLoadedPackedEventRecordSummary, + runtime_context: &RuntimeImportContext, +) -> Option { + if record.standalone_condition_rows.is_empty() { + return None; + } + + let ordinary_condition_row_count = record + .standalone_condition_rows + .iter() + .filter(|row| row.raw_condition_id >= 0) + .count(); + if ordinary_condition_row_count != 0 { + if ordinary_condition_row_count != record.decoded_conditions.len() { + return Some(if record_has_world_state_condition_rows(record) { + RuntimeImportBlocker::UnmappedWorldCondition + } else { + RuntimeImportBlocker::UnmappedOrdinaryCondition + }); + } + if (!runtime_context.has_territory_context) + && (record + .standalone_condition_rows + .iter() + .any(|row| row.requires_candidate_name_binding) + || record.decoded_conditions.iter().any(|condition| { + matches!( + condition, + RuntimeCondition::TerritoryNumericThreshold { .. } + | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } + ) + })) + { + return Some(RuntimeImportBlocker::MissingTerritoryContext); + } + } + + let negative_sentinel_row_count = record + .standalone_condition_rows + .iter() + .filter(|row| row.raw_condition_id == -1) + .count(); + if negative_sentinel_row_count == 0 { + return if ordinary_condition_row_count == 0 { + Some(RuntimeImportBlocker::MissingConditionContext) + } else { + None + }; + } + if ordinary_condition_row_count == 0 + && negative_sentinel_row_count != record.standalone_condition_rows.len() + { + return Some(RuntimeImportBlocker::MissingConditionContext); + } + + if record.negative_sentinel_scope.is_none() { + return Some(RuntimeImportBlocker::MissingConditionContext); + } + + None +} + +pub(crate) fn chairman_target_import_blocker( + target: &RuntimeChairmanTarget, + runtime_context: &RuntimeImportContext, +) -> Option { + match target { + RuntimeChairmanTarget::AllActive => { + if runtime_context.known_chairman_profile_ids.is_empty() { + Some(RuntimeImportBlocker::MissingChairmanContext) + } else { + None + } + } + RuntimeChairmanTarget::HumanChairmen | RuntimeChairmanTarget::AiChairmen => { + if runtime_context.known_chairman_profile_ids.is_empty() { + Some(RuntimeImportBlocker::MissingChairmanContext) + } else if !runtime_context.has_complete_company_controller_context { + Some(RuntimeImportBlocker::MissingCompanyRoleContext) + } else { + None + } + } + RuntimeChairmanTarget::SelectedChairman => { + if runtime_context.selected_chairman_profile_id.is_some() { + None + } else { + Some(RuntimeImportBlocker::MissingChairmanContext) + } + } + RuntimeChairmanTarget::ConditionTrueChairman => { + if runtime_context.known_chairman_profile_ids.is_empty() { + Some(RuntimeImportBlocker::MissingChairmanContext) + } else { + None + } + } + RuntimeChairmanTarget::Ids { ids } => { + if runtime_context.known_chairman_profile_ids.is_empty() { + Some(RuntimeImportBlocker::MissingChairmanContext) + } else if ids + .iter() + .all(|id| runtime_context.known_chairman_profile_ids.contains(id)) + { + None + } else { + Some(RuntimeImportBlocker::MissingChairmanContext) + } + } + } +} diff --git a/crates/rrt-runtime/src/documents/lowering/condition_targets/conditions.rs b/crates/rrt-runtime/src/documents/lowering/condition_targets/conditions.rs new file mode 100644 index 0000000..7ce26ed --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/condition_targets/conditions.rs @@ -0,0 +1,213 @@ +use super::context_targets::{ + lower_condition_true_company_target_in_company_target, + lower_condition_true_player_target_in_player_target, +}; +use super::territory::lower_territory_target_in_condition; +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::event::conditions::RuntimeCondition; +use crate::event::targets::{RuntimeCompanyTarget, RuntimePlayerTarget}; +use crate::inspect::smp::events::SmpLoadedPackedEventConditionRowSummary; + +pub(crate) fn lower_condition_targets_in_condition( + condition: &RuntimeCondition, + row: &SmpLoadedPackedEventConditionRowSummary, + lowered_company_target: Option<&RuntimeCompanyTarget>, + lowered_player_target: Option<&RuntimePlayerTarget>, + runtime_context: &RuntimeImportContext, +) -> Result { + Ok(match condition { + RuntimeCondition::WorldVariableThreshold { + index, + comparator, + value, + } => RuntimeCondition::WorldVariableThreshold { + index: *index, + comparator: *comparator, + value: *value, + }, + RuntimeCondition::CompanyNumericThreshold { + target, + metric, + comparator, + value, + } => RuntimeCondition::CompanyNumericThreshold { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + metric: *metric, + comparator: *comparator, + value: *value, + }, + RuntimeCondition::CompanyVariableThreshold { + target, + index, + comparator, + value, + } => RuntimeCondition::CompanyVariableThreshold { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + index: *index, + comparator: *comparator, + value: *value, + }, + RuntimeCondition::ChairmanNumericThreshold { + target, + metric, + comparator, + value, + } => RuntimeCondition::ChairmanNumericThreshold { + target: target.clone(), + metric: *metric, + comparator: *comparator, + value: *value, + }, + RuntimeCondition::PlayerVariableThreshold { + target, + index, + comparator, + value, + } => RuntimeCondition::PlayerVariableThreshold { + target: lower_condition_true_player_target_in_player_target( + target, + lowered_player_target, + )?, + index: *index, + comparator: *comparator, + value: *value, + }, + RuntimeCondition::TerritoryNumericThreshold { + target, + metric, + comparator, + value, + } => RuntimeCondition::TerritoryNumericThreshold { + target: lower_territory_target_in_condition(target, row, runtime_context)?, + metric: *metric, + comparator: *comparator, + value: *value, + }, + RuntimeCondition::TerritoryVariableThreshold { + target, + index, + comparator, + value, + } => RuntimeCondition::TerritoryVariableThreshold { + target: lower_territory_target_in_condition(target, row, runtime_context)?, + index: *index, + comparator: *comparator, + value: *value, + }, + RuntimeCondition::CompanyTerritoryNumericThreshold { + target, + territory, + metric, + comparator, + value, + } => RuntimeCondition::CompanyTerritoryNumericThreshold { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + territory: lower_territory_target_in_condition(territory, row, runtime_context)?, + metric: *metric, + comparator: *comparator, + value: *value, + }, + RuntimeCondition::SpecialConditionThreshold { + label, + comparator, + value, + } => RuntimeCondition::SpecialConditionThreshold { + label: label.clone(), + comparator: *comparator, + value: *value, + }, + RuntimeCondition::CandidateAvailabilityThreshold { + name, + comparator, + value, + } => RuntimeCondition::CandidateAvailabilityThreshold { + name: name.clone(), + comparator: *comparator, + value: *value, + }, + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name, + comparator, + value, + } => RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: name.clone(), + comparator: *comparator, + value: *value, + }, + RuntimeCondition::NamedLocomotiveCostThreshold { + name, + comparator, + value, + } => RuntimeCondition::NamedLocomotiveCostThreshold { + name: name.clone(), + comparator: *comparator, + value: *value, + }, + RuntimeCondition::CargoProductionSlotThreshold { + slot, + label, + comparator, + value, + } => RuntimeCondition::CargoProductionSlotThreshold { + slot: *slot, + label: label.clone(), + comparator: *comparator, + value: *value, + }, + RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => { + RuntimeCondition::CargoProductionTotalThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::FactoryProductionTotalThreshold { comparator, value } => { + RuntimeCondition::FactoryProductionTotalThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value } => { + RuntimeCondition::FarmMineProductionTotalThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value } => { + RuntimeCondition::OtherCargoProductionTotalThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => { + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => { + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => { + RuntimeCondition::EconomicStatusCodeThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::WorldFlagEquals { key, value } => RuntimeCondition::WorldFlagEquals { + key: key.clone(), + value: *value, + }, + }) +} diff --git a/crates/rrt-runtime/src/documents/lowering/condition_targets/context_targets.rs b/crates/rrt-runtime/src/documents/lowering/condition_targets/context_targets.rs new file mode 100644 index 0000000..a2ff9db --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/condition_targets/context_targets.rs @@ -0,0 +1,100 @@ +use super::super::super::blockers::record_uses_condition_true_chairman; +use super::predicates::{record_uses_condition_true_company, record_uses_condition_true_player}; +use crate::documents::RuntimeImportBlocker; +use crate::event::conditions::RuntimeCondition; +use crate::event::metrics::{RuntimeCompanyConditionTestScope, RuntimePlayerConditionTestScope}; +use crate::event::targets::{RuntimeCompanyTarget, RuntimePlayerTarget}; +use crate::inspect::smp::events::SmpLoadedPackedEventRecordSummary; + +pub(crate) fn lowered_condition_true_company_target( + record: &SmpLoadedPackedEventRecordSummary, +) -> Result, RuntimeImportBlocker> { + if !record_uses_condition_true_company(record) { + return Ok(None); + } + let scope = record + .negative_sentinel_scope + .as_ref() + .ok_or(RuntimeImportBlocker::MissingConditionContext)?; + match scope.company_test_scope { + RuntimeCompanyConditionTestScope::Disabled => { + Err(RuntimeImportBlocker::CompanyConditionScopeDisabled) + } + RuntimeCompanyConditionTestScope::AllCompanies => Ok(Some(RuntimeCompanyTarget::AllActive)), + RuntimeCompanyConditionTestScope::SelectedCompanyOnly => { + Ok(Some(RuntimeCompanyTarget::SelectedCompany)) + } + RuntimeCompanyConditionTestScope::AiCompaniesOnly => { + Ok(Some(RuntimeCompanyTarget::AiCompanies)) + } + RuntimeCompanyConditionTestScope::HumanCompaniesOnly => { + Ok(Some(RuntimeCompanyTarget::HumanCompanies)) + } + } +} + +pub(crate) fn lowered_condition_true_player_target( + record: &SmpLoadedPackedEventRecordSummary, +) -> Result, RuntimeImportBlocker> { + if !record_uses_condition_true_player(record) { + return Ok(None); + } + let scope = record + .negative_sentinel_scope + .as_ref() + .ok_or(RuntimeImportBlocker::MissingPlayerConditionContext)?; + match scope.player_test_scope { + RuntimePlayerConditionTestScope::Disabled => { + Err(RuntimeImportBlocker::MissingPlayerConditionContext) + } + RuntimePlayerConditionTestScope::AllPlayers => Ok(Some(RuntimePlayerTarget::AllActive)), + RuntimePlayerConditionTestScope::SelectedPlayerOnly => { + Ok(Some(RuntimePlayerTarget::SelectedPlayer)) + } + RuntimePlayerConditionTestScope::AiPlayersOnly => Ok(Some(RuntimePlayerTarget::AiPlayers)), + RuntimePlayerConditionTestScope::HumanPlayersOnly => { + Ok(Some(RuntimePlayerTarget::HumanPlayers)) + } + } +} + +pub(crate) fn lower_condition_true_company_target_in_company_target( + target: &RuntimeCompanyTarget, + lowered_target: Option<&RuntimeCompanyTarget>, +) -> Result { + match target { + RuntimeCompanyTarget::ConditionTrueCompany => lowered_target + .cloned() + .ok_or(RuntimeImportBlocker::MissingConditionContext), + _ => Ok(target.clone()), + } +} + +pub(crate) fn lower_condition_true_player_target_in_player_target( + target: &RuntimePlayerTarget, + lowered_target: Option<&RuntimePlayerTarget>, +) -> Result { + match target { + RuntimePlayerTarget::ConditionTruePlayer => lowered_target + .cloned() + .ok_or(RuntimeImportBlocker::MissingPlayerConditionContext), + _ => Ok(target.clone()), + } +} + +pub(crate) fn ensure_condition_true_chairman_context( + record: &SmpLoadedPackedEventRecordSummary, +) -> Result<(), RuntimeImportBlocker> { + if !record_uses_condition_true_chairman(record) { + return Ok(()); + } + if record + .decoded_conditions + .iter() + .any(|condition| matches!(condition, RuntimeCondition::ChairmanNumericThreshold { .. })) + { + Ok(()) + } else { + Err(RuntimeImportBlocker::MissingConditionContext) + } +} diff --git a/crates/rrt-runtime/src/documents/lowering/condition_targets/effects.rs b/crates/rrt-runtime/src/documents/lowering/condition_targets/effects.rs new file mode 100644 index 0000000..095c9df --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/condition_targets/effects.rs @@ -0,0 +1,256 @@ +use super::context_targets::{ + lower_condition_true_company_target_in_company_target, + lower_condition_true_player_target_in_player_target, +}; +use crate::documents::RuntimeImportBlocker; +use crate::event::effects::RuntimeEffect; +use crate::event::records::RuntimeEventRecordTemplate; +use crate::event::targets::{RuntimeCompanyTarget, RuntimePlayerTarget}; + +pub(crate) fn lower_condition_targets_in_effect( + effect: &RuntimeEffect, + lowered_company_target: Option<&RuntimeCompanyTarget>, + lowered_player_target: Option<&RuntimePlayerTarget>, +) -> Result { + Ok(match effect { + RuntimeEffect::SetWorldFlag { key, value } => RuntimeEffect::SetWorldFlag { + key: key.clone(), + value: *value, + }, + RuntimeEffect::SetWorldScalarOverride { key, value } => { + RuntimeEffect::SetWorldScalarOverride { + key: key.clone(), + value: *value, + } + } + RuntimeEffect::SetWorldVariable { index, value } => RuntimeEffect::SetWorldVariable { + index: *index, + value: *value, + }, + RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { + RuntimeEffect::SetLimitedTrackBuildingAmount { value: *value } + } + RuntimeEffect::SetEconomicStatusCode { value } => { + RuntimeEffect::SetEconomicStatusCode { value: *value } + } + RuntimeEffect::SetCompanyVariable { + target, + index, + value, + } => RuntimeEffect::SetCompanyVariable { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + index: *index, + value: *value, + }, + RuntimeEffect::SetCompanyCash { target, value } => RuntimeEffect::SetCompanyCash { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + value: *value, + }, + RuntimeEffect::SetPlayerVariable { + target, + index, + value, + } => RuntimeEffect::SetPlayerVariable { + target: lower_condition_true_player_target_in_player_target( + target, + lowered_player_target, + )?, + index: *index, + value: *value, + }, + RuntimeEffect::SetPlayerCash { target, value } => RuntimeEffect::SetPlayerCash { + target: lower_condition_true_player_target_in_player_target( + target, + lowered_player_target, + )?, + value: *value, + }, + RuntimeEffect::SetCompanyGovernanceScalar { + target, + metric, + value, + } => RuntimeEffect::SetCompanyGovernanceScalar { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + metric: *metric, + value: *value, + }, + RuntimeEffect::SetChairmanCash { target, value } => RuntimeEffect::SetChairmanCash { + target: target.clone(), + value: *value, + }, + RuntimeEffect::DeactivatePlayer { target } => RuntimeEffect::DeactivatePlayer { + target: lower_condition_true_player_target_in_player_target( + target, + lowered_player_target, + )?, + }, + RuntimeEffect::DeactivateChairman { target } => RuntimeEffect::DeactivateChairman { + target: target.clone(), + }, + RuntimeEffect::SetCompanyTerritoryAccess { + target, + territory, + value, + } => RuntimeEffect::SetCompanyTerritoryAccess { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + territory: territory.clone(), + value: *value, + }, + RuntimeEffect::ConfiscateCompanyAssets { target } => { + RuntimeEffect::ConfiscateCompanyAssets { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + } + } + RuntimeEffect::DeactivateCompany { target } => RuntimeEffect::DeactivateCompany { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + }, + RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { + RuntimeEffect::SetCompanyTrackLayingCapacity { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + value: *value, + } + } + RuntimeEffect::RetireTrains { + company_target, + territory_target, + locomotive_name, + } => RuntimeEffect::RetireTrains { + company_target: company_target + .as_ref() + .map(|target| { + lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + ) + }) + .transpose()?, + territory_target: territory_target.clone(), + locomotive_name: locomotive_name.clone(), + }, + RuntimeEffect::AdjustCompanyCash { target, delta } => RuntimeEffect::AdjustCompanyCash { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + delta: *delta, + }, + RuntimeEffect::AdjustCompanyDebt { target, delta } => RuntimeEffect::AdjustCompanyDebt { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + delta: *delta, + }, + RuntimeEffect::SetCandidateAvailability { name, value } => { + RuntimeEffect::SetCandidateAvailability { + name: name.clone(), + value: *value, + } + } + RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => { + RuntimeEffect::SetNamedLocomotiveAvailability { + name: name.clone(), + value: *value, + } + } + RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => { + RuntimeEffect::SetNamedLocomotiveAvailabilityValue { + name: name.clone(), + value: *value, + } + } + RuntimeEffect::SetNamedLocomotiveCost { name, value } => { + RuntimeEffect::SetNamedLocomotiveCost { + name: name.clone(), + value: *value, + } + } + RuntimeEffect::SetCargoPriceOverride { target, value } => { + RuntimeEffect::SetCargoPriceOverride { + target: target.clone(), + value: *value, + } + } + RuntimeEffect::SetTerritoryVariable { + target, + index, + value, + } => RuntimeEffect::SetTerritoryVariable { + target: target.clone(), + index: *index, + value: *value, + }, + RuntimeEffect::SetCargoProductionOverride { target, value } => { + RuntimeEffect::SetCargoProductionOverride { + target: target.clone(), + value: *value, + } + } + RuntimeEffect::SetCargoProductionSlot { slot, value } => { + RuntimeEffect::SetCargoProductionSlot { + slot: *slot, + value: *value, + } + } + RuntimeEffect::SetTerritoryAccessCost { value } => { + RuntimeEffect::SetTerritoryAccessCost { value: *value } + } + RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition { + label: label.clone(), + value: *value, + }, + RuntimeEffect::AppendEventRecord { record } => RuntimeEffect::AppendEventRecord { + record: Box::new(RuntimeEventRecordTemplate { + record_id: record.record_id, + trigger_kind: record.trigger_kind, + active: record.active, + marks_collection_dirty: record.marks_collection_dirty, + one_shot: record.one_shot, + conditions: record.conditions.clone(), + effects: record + .effects + .iter() + .map(|nested| { + lower_condition_targets_in_effect( + nested, + lowered_company_target, + lowered_player_target, + ) + }) + .collect::, _>>()?, + }), + }, + RuntimeEffect::ActivateEventRecord { record_id } => RuntimeEffect::ActivateEventRecord { + record_id: *record_id, + }, + RuntimeEffect::DeactivateEventRecord { record_id } => { + RuntimeEffect::DeactivateEventRecord { + record_id: *record_id, + } + } + RuntimeEffect::RemoveEventRecord { record_id } => RuntimeEffect::RemoveEventRecord { + record_id: *record_id, + }, + }) +} diff --git a/crates/rrt-runtime/src/documents/lowering/condition_targets/mod.rs b/crates/rrt-runtime/src/documents/lowering/condition_targets/mod.rs new file mode 100644 index 0000000..b4f23ca --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/condition_targets/mod.rs @@ -0,0 +1,16 @@ +mod blockers; +mod conditions; +mod context_targets; +mod effects; +mod predicates; +mod territory; + +pub(crate) use blockers::{ + chairman_target_import_blocker, packed_record_condition_scope_import_blocker, +}; +pub(crate) use conditions::lower_condition_targets_in_condition; +pub(crate) use context_targets::{ + ensure_condition_true_chairman_context, lowered_condition_true_company_target, + lowered_condition_true_player_target, +}; +pub(crate) use effects::lower_condition_targets_in_effect; diff --git a/crates/rrt-runtime/src/documents/lowering/condition_targets/predicates.rs b/crates/rrt-runtime/src/documents/lowering/condition_targets/predicates.rs new file mode 100644 index 0000000..3970136 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/condition_targets/predicates.rs @@ -0,0 +1,58 @@ +use super::super::super::blockers::{ + runtime_effect_uses_condition_true_company, runtime_effect_uses_condition_true_player, +}; +use crate::event::conditions::RuntimeCondition; +use crate::event::targets::{RuntimeCompanyTarget, RuntimePlayerTarget}; +use crate::inspect::smp::events::SmpLoadedPackedEventRecordSummary; + +pub(crate) fn record_uses_condition_true_company( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .decoded_conditions + .iter() + .any(condition_uses_condition_true_company) + || record + .decoded_actions + .iter() + .any(runtime_effect_uses_condition_true_company) +} + +pub(crate) fn record_uses_condition_true_player( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .decoded_actions + .iter() + .any(runtime_effect_uses_condition_true_player) +} + +pub(crate) fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool { + match condition { + RuntimeCondition::CompanyNumericThreshold { target, .. } + | RuntimeCondition::CompanyVariableThreshold { target, .. } + | RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => { + matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) + } + RuntimeCondition::PlayerVariableThreshold { target, .. } => { + matches!(target, RuntimePlayerTarget::ConditionTruePlayer) + } + RuntimeCondition::ChairmanNumericThreshold { .. } => false, + RuntimeCondition::TerritoryNumericThreshold { .. } + | RuntimeCondition::TerritoryVariableThreshold { .. } + | RuntimeCondition::WorldVariableThreshold { .. } + | RuntimeCondition::SpecialConditionThreshold { .. } + | RuntimeCondition::CandidateAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionSlotThreshold { .. } + | RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::FactoryProductionTotalThreshold { .. } + | RuntimeCondition::FarmMineProductionTotalThreshold { .. } + | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } + | RuntimeCondition::EconomicStatusCodeThreshold { .. } + | RuntimeCondition::WorldFlagEquals { .. } => false, + } +} diff --git a/crates/rrt-runtime/src/documents/lowering/condition_targets/territory.rs b/crates/rrt-runtime/src/documents/lowering/condition_targets/territory.rs new file mode 100644 index 0000000..e9b907e --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/condition_targets/territory.rs @@ -0,0 +1,28 @@ +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::event::targets::RuntimeTerritoryTarget; +use crate::inspect::smp::events::SmpLoadedPackedEventConditionRowSummary; + +pub(crate) fn lower_territory_target_in_condition( + target: &RuntimeTerritoryTarget, + row: &SmpLoadedPackedEventConditionRowSummary, + runtime_context: &RuntimeImportContext, +) -> Result { + if !runtime_context.has_territory_context { + return Err(RuntimeImportBlocker::MissingTerritoryContext); + } + if !row.requires_candidate_name_binding { + return Ok(target.clone()); + } + let candidate_name = row + .candidate_name + .as_ref() + .ok_or(RuntimeImportBlocker::NamedTerritoryBinding)?; + let territory_id = runtime_context + .territory_name_to_id + .get(candidate_name) + .copied() + .ok_or(RuntimeImportBlocker::NamedTerritoryBinding)?; + Ok(RuntimeTerritoryTarget::Ids { + ids: vec![territory_id], + }) +} diff --git a/crates/rrt-runtime/src/documents/lowering/contextual_effects.rs b/crates/rrt-runtime/src/documents/lowering/contextual_effects.rs new file mode 100644 index 0000000..1f7685c --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/contextual_effects.rs @@ -0,0 +1,244 @@ +use super::super::blockers::real_grouped_row_is_unsupported_chairman_target_scope; +use super::import_outcome::real_grouped_row_is_world_state_family; +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::event::effects::RuntimeEffect; +use crate::event::targets::{RuntimeCargoPriceTarget, RuntimeCargoProductionTarget}; +use crate::inspect::smp::events::{ + SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventRecordSummary, +}; + +pub(crate) fn lower_contextual_real_grouped_effects( + record: &SmpLoadedPackedEventRecordSummary, + runtime_context: &RuntimeImportContext, +) -> Result, RuntimeImportBlocker> { + if record.payload_family != "real_packed_v1" || record.compact_control.is_none() { + return Err(RuntimeImportBlocker::UnmappedWorldCondition); + } + + let mut effects = Vec::with_capacity(record.grouped_effect_rows.len()); + for row in &record.grouped_effect_rows { + if real_grouped_row_is_unsupported_chairman_target_scope(row) { + return Err(RuntimeImportBlocker::ChairmanTargetScope); + } + if let Some(effect) = lower_contextual_cargo_price_effect(row)? { + effects.push(effect); + continue; + } + if let Some(effect) = lower_contextual_world_scalar_override_effect(row)? { + effects.push(effect); + continue; + } + if let Some(effect) = lower_contextual_runtime_variable_effect(row)? { + effects.push(effect); + continue; + } + if let Some(effect) = lower_contextual_cargo_production_effect(row)? { + effects.push(effect); + continue; + } + if let Some(effect) = lower_contextual_territory_access_cost_effect(row)? { + effects.push(effect); + continue; + } + if let Some(effect) = lower_contextual_locomotive_cost_effect(row, runtime_context)? { + effects.push(effect); + continue; + } + if let Some(effect) = lower_contextual_locomotive_availability_effect(row, runtime_context)? + { + effects.push(effect); + continue; + } + return Err(if real_grouped_row_is_world_state_family(row) { + RuntimeImportBlocker::UnmappedWorldCondition + } else { + RuntimeImportBlocker::UnmappedOrdinaryCondition + }); + } + + if effects.is_empty() { + return Err(RuntimeImportBlocker::UnmappedWorldCondition); + } + + Ok(effects) +} + +pub(crate) fn lower_contextual_cargo_price_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> Result, RuntimeImportBlocker> { + if row.parameter_family.as_deref() != Some("cargo_price_scalar") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let Some(value) = u32::try_from(row.raw_scalar_value).ok() else { + return Ok(None); + }; + let target = if row.descriptor_id == 105 { + RuntimeCargoPriceTarget::All + } else if let Some(name) = row.recovered_cargo_label.as_deref() { + RuntimeCargoPriceTarget::Named { + name: name.to_string(), + } + } else { + return Ok(None); + }; + Ok(Some(RuntimeEffect::SetCargoPriceOverride { target, value })) +} + +pub(crate) fn lower_contextual_world_scalar_override_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> Result, RuntimeImportBlocker> { + if row.parameter_family.as_deref() != Some("world_scalar_override") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let Some(key) = row + .descriptor_label + .as_deref() + .map(crate::inspect::smp::events::runtime_world_scalar_key_from_label) + else { + return Ok(None); + }; + Ok(Some(RuntimeEffect::SetWorldScalarOverride { + key, + value: i64::from(row.raw_scalar_value), + })) +} + +pub(crate) fn lower_contextual_runtime_variable_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> Result, RuntimeImportBlocker> { + if row.parameter_family.as_deref() != Some("runtime_variable_scalar") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let value = i64::from(row.raw_scalar_value); + Ok(match row.descriptor_id { + 39..=42 => Some(RuntimeEffect::SetWorldVariable { + index: row.descriptor_id - 38, + value, + }), + _ => None, + }) +} + +pub(crate) fn lower_contextual_locomotive_availability_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + runtime_context: &RuntimeImportContext, +) -> Result, RuntimeImportBlocker> { + if row.parameter_family.as_deref() != Some("locomotive_availability_scalar") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let Some(value) = u32::try_from(row.raw_scalar_value).ok() else { + return Ok(None); + }; + let Some(locomotive_id) = row.recovered_locomotive_id else { + return Ok(None); + }; + let Some(name) = runtime_context + .locomotive_catalog_names_by_id + .get(&locomotive_id) + .cloned() + else { + return Err(RuntimeImportBlocker::MissingLocomotiveCatalogContext); + }; + Ok(Some(RuntimeEffect::SetNamedLocomotiveAvailabilityValue { + name, + value, + })) +} + +pub(crate) fn lower_contextual_cargo_production_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> Result, RuntimeImportBlocker> { + if row.parameter_family.as_deref() != Some("cargo_production_scalar") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let Some(value) = u32::try_from(row.raw_scalar_value).ok() else { + return Ok(None); + }; + match row.descriptor_id { + 177 => Ok(Some(RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::All, + value, + })), + 178 => Ok(Some(RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::Factory, + value, + })), + 179 => Ok(Some(RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::FarmMine, + value, + })), + 180..=229 => { + let Some(name) = row.recovered_cargo_label.clone() else { + return Err(RuntimeImportBlocker::EvidenceBlockedDescriptor); + }; + Ok(Some(RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::Named { name }, + value, + })) + } + 230..=240 => { + let Some(slot) = row.descriptor_id.checked_sub(229) else { + return Ok(None); + }; + Ok(Some(RuntimeEffect::SetCargoProductionSlot { slot, value })) + } + _ => Ok(None), + } +} + +pub(crate) fn lower_contextual_territory_access_cost_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> Result, RuntimeImportBlocker> { + if row.parameter_family.as_deref() != Some("territory_access_cost_scalar") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let Some(value) = u32::try_from(row.raw_scalar_value).ok() else { + return Ok(None); + }; + Ok(Some(RuntimeEffect::SetTerritoryAccessCost { value })) +} + +pub(crate) fn lower_contextual_locomotive_cost_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + runtime_context: &RuntimeImportContext, +) -> Result, RuntimeImportBlocker> { + if row.parameter_family.as_deref() != Some("locomotive_cost_scalar") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let value = u32::try_from(row.raw_scalar_value).ok(); + let Some(value) = value else { + return Ok(None); + }; + let Some(locomotive_id) = row.recovered_locomotive_id else { + return Ok(None); + }; + let Some(name) = runtime_context + .locomotive_catalog_names_by_id + .get(&locomotive_id) + .cloned() + else { + return Err(RuntimeImportBlocker::MissingLocomotiveCatalogContext); + }; + Ok(Some(RuntimeEffect::SetNamedLocomotiveCost { name, value })) +} diff --git a/crates/rrt-runtime/src/documents/lowering/decode/actions.rs b/crates/rrt-runtime/src/documents/lowering/decode/actions.rs new file mode 100644 index 0000000..7b39d5b --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/decode/actions.rs @@ -0,0 +1,39 @@ +use super::super::condition_targets::{ + ensure_condition_true_chairman_context, lower_condition_targets_in_effect, + lowered_condition_true_company_target, lowered_condition_true_player_target, + packed_record_condition_scope_import_blocker, +}; +use super::super::contextual_effects::lower_contextual_real_grouped_effects; +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::event::effects::RuntimeEffect; +use crate::inspect::smp::events::SmpLoadedPackedEventRecordSummary; + +pub(crate) fn lowered_record_decoded_actions( + record: &SmpLoadedPackedEventRecordSummary, + runtime_context: &RuntimeImportContext, +) -> Result, RuntimeImportBlocker> { + if let Some(blocker) = packed_record_condition_scope_import_blocker(record, runtime_context) { + return Err(blocker); + } + ensure_condition_true_chairman_context(record)?; + + let lowered_company_target = lowered_condition_true_company_target(record)?; + let lowered_player_target = lowered_condition_true_player_target(record)?; + let base_effects = if record.payload_family != "real_packed_v1" + || record.decoded_actions.len() == record.grouped_effect_rows.len() + { + record.decoded_actions.clone() + } else { + lower_contextual_real_grouped_effects(record, runtime_context)? + }; + base_effects + .iter() + .map(|effect| { + lower_condition_targets_in_effect( + effect, + lowered_company_target.as_ref(), + lowered_player_target.as_ref(), + ) + }) + .collect() +} diff --git a/crates/rrt-runtime/src/documents/lowering/decode/conditions.rs b/crates/rrt-runtime/src/documents/lowering/decode/conditions.rs new file mode 100644 index 0000000..976df09 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/decode/conditions.rs @@ -0,0 +1,35 @@ +use super::super::condition_targets::{ + lower_condition_targets_in_condition, lowered_condition_true_company_target, + lowered_condition_true_player_target, packed_record_condition_scope_import_blocker, +}; +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::event::conditions::RuntimeCondition; +use crate::inspect::smp::events::SmpLoadedPackedEventRecordSummary; + +pub(crate) fn lowered_record_decoded_conditions( + record: &SmpLoadedPackedEventRecordSummary, + runtime_context: &RuntimeImportContext, +) -> Result, RuntimeImportBlocker> { + if let Some(blocker) = packed_record_condition_scope_import_blocker(record, runtime_context) { + return Err(blocker); + } + + let lowered_company_target = lowered_condition_true_company_target(record)?; + let lowered_player_target = lowered_condition_true_player_target(record)?; + let ordinary_rows = record + .standalone_condition_rows + .iter() + .filter(|row| row.raw_condition_id >= 0); + ordinary_rows + .zip(record.decoded_conditions.iter()) + .map(|(row, condition)| { + lower_condition_targets_in_condition( + condition, + row, + lowered_company_target.as_ref(), + lowered_player_target.as_ref(), + runtime_context, + ) + }) + .collect() +} diff --git a/crates/rrt-runtime/src/documents/lowering/decode/effects.rs b/crates/rrt-runtime/src/documents/lowering/decode/effects.rs new file mode 100644 index 0000000..9271c76 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/decode/effects.rs @@ -0,0 +1,263 @@ +use super::record::smp_runtime_record_template_to_runtime; +use super::targets::{ + import_chairman_target, import_company_target, import_player_target, import_territory_target, +}; +use crate::documents::RuntimeImportContext; +use crate::event::effects::RuntimeEffect; + +pub(crate) fn smp_runtime_effects_to_runtime_effects( + effects: &[RuntimeEffect], + runtime_context: &RuntimeImportContext, + allow_condition_true_company: bool, + allow_condition_true_player: bool, +) -> Result, String> { + effects + .iter() + .map(|effect| { + smp_runtime_effect_to_runtime_effect( + effect, + runtime_context, + allow_condition_true_company, + allow_condition_true_player, + ) + }) + .collect() +} + +pub(crate) fn smp_runtime_effect_to_runtime_effect( + effect: &RuntimeEffect, + runtime_context: &RuntimeImportContext, + allow_condition_true_company: bool, + allow_condition_true_player: bool, +) -> Result { + match effect { + RuntimeEffect::SetWorldFlag { key, value } => Ok(RuntimeEffect::SetWorldFlag { + key: key.clone(), + value: *value, + }), + RuntimeEffect::SetWorldVariable { index, value } => Ok(RuntimeEffect::SetWorldVariable { + index: *index, + value: *value, + }), + RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { + Ok(RuntimeEffect::SetLimitedTrackBuildingAmount { value: *value }) + } + RuntimeEffect::SetEconomicStatusCode { value } => { + Ok(RuntimeEffect::SetEconomicStatusCode { value: *value }) + } + RuntimeEffect::SetCompanyVariable { + target, + index, + value, + } => Ok(RuntimeEffect::SetCompanyVariable { + target: import_company_target(target, runtime_context, allow_condition_true_company)?, + index: *index, + value: *value, + }), + RuntimeEffect::SetCompanyCash { target, value } => Ok(RuntimeEffect::SetCompanyCash { + target: import_company_target(target, runtime_context, allow_condition_true_company)?, + value: *value, + }), + RuntimeEffect::SetPlayerVariable { + target, + index, + value, + } => Ok(RuntimeEffect::SetPlayerVariable { + target: import_player_target(target, runtime_context, allow_condition_true_player)?, + index: *index, + value: *value, + }), + RuntimeEffect::SetPlayerCash { target, value } => Ok(RuntimeEffect::SetPlayerCash { + target: import_player_target(target, runtime_context, allow_condition_true_player)?, + value: *value, + }), + RuntimeEffect::SetTerritoryVariable { + target, + index, + value, + } => Ok(RuntimeEffect::SetTerritoryVariable { + target: import_territory_target(target, runtime_context)?, + index: *index, + value: *value, + }), + RuntimeEffect::SetChairmanCash { target, value } => Ok(RuntimeEffect::SetChairmanCash { + target: import_chairman_target(target, runtime_context)?, + value: *value, + }), + RuntimeEffect::DeactivatePlayer { target } => Ok(RuntimeEffect::DeactivatePlayer { + target: import_player_target(target, runtime_context, allow_condition_true_player)?, + }), + RuntimeEffect::DeactivateChairman { target } => Ok(RuntimeEffect::DeactivateChairman { + target: import_chairman_target(target, runtime_context)?, + }), + RuntimeEffect::SetCompanyTerritoryAccess { + target, + territory, + value, + } => Ok(RuntimeEffect::SetCompanyTerritoryAccess { + target: import_company_target(target, runtime_context, allow_condition_true_company)?, + territory: import_territory_target(territory, runtime_context)?, + value: *value, + }), + RuntimeEffect::ConfiscateCompanyAssets { target } => { + if !runtime_context.has_train_context { + Err("packed effect requires runtime train context".to_string()) + } else { + Ok(RuntimeEffect::ConfiscateCompanyAssets { + target: import_company_target( + target, + runtime_context, + allow_condition_true_company, + )?, + }) + } + } + RuntimeEffect::DeactivateCompany { target } => Ok(RuntimeEffect::DeactivateCompany { + target: import_company_target(target, runtime_context, allow_condition_true_company)?, + }), + RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { + Ok(RuntimeEffect::SetCompanyTrackLayingCapacity { + target: import_company_target( + target, + runtime_context, + allow_condition_true_company, + )?, + value: *value, + }) + } + RuntimeEffect::SetCompanyGovernanceScalar { + target, + metric, + value, + } => Ok(RuntimeEffect::SetCompanyGovernanceScalar { + target: import_company_target(target, runtime_context, allow_condition_true_company)?, + metric: *metric, + value: *value, + }), + RuntimeEffect::RetireTrains { + company_target, + territory_target, + locomotive_name, + } => { + if !runtime_context.has_train_context { + Err("packed effect requires runtime train context".to_string()) + } else if territory_target.is_some() && !runtime_context.has_train_territory_context { + Err("packed train effect requires runtime train territory context".to_string()) + } else { + let imported_company_target = company_target + .as_ref() + .map(|target| { + import_company_target(target, runtime_context, allow_condition_true_company) + }) + .transpose()?; + let imported_territory_target = territory_target + .as_ref() + .map(|target| import_territory_target(target, runtime_context)) + .transpose()?; + Ok(RuntimeEffect::RetireTrains { + company_target: imported_company_target, + territory_target: imported_territory_target, + locomotive_name: locomotive_name.clone(), + }) + } + } + RuntimeEffect::AdjustCompanyCash { target, delta } => { + Ok(RuntimeEffect::AdjustCompanyCash { + target: import_company_target( + target, + runtime_context, + allow_condition_true_company, + )?, + delta: *delta, + }) + } + RuntimeEffect::AdjustCompanyDebt { target, delta } => { + Ok(RuntimeEffect::AdjustCompanyDebt { + target: import_company_target( + target, + runtime_context, + allow_condition_true_company, + )?, + delta: *delta, + }) + } + RuntimeEffect::SetCandidateAvailability { name, value } => { + Ok(RuntimeEffect::SetCandidateAvailability { + name: name.clone(), + value: *value, + }) + } + RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => { + Ok(RuntimeEffect::SetNamedLocomotiveAvailability { + name: name.clone(), + value: *value, + }) + } + RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => { + Ok(RuntimeEffect::SetNamedLocomotiveAvailabilityValue { + name: name.clone(), + value: *value, + }) + } + RuntimeEffect::SetNamedLocomotiveCost { name, value } => { + Ok(RuntimeEffect::SetNamedLocomotiveCost { + name: name.clone(), + value: *value, + }) + } + RuntimeEffect::SetCargoPriceOverride { target, value } => { + Ok(RuntimeEffect::SetCargoPriceOverride { + target: target.clone(), + value: *value, + }) + } + RuntimeEffect::SetCargoProductionOverride { target, value } => { + Ok(RuntimeEffect::SetCargoProductionOverride { + target: target.clone(), + value: *value, + }) + } + RuntimeEffect::SetWorldScalarOverride { key, value } => { + Ok(RuntimeEffect::SetWorldScalarOverride { + key: key.clone(), + value: *value, + }) + } + RuntimeEffect::SetCargoProductionSlot { slot, value } => { + Ok(RuntimeEffect::SetCargoProductionSlot { + slot: *slot, + value: *value, + }) + } + RuntimeEffect::SetTerritoryAccessCost { value } => { + Ok(RuntimeEffect::SetTerritoryAccessCost { value: *value }) + } + RuntimeEffect::SetSpecialCondition { label, value } => { + Ok(RuntimeEffect::SetSpecialCondition { + label: label.clone(), + value: *value, + }) + } + RuntimeEffect::AppendEventRecord { record } => Ok(RuntimeEffect::AppendEventRecord { + record: Box::new(smp_runtime_record_template_to_runtime( + record, + runtime_context, + allow_condition_true_company, + allow_condition_true_player, + )?), + }), + RuntimeEffect::ActivateEventRecord { record_id } => { + Ok(RuntimeEffect::ActivateEventRecord { + record_id: *record_id, + }) + } + RuntimeEffect::DeactivateEventRecord { record_id } => { + Ok(RuntimeEffect::DeactivateEventRecord { + record_id: *record_id, + }) + } + RuntimeEffect::RemoveEventRecord { record_id } => Ok(RuntimeEffect::RemoveEventRecord { + record_id: *record_id, + }), + } +} diff --git a/crates/rrt-runtime/src/documents/lowering/decode/mod.rs b/crates/rrt-runtime/src/documents/lowering/decode/mod.rs new file mode 100644 index 0000000..f99cfbf --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/decode/mod.rs @@ -0,0 +1,9 @@ +mod actions; +mod conditions; +mod effects; +mod record; +mod targets; + +pub(crate) use actions::lowered_record_decoded_actions; +pub(crate) use conditions::lowered_record_decoded_conditions; +pub(crate) use record::smp_packed_record_to_runtime_event_record; diff --git a/crates/rrt-runtime/src/documents/lowering/decode/record.rs b/crates/rrt-runtime/src/documents/lowering/decode/record.rs new file mode 100644 index 0000000..aab64fc --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/decode/record.rs @@ -0,0 +1,81 @@ +use super::super::import_outcome::conditions_provide_company_context; +use super::actions::lowered_record_decoded_actions; +use super::conditions::lowered_record_decoded_conditions; +use super::effects::smp_runtime_effects_to_runtime_effects; +use crate::documents::RuntimeImportContext; +use crate::event::records::{RuntimeEventRecord, RuntimeEventRecordTemplate}; +use crate::inspect::smp::events::SmpLoadedPackedEventRecordSummary; + +pub(crate) fn smp_packed_record_to_runtime_event_record( + record: &SmpLoadedPackedEventRecordSummary, + runtime_context: &RuntimeImportContext, +) -> Option> { + if record.decode_status == "unsupported_framing" { + return None; + } + if record.payload_family == "real_packed_v1" && record.compact_control.is_none() { + return None; + } + + let lowered_conditions = match lowered_record_decoded_conditions(record, runtime_context) { + Ok(conditions) => conditions, + Err(_) => return None, + }; + let lowered_effects = match lowered_record_decoded_actions(record, runtime_context) { + Ok(effects) => effects, + Err(_) => return None, + }; + let effects = match smp_runtime_effects_to_runtime_effects( + &lowered_effects, + runtime_context, + conditions_provide_company_context(&lowered_conditions), + false, + ) { + Ok(effects) => effects, + Err(_) => return None, + }; + + Some((|| { + let trigger_kind = record.trigger_kind.ok_or_else(|| { + format!( + "packed event record {} is missing trigger_kind", + record.live_entry_id + ) + })?; + let active = record.active.unwrap_or(true); + let marks_collection_dirty = record.marks_collection_dirty.unwrap_or(false); + let one_shot = record.one_shot.unwrap_or(false); + Ok(RuntimeEventRecordTemplate { + record_id: record.live_entry_id, + trigger_kind, + active, + marks_collection_dirty, + one_shot, + conditions: lowered_conditions, + effects, + } + .into_runtime_record()) + })()) +} + +pub(crate) fn smp_runtime_record_template_to_runtime( + record: &RuntimeEventRecordTemplate, + runtime_context: &RuntimeImportContext, + allow_condition_true_company: bool, + allow_condition_true_player: bool, +) -> Result { + Ok(RuntimeEventRecordTemplate { + record_id: record.record_id, + trigger_kind: record.trigger_kind, + active: record.active, + marks_collection_dirty: record.marks_collection_dirty, + one_shot: record.one_shot, + conditions: record.conditions.clone(), + effects: smp_runtime_effects_to_runtime_effects( + &record.effects, + runtime_context, + allow_condition_true_company, + allow_condition_true_player, + )?, + }) +} diff --git a/crates/rrt-runtime/src/documents/lowering/decode/targets.rs b/crates/rrt-runtime/src/documents/lowering/decode/targets.rs new file mode 100644 index 0000000..2e0a1fa --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/decode/targets.rs @@ -0,0 +1,56 @@ +use super::super::super::blockers::territory_target_import_blocker; +use super::super::condition_targets::chairman_target_import_blocker; +use super::super::import_outcome::{ + company_target_allowed_for_import, company_target_import_error_message, + player_target_allowed_for_import, player_target_import_error_message, +}; +use crate::documents::RuntimeImportContext; +use crate::event::targets::{ + RuntimeChairmanTarget, RuntimeCompanyTarget, RuntimePlayerTarget, RuntimeTerritoryTarget, +}; + +pub(super) fn import_company_target( + target: &RuntimeCompanyTarget, + runtime_context: &RuntimeImportContext, + allow_condition_true_company: bool, +) -> Result { + if company_target_allowed_for_import(target, runtime_context, allow_condition_true_company) { + Ok(target.clone()) + } else { + Err(company_target_import_error_message(target, runtime_context)) + } +} + +pub(super) fn import_player_target( + target: &RuntimePlayerTarget, + runtime_context: &RuntimeImportContext, + allow_condition_true_player: bool, +) -> Result { + if player_target_allowed_for_import(target, runtime_context, allow_condition_true_player) { + Ok(target.clone()) + } else { + Err(player_target_import_error_message(target, runtime_context)) + } +} + +pub(super) fn import_territory_target( + target: &RuntimeTerritoryTarget, + runtime_context: &RuntimeImportContext, +) -> Result { + if territory_target_import_blocker(target, runtime_context).is_none() { + Ok(target.clone()) + } else { + Err("packed effect requires territory runtime context".to_string()) + } +} + +pub(super) fn import_chairman_target( + target: &RuntimeChairmanTarget, + runtime_context: &RuntimeImportContext, +) -> Result { + if chairman_target_import_blocker(target, runtime_context).is_none() { + Ok(target.clone()) + } else { + Err("packed effect requires chairman runtime context".to_string()) + } +} diff --git a/crates/rrt-runtime/src/documents/lowering/import_outcome/conditions.rs b/crates/rrt-runtime/src/documents/lowering/import_outcome/conditions.rs new file mode 100644 index 0000000..2d1d9f9 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/import_outcome/conditions.rs @@ -0,0 +1,71 @@ +use crate::event::conditions::RuntimeCondition; +use crate::inspect::smp::events::{ + SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventRecordSummary, +}; + +pub(crate) fn conditions_provide_company_context(conditions: &[RuntimeCondition]) -> bool { + conditions.iter().any(|condition| { + matches!( + condition, + RuntimeCondition::CompanyNumericThreshold { .. } + | RuntimeCondition::CompanyVariableThreshold { .. } + | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } + ) + }) +} + +pub(crate) fn record_has_world_state_condition_rows( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .decoded_conditions + .iter() + .any(runtime_condition_is_world_state) + || record + .standalone_condition_rows + .iter() + .any(ordinary_condition_row_is_world_state_family) +} + +pub(crate) fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool { + matches!( + condition, + RuntimeCondition::WorldVariableThreshold { .. } + | RuntimeCondition::SpecialConditionThreshold { .. } + | RuntimeCondition::CandidateAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionSlotThreshold { .. } + | RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::FactoryProductionTotalThreshold { .. } + | RuntimeCondition::FarmMineProductionTotalThreshold { .. } + | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } + | RuntimeCondition::EconomicStatusCodeThreshold { .. } + | RuntimeCondition::WorldFlagEquals { .. } + ) +} + +pub(crate) fn ordinary_condition_row_is_world_state_family( + row: &SmpLoadedPackedEventConditionRowSummary, +) -> bool { + row.metric.as_deref().is_some_and(|metric| { + metric.contains("Special Condition") + || metric.contains("Candidate Availability") + || metric.contains("Named Locomotive") + || metric.contains("Cargo Production") + || metric.contains("Factory Production") + || metric.contains("Farm/Mine Production") + || metric.contains("Other Cargo Production") + || metric.contains("Limited Track Building Amount") + || metric.contains("Territory Access Cost") + || metric.contains("Economic Status") + || metric.contains("World Flag") + }) || row.semantic_family.as_deref().is_some_and(|family| { + matches!( + family, + "world_state_threshold" | "world_scalar_threshold" | "world_flag_equals" + ) + }) +} diff --git a/crates/rrt-runtime/src/documents/lowering/import_outcome/descriptors.rs b/crates/rrt-runtime/src/documents/lowering/import_outcome/descriptors.rs new file mode 100644 index 0000000..c36c6f4 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/import_outcome/descriptors.rs @@ -0,0 +1,81 @@ +use crate::inspect::smp::events::SmpLoadedPackedEventGroupedEffectRowSummary; + +pub(crate) fn real_grouped_row_is_world_state_family( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + row.target_mask_bits == Some(0x08) + || row.parameter_family.as_deref().is_some_and(|family| { + family.starts_with("whole_game_") + || family.starts_with("special_condition") + || family.starts_with("candidate_availability") + || family.starts_with("world_flag") + }) + || row.descriptor_label.as_deref().is_some_and(|label| { + label.contains("Special Condition") + || label.contains("Candidate Availability") + || label.contains("World Flag") + || label == "Economic Status" + }) +} + +pub(crate) fn real_grouped_row_is_shell_owned_descriptor_family( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + real_grouped_row_has_runtime_status(row, "shell_owned") +} + +pub(crate) fn real_grouped_row_is_evidence_blocked_descriptor_family( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + real_grouped_row_has_runtime_status(row, "evidence_blocked") +} + +pub(crate) fn real_grouped_row_is_variant_or_scope_blocked_descriptor_family( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + real_grouped_row_has_runtime_status(row, "variant_or_scope_blocked") +} + +pub(crate) fn real_grouped_row_has_runtime_status( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + status: &str, +) -> bool { + crate::inspect::smp::catalog::grouped_effect_descriptor_runtime_status_name(row.descriptor_id) + .is_some_and(|runtime_status| runtime_status == status) +} + +pub(crate) fn real_grouped_row_is_unsupported_executable_descriptor_variant( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + if !real_grouped_row_has_runtime_status(row, "executable") { + return false; + } + match row.descriptor_id { + 1 => !(row.opcode == 8 && row.row_shape == "multivalue_scalar"), + 2 => !(row.opcode == 8 && row.row_shape == "multivalue_scalar"), + 8 | 108 | 109 | 122 => row.row_shape != "scalar_assignment", + 13 | 14 => !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0), + 56 | 57 => row.row_shape != "scalar_assignment", + _ => match row.parameter_family.as_deref() { + Some("world_scalar_override") => row.row_shape != "scalar_assignment", + Some("runtime_variable_scalar") => row.row_shape != "scalar_assignment", + Some("locomotive_availability_scalar") => { + !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) + } + Some("locomotive_cost_scalar") => { + !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) + } + Some("cargo_price_scalar") => { + !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) + } + Some("cargo_production_scalar") => { + matches!(row.descriptor_id, 177 | 178 | 179 | 230..=240) + && !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) + } + Some("territory_access_cost_scalar") => { + !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) + } + _ => false, + }, + } +} diff --git a/crates/rrt-runtime/src/documents/lowering/import_outcome/mod.rs b/crates/rrt-runtime/src/documents/lowering/import_outcome/mod.rs new file mode 100644 index 0000000..9509d7b --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/import_outcome/mod.rs @@ -0,0 +1,10 @@ +mod conditions; +mod descriptors; +mod outcome; +mod targets; +mod variants; + +pub(crate) use conditions::*; +pub(crate) use descriptors::*; +pub(crate) use outcome::*; +pub(crate) use targets::*; diff --git a/crates/rrt-runtime/src/documents/lowering/import_outcome/outcome.rs b/crates/rrt-runtime/src/documents/lowering/import_outcome/outcome.rs new file mode 100644 index 0000000..9ec1d34 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/import_outcome/outcome.rs @@ -0,0 +1,124 @@ +use super::super::super::blockers::{packed_record_import_blocker, runtime_import_blocker_outcome}; +use super::super::condition_targets::packed_record_condition_scope_import_blocker; +use super::super::decode::lowered_record_decoded_actions; +use super::variants::{ + record_has_unsupported_chairman_target_scope, record_has_unsupported_confiscation_variant, + record_has_unsupported_retire_train_scope, record_has_unsupported_retire_train_variant, + record_has_unsupported_territory_access_scope, record_has_unsupported_territory_access_variant, +}; +use super::{ + real_grouped_row_is_evidence_blocked_descriptor_family, + real_grouped_row_is_shell_owned_descriptor_family, + real_grouped_row_is_unsupported_executable_descriptor_variant, + real_grouped_row_is_variant_or_scope_blocked_descriptor_family, + real_grouped_row_is_world_state_family, record_has_world_state_condition_rows, +}; +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::inspect::smp::events::SmpLoadedPackedEventRecordSummary; + +pub(crate) fn determine_packed_event_import_outcome( + record: &SmpLoadedPackedEventRecordSummary, + runtime_context: &RuntimeImportContext, + imported: bool, +) -> String { + if imported { + return "imported".to_string(); + } + if record.decode_status == "unsupported_framing" { + return "blocked_unsupported_decode".to_string(); + } + if record.payload_family == "real_packed_v1" { + if record.compact_control.is_none() { + return "blocked_missing_compact_control".to_string(); + } + if !record.executable_import_ready { + if let Err(blocker) = lowered_record_decoded_actions(record, runtime_context) { + if matches!( + blocker, + RuntimeImportBlocker::MissingLocomotiveCatalogContext + | RuntimeImportBlocker::MissingChairmanContext + | RuntimeImportBlocker::ChairmanTargetScope + ) { + return runtime_import_blocker_outcome(blocker).to_string(); + } + } + if record_has_unsupported_chairman_target_scope(record) { + return "blocked_chairman_target_scope".to_string(); + } + if record_has_unsupported_territory_access_scope(record) { + return "blocked_territory_access_scope".to_string(); + } + if record_has_unsupported_territory_access_variant(record) { + return "blocked_territory_access_variant".to_string(); + } + if record_has_unsupported_confiscation_variant(record) { + return "blocked_confiscation_variant".to_string(); + } + if record_has_unsupported_retire_train_scope(record) { + return "blocked_retire_train_scope".to_string(); + } + if record_has_unsupported_retire_train_variant(record) { + return "blocked_retire_train_variant".to_string(); + } + if record + .standalone_condition_rows + .iter() + .any(|row| row.raw_condition_id >= 0) + { + if record_has_world_state_condition_rows(record) { + return "blocked_unmapped_world_condition".to_string(); + } else { + return "blocked_unmapped_ordinary_condition".to_string(); + } + } + if record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_shell_owned_descriptor_family) + { + return "blocked_shell_owned_descriptor".to_string(); + } + if record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_evidence_blocked_descriptor_family) + { + return "blocked_evidence_blocked_descriptor".to_string(); + } + if record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_variant_or_scope_blocked_descriptor_family) + { + return "blocked_variant_or_scope_blocked_descriptor".to_string(); + } + if record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_unsupported_executable_descriptor_variant) + { + return "blocked_variant_or_scope_blocked_descriptor".to_string(); + } + if record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_world_state_family) + { + return "blocked_unmapped_world_descriptor".to_string(); + } + return "blocked_unmapped_real_descriptor".to_string(); + } + if let Some(blocker) = packed_record_condition_scope_import_blocker(record, runtime_context) + { + return runtime_import_blocker_outcome(blocker).to_string(); + } + if let Some(blocker) = packed_record_import_blocker(record, runtime_context) { + return runtime_import_blocker_outcome(blocker).to_string(); + } + return "blocked_unsupported_decode".to_string(); + } + if let Some(blocker) = packed_record_import_blocker(record, runtime_context) { + return runtime_import_blocker_outcome(blocker).to_string(); + } + "blocked_unsupported_decode".to_string() +} diff --git a/crates/rrt-runtime/src/documents/lowering/import_outcome/targets.rs b/crates/rrt-runtime/src/documents/lowering/import_outcome/targets.rs new file mode 100644 index 0000000..d270384 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/import_outcome/targets.rs @@ -0,0 +1,194 @@ +use crate::documents::{RuntimeImportBlocker, RuntimeImportContext}; +use crate::event::targets::{RuntimeCompanyTarget, RuntimePlayerTarget}; + +pub(crate) fn company_target_allowed_for_import( + target: &RuntimeCompanyTarget, + runtime_context: &RuntimeImportContext, + allow_condition_true_company: bool, +) -> bool { + match company_target_import_blocker(target, runtime_context) { + None => true, + Some(RuntimeImportBlocker::MissingConditionContext) + if allow_condition_true_company + && matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) => + { + true + } + Some(_) => false, + } +} + +pub(crate) fn player_target_allowed_for_import( + target: &RuntimePlayerTarget, + runtime_context: &RuntimeImportContext, + allow_condition_true_player: bool, +) -> bool { + match player_target_import_blocker(target, runtime_context) { + None => true, + Some(RuntimeImportBlocker::MissingPlayerConditionContext) + if allow_condition_true_player + && matches!(target, RuntimePlayerTarget::ConditionTruePlayer) => + { + true + } + Some(_) => false, + } +} + +pub(crate) fn company_target_import_blocker( + target: &RuntimeCompanyTarget, + runtime_context: &RuntimeImportContext, +) -> Option { + match target { + RuntimeCompanyTarget::AllActive => None, + RuntimeCompanyTarget::Ids { ids } => { + if ids.is_empty() + || ids + .iter() + .any(|company_id| !runtime_context.known_company_ids.contains(company_id)) + { + Some(RuntimeImportBlocker::MissingCompanyContext) + } else { + None + } + } + RuntimeCompanyTarget::HumanCompanies | RuntimeCompanyTarget::AiCompanies => { + if !runtime_context.has_complete_company_controller_context { + Some(RuntimeImportBlocker::MissingCompanyRoleContext) + } else { + None + } + } + RuntimeCompanyTarget::SelectedCompany => { + if runtime_context.selected_company_id.is_some() { + None + } else { + Some(RuntimeImportBlocker::MissingSelectionContext) + } + } + RuntimeCompanyTarget::ConditionTrueCompany => { + Some(RuntimeImportBlocker::MissingConditionContext) + } + } +} + +pub(crate) fn company_target_import_error_message( + target: &RuntimeCompanyTarget, + runtime_context: &RuntimeImportContext, +) -> String { + match company_target_import_blocker(target, runtime_context) { + Some(RuntimeImportBlocker::MissingCompanyContext) => { + "packed company effect requires resolved company ids".to_string() + } + Some(RuntimeImportBlocker::MissingSelectionContext) => { + "packed company effect requires selected_company_id context".to_string() + } + Some(RuntimeImportBlocker::MissingCompanyRoleContext) => { + "packed company effect requires company controller role context".to_string() + } + Some(RuntimeImportBlocker::MissingConditionContext) => { + "packed company effect requires condition-relative context".to_string() + } + Some(RuntimeImportBlocker::CompanyConditionScopeDisabled) => { + "packed company effect disables company-side negative-sentinel condition scope" + .to_string() + } + Some(RuntimeImportBlocker::MissingTerritoryContext) => { + "packed condition requires territory runtime context".to_string() + } + Some(RuntimeImportBlocker::NamedTerritoryBinding) => { + "packed condition requires named territory binding".to_string() + } + Some(RuntimeImportBlocker::EvidenceBlockedDescriptor) => { + "packed descriptor is still evidence-blocked".to_string() + } + Some(RuntimeImportBlocker::UnmappedOrdinaryCondition) => { + "packed ordinary condition is not yet mapped".to_string() + } + Some(RuntimeImportBlocker::UnmappedWorldCondition) => { + "packed whole-game condition is not yet mapped".to_string() + } + Some(RuntimeImportBlocker::MissingTrainContext) => { + "packed effect requires runtime train context".to_string() + } + Some(RuntimeImportBlocker::MissingTrainTerritoryContext) => { + "packed train effect requires runtime train territory context".to_string() + } + Some(RuntimeImportBlocker::MissingLocomotiveCatalogContext) => { + "packed locomotive availability row requires locomotive catalog context".to_string() + } + Some(RuntimeImportBlocker::MissingPlayerContext) + | Some(RuntimeImportBlocker::MissingPlayerSelectionContext) + | Some(RuntimeImportBlocker::MissingPlayerRoleContext) + | Some(RuntimeImportBlocker::MissingChairmanContext) + | Some(RuntimeImportBlocker::ChairmanTargetScope) + | Some(RuntimeImportBlocker::MissingPlayerConditionContext) => { + "packed company effect is blocked by non-company import context".to_string() + } + None => "packed company effect is importable".to_string(), + } +} + +pub(crate) fn player_target_import_blocker( + target: &RuntimePlayerTarget, + runtime_context: &RuntimeImportContext, +) -> Option { + match target { + RuntimePlayerTarget::AllActive => { + if runtime_context.known_player_ids.is_empty() { + Some(RuntimeImportBlocker::MissingPlayerContext) + } else { + None + } + } + RuntimePlayerTarget::Ids { ids } => { + if ids.is_empty() + || ids + .iter() + .any(|player_id| !runtime_context.known_player_ids.contains(player_id)) + { + Some(RuntimeImportBlocker::MissingPlayerContext) + } else { + None + } + } + RuntimePlayerTarget::HumanPlayers | RuntimePlayerTarget::AiPlayers => { + if !runtime_context.has_complete_player_controller_context { + Some(RuntimeImportBlocker::MissingPlayerRoleContext) + } else { + None + } + } + RuntimePlayerTarget::SelectedPlayer => { + if runtime_context.selected_player_id.is_some() { + None + } else { + Some(RuntimeImportBlocker::MissingPlayerSelectionContext) + } + } + RuntimePlayerTarget::ConditionTruePlayer => { + Some(RuntimeImportBlocker::MissingPlayerConditionContext) + } + } +} + +pub(crate) fn player_target_import_error_message( + target: &RuntimePlayerTarget, + runtime_context: &RuntimeImportContext, +) -> String { + match player_target_import_blocker(target, runtime_context) { + Some(RuntimeImportBlocker::MissingPlayerContext) => { + "packed player effect requires resolved player ids".to_string() + } + Some(RuntimeImportBlocker::MissingPlayerSelectionContext) => { + "packed player effect requires selected_player_id context".to_string() + } + Some(RuntimeImportBlocker::MissingPlayerRoleContext) => { + "packed player effect requires player controller role context".to_string() + } + Some(RuntimeImportBlocker::MissingPlayerConditionContext) => { + "packed player effect requires player condition-relative context".to_string() + } + _ => "packed player effect is importable".to_string(), + } +} diff --git a/crates/rrt-runtime/src/documents/lowering/import_outcome/variants.rs b/crates/rrt-runtime/src/documents/lowering/import_outcome/variants.rs new file mode 100644 index 0000000..1e6aa49 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/import_outcome/variants.rs @@ -0,0 +1,63 @@ +use super::super::super::blockers::{ + real_grouped_row_is_unsupported_chairman_target_scope, + real_grouped_row_is_unsupported_confiscation_variant, + real_grouped_row_is_unsupported_retire_train_scope, + real_grouped_row_is_unsupported_retire_train_variant, + real_grouped_row_is_unsupported_territory_access_scope, + real_grouped_row_is_unsupported_territory_access_variant, +}; +use crate::inspect::smp::events::SmpLoadedPackedEventRecordSummary; + +pub(super) fn record_has_unsupported_chairman_target_scope( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_unsupported_chairman_target_scope) +} + +pub(super) fn record_has_unsupported_territory_access_scope( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_unsupported_territory_access_scope) +} + +pub(super) fn record_has_unsupported_territory_access_variant( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_unsupported_territory_access_variant) +} + +pub(super) fn record_has_unsupported_confiscation_variant( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_unsupported_confiscation_variant) +} + +pub(super) fn record_has_unsupported_retire_train_scope( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_unsupported_retire_train_scope) +} + +pub(super) fn record_has_unsupported_retire_train_variant( + record: &SmpLoadedPackedEventRecordSummary, +) -> bool { + record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_unsupported_retire_train_variant) +} diff --git a/crates/rrt-runtime/src/documents/lowering/mod.rs b/crates/rrt-runtime/src/documents/lowering/mod.rs new file mode 100644 index 0000000..317b071 --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/mod.rs @@ -0,0 +1,10 @@ +mod condition_targets; +mod contextual_effects; +mod decode; +mod import_outcome; +mod summary; + +pub(super) use condition_targets::*; +pub(super) use decode::*; +pub(super) use import_outcome::*; +pub(super) use summary::*; diff --git a/crates/rrt-runtime/src/documents/lowering/summary.rs b/crates/rrt-runtime/src/documents/lowering/summary.rs new file mode 100644 index 0000000..354676d --- /dev/null +++ b/crates/rrt-runtime/src/documents/lowering/summary.rs @@ -0,0 +1,266 @@ +use std::collections::BTreeSet; + +use super::super::blockers::classify_real_grouped_company_targets; +use super::decode::{ + lowered_record_decoded_actions, lowered_record_decoded_conditions, + smp_packed_record_to_runtime_event_record, +}; +use super::import_outcome::determine_packed_event_import_outcome; +use crate::documents::RuntimeImportContext; +use crate::event::packed::{ + RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, + RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, + RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, + RuntimePackedEventTextBandSummary, +}; +use crate::event::records::RuntimeEventRecord; +use crate::inspect::smp::events::{ + SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary, + SmpLoadedPackedEventTextBandSummary, +}; +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::RuntimeCargoCatalogEntry; + +pub(crate) fn project_packed_event_collection( + save_slice: &SmpLoadedSaveSlice, + runtime_context: &RuntimeImportContext, + cargo_catalog: &[RuntimeCargoCatalogEntry], +) -> Result< + ( + Option, + Vec, + ), + String, +> { + let Some(summary) = save_slice.event_runtime_collection.as_ref() else { + return Ok((None, Vec::new())); + }; + + let mut imported_runtime_records = Vec::new(); + let mut imported_record_ids = BTreeSet::new(); + for record in &summary.records { + if let Some(import_result) = + smp_packed_record_to_runtime_event_record(record, runtime_context) + { + let runtime_record = import_result?; + imported_record_ids.insert(record.live_entry_id); + imported_runtime_records.push(runtime_record); + } + } + + let records = summary + .records + .iter() + .map(|record| { + runtime_packed_event_record_summary_from_smp( + record, + runtime_context, + cargo_catalog, + imported_record_ids.contains(&record.live_entry_id), + ) + }) + .collect::>(); + + Ok(( + Some(RuntimePackedEventCollectionSummary { + source_kind: summary.source_kind.clone(), + mechanism_family: summary.mechanism_family.clone(), + mechanism_confidence: summary.mechanism_confidence.clone(), + container_profile_family: summary.container_profile_family.clone(), + packed_state_version: summary.packed_state_version, + packed_state_version_hex: summary.packed_state_version_hex.clone(), + live_id_bound: summary.live_id_bound, + live_record_count: summary.live_record_count, + live_entry_ids: summary.live_entry_ids.clone(), + decoded_record_count: records + .iter() + .filter(|record| record.decode_status != "unsupported_framing") + .count(), + imported_runtime_record_count: imported_runtime_records.len(), + records, + }), + imported_runtime_records, + )) +} + +pub(crate) fn runtime_packed_event_record_summary_from_smp( + record: &SmpLoadedPackedEventRecordSummary, + runtime_context: &RuntimeImportContext, + cargo_catalog: &[RuntimeCargoCatalogEntry], + imported: bool, +) -> RuntimePackedEventRecordSummary { + let lowered_decoded_conditions = lowered_record_decoded_conditions(record, runtime_context) + .unwrap_or_else(|_| record.decoded_conditions.clone()); + let lowered_decoded_actions = lowered_record_decoded_actions(record, runtime_context) + .unwrap_or_else(|_| record.decoded_actions.clone()); + RuntimePackedEventRecordSummary { + record_index: record.record_index, + live_entry_id: record.live_entry_id, + payload_offset: record.payload_offset, + payload_len: record.payload_len, + decode_status: record.decode_status.clone(), + payload_family: record.payload_family.clone(), + trigger_kind: record.trigger_kind, + active: record.active, + marks_collection_dirty: record.marks_collection_dirty, + one_shot: record.one_shot, + compact_control: record + .compact_control + .as_ref() + .map(runtime_packed_event_compact_control_summary_from_smp), + text_bands: record + .text_bands + .iter() + .map(runtime_packed_event_text_band_summary_from_smp) + .collect(), + standalone_condition_row_count: record.standalone_condition_row_count, + standalone_condition_rows: record + .standalone_condition_rows + .iter() + .map(|row| runtime_packed_event_condition_row_summary_from_smp(row, cargo_catalog)) + .collect(), + negative_sentinel_scope: record + .negative_sentinel_scope + .as_ref() + .map(runtime_packed_event_negative_sentinel_scope_summary_from_smp), + grouped_effect_row_counts: record.grouped_effect_row_counts.clone(), + grouped_effect_rows: record + .grouped_effect_rows + .iter() + .map(|row| runtime_packed_event_grouped_effect_row_summary_from_smp(row, cargo_catalog)) + .collect(), + grouped_company_targets: classify_real_grouped_company_targets(record), + decoded_conditions: lowered_decoded_conditions, + decoded_actions: lowered_decoded_actions, + executable_import_ready: record.executable_import_ready, + import_outcome: Some(determine_packed_event_import_outcome( + record, + runtime_context, + imported, + )), + notes: record.notes.clone(), + } +} + +pub(crate) fn runtime_packed_event_negative_sentinel_scope_summary_from_smp( + scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, +) -> RuntimePackedEventNegativeSentinelScopeSummary { + RuntimePackedEventNegativeSentinelScopeSummary { + company_test_scope: scope.company_test_scope, + player_test_scope: scope.player_test_scope, + territory_scope_selector_is_0x63: scope.territory_scope_selector_is_0x63, + source_row_indexes: scope.source_row_indexes.clone(), + } +} + +pub(crate) fn runtime_packed_event_compact_control_summary_from_smp( + control: &crate::inspect::smp::events::SmpLoadedPackedEventCompactControlSummary, +) -> RuntimePackedEventCompactControlSummary { + RuntimePackedEventCompactControlSummary { + mode_byte_0x7ef: control.mode_byte_0x7ef, + primary_selector_0x7f0: control.primary_selector_0x7f0, + grouped_mode_0x7f4: control.grouped_mode_0x7f4, + one_shot_header_0x7f5: control.one_shot_header_0x7f5, + modifier_flag_0x7f9: control.modifier_flag_0x7f9, + modifier_flag_0x7fa: control.modifier_flag_0x7fa, + grouped_target_scope_ordinals_0x7fb: control.grouped_target_scope_ordinals_0x7fb.clone(), + grouped_scope_checkboxes_0x7ff: control.grouped_scope_checkboxes_0x7ff.clone(), + summary_toggle_0x800: control.summary_toggle_0x800, + grouped_territory_selectors_0x80f: control.grouped_territory_selectors_0x80f.clone(), + } +} + +pub(crate) fn runtime_packed_event_text_band_summary_from_smp( + band: &SmpLoadedPackedEventTextBandSummary, +) -> RuntimePackedEventTextBandSummary { + RuntimePackedEventTextBandSummary { + label: band.label.clone(), + packed_len: band.packed_len, + present: band.present, + preview: band.preview.clone(), + } +} + +pub(crate) fn runtime_packed_event_condition_row_summary_from_smp( + row: &crate::inspect::smp::events::SmpLoadedPackedEventConditionRowSummary, + cargo_catalog: &[RuntimeCargoCatalogEntry], +) -> RuntimePackedEventConditionRowSummary { + let cargo_entry = row + .recovered_cargo_slot + .and_then(|slot| cargo_catalog.iter().find(|entry| entry.slot_id == slot)); + RuntimePackedEventConditionRowSummary { + row_index: row.row_index, + raw_condition_id: row.raw_condition_id, + subtype: row.subtype, + flag_bytes: row.flag_bytes.clone(), + candidate_name: row.candidate_name.clone(), + comparator: row.comparator.clone(), + metric: row.metric.clone(), + semantic_family: row.semantic_family.clone(), + semantic_preview: row.semantic_preview.clone(), + requires_candidate_name_binding: row.requires_candidate_name_binding, + recovered_cargo_slot: row.recovered_cargo_slot, + recovered_cargo_class: cargo_entry + .map(|entry| format!("{:?}", entry.cargo_class).to_ascii_lowercase()) + .map(|value| value.replace("farmmine", "farm_mine")) + .or_else(|| row.recovered_cargo_class.clone()), + recovered_cargo_label: cargo_entry + .map(|entry| entry.label.clone()) + .or_else(|| row.recovered_cargo_slot.map(default_cargo_slot_label)), + recovered_cargo_supplied_token_stem: cargo_entry + .and_then(|entry| entry.supplied_token_stem.clone()), + recovered_cargo_demanded_token_stem: cargo_entry + .and_then(|entry| entry.demanded_token_stem.clone()), + notes: row.notes.clone(), + } +} + +pub(crate) fn runtime_packed_event_grouped_effect_row_summary_from_smp( + row: &crate::inspect::smp::events::SmpLoadedPackedEventGroupedEffectRowSummary, + cargo_catalog: &[RuntimeCargoCatalogEntry], +) -> RuntimePackedEventGroupedEffectRowSummary { + let cargo_entry = row + .recovered_cargo_slot + .and_then(|slot| cargo_catalog.iter().find(|entry| entry.slot_id == slot)); + RuntimePackedEventGroupedEffectRowSummary { + group_index: row.group_index, + row_index: row.row_index, + descriptor_id: row.descriptor_id, + descriptor_label: row.descriptor_label.clone(), + target_mask_bits: row.target_mask_bits, + parameter_family: row.parameter_family.clone(), + grouped_target_subject: row.grouped_target_subject.clone(), + grouped_target_scope: row.grouped_target_scope.clone(), + opcode: row.opcode, + raw_scalar_value: row.raw_scalar_value, + value_byte_0x09: row.value_byte_0x09, + value_dword_0x0d: row.value_dword_0x0d, + value_byte_0x11: row.value_byte_0x11, + value_byte_0x12: row.value_byte_0x12, + value_word_0x14: row.value_word_0x14, + value_word_0x16: row.value_word_0x16, + row_shape: row.row_shape.clone(), + semantic_family: row.semantic_family.clone(), + semantic_preview: row.semantic_preview.clone(), + recovered_cargo_slot: row.recovered_cargo_slot, + recovered_cargo_class: cargo_entry + .map(|entry| format!("{:?}", entry.cargo_class).to_ascii_lowercase()) + .map(|value| value.replace("farmmine", "farm_mine")) + .or_else(|| row.recovered_cargo_class.clone()), + recovered_cargo_label: cargo_entry + .map(|entry| entry.label.clone()) + .or_else(|| row.recovered_cargo_label.clone()) + .or_else(|| row.recovered_cargo_slot.map(default_cargo_slot_label)), + recovered_cargo_supplied_token_stem: cargo_entry + .and_then(|entry| entry.supplied_token_stem.clone()), + recovered_cargo_demanded_token_stem: cargo_entry + .and_then(|entry| entry.demanded_token_stem.clone()), + recovered_locomotive_id: row.recovered_locomotive_id, + locomotive_name: row.locomotive_name.clone(), + notes: row.notes.clone(), + } +} + +pub(crate) fn default_cargo_slot_label(slot: u32) -> String { + format!("Cargo Production Slot {slot}") +} diff --git a/crates/rrt-runtime/src/documents/mod.rs b/crates/rrt-runtime/src/documents/mod.rs new file mode 100644 index 0000000..61e0781 --- /dev/null +++ b/crates/rrt-runtime/src/documents/mod.rs @@ -0,0 +1,17 @@ +pub use crate::persistence::{ + RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION, + save_runtime_snapshot_document, +}; + +mod blockers; +mod io; +mod lowering; +mod model; +mod project; + +pub use io::*; +pub use model::*; +pub use project::*; + +#[cfg(test)] +mod tests; diff --git a/crates/rrt-runtime/src/documents/model.rs b/crates/rrt-runtime/src/documents/model.rs new file mode 100644 index 0000000..6becd8e --- /dev/null +++ b/crates/rrt-runtime/src/documents/model.rs @@ -0,0 +1,248 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde::{Deserialize, Serialize}; + +use crate::event::packed::RuntimePackedEventCollectionSummary; +use crate::event::records::RuntimeEventRecord; +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::{ + RuntimeCargoCatalogEntry, RuntimeChairmanProfile, RuntimeCompany, RuntimeCompanyControllerKind, + RuntimeCompanyMarketState, RuntimeLocomotiveCatalogEntry, RuntimeSaveProfileState, + RuntimeState, RuntimeWorldRestoreState, +}; +use crate::state::{RuntimeNearCityAcquisitionRegion, RuntimeNearCityAcquisitionSite}; + +pub const STATE_INPUT_DOCUMENT_FORMAT_VERSION: u32 = 1; +pub const SAVE_SLICE_DOCUMENT_FORMAT_VERSION: u32 = 1; +pub const OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION: u32 = 1; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct RuntimeStateInputSource { + #[serde(default)] + pub description: Option, + #[serde(default)] + pub source_binary: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeStateInputDocument { + pub format_version: u32, + pub input_id: String, + #[serde(default)] + pub source: RuntimeStateInputSource, + pub state: RuntimeState, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct RuntimeSaveSliceDocumentSource { + #[serde(default)] + pub description: Option, + #[serde(default)] + pub original_save_filename: Option, + #[serde(default)] + pub original_save_sha256: Option, + #[serde(default)] + pub notes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RuntimeSaveSliceDocument { + pub format_version: u32, + pub save_slice_id: String, + #[serde(default)] + pub source: RuntimeSaveSliceDocumentSource, + pub save_slice: SmpLoadedSaveSlice, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct RuntimeOverlayImportDocumentSource { + #[serde(default)] + pub description: Option, + #[serde(default)] + pub notes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeOverlayImportDocument { + pub format_version: u32, + pub import_id: String, + #[serde(default)] + pub source: RuntimeOverlayImportDocumentSource, + pub base_snapshot_path: String, + pub save_slice_path: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeStateInput { + pub input_id: String, + pub description: Option, + pub state: RuntimeState, +} + +#[derive(Debug)] +pub(super) struct SaveSliceProjection { + pub(super) world_flags: BTreeMap, + pub(super) save_profile: RuntimeSaveProfileState, + pub(super) world_restore: RuntimeWorldRestoreState, + pub(super) metadata: BTreeMap, + pub(super) packed_event_collection: Option, + pub(super) event_runtime_records: Vec, + pub(super) companies: Vec, + pub(super) has_company_projection: bool, + pub(super) has_company_selection_override: bool, + pub(super) selected_company_id: Option, + pub(super) company_market_state: BTreeMap, + pub(super) company_periodic_side_latch_state: + BTreeMap, + pub(super) has_company_market_projection: bool, + pub(super) world_issue_opinion_base_terms_raw_i32: Vec, + pub(super) chairman_profiles: Vec, + pub(super) has_chairman_projection: bool, + pub(super) has_chairman_selection_override: bool, + pub(super) selected_chairman_profile_id: Option, + pub(super) chairman_issue_opinion_terms_raw_i32: BTreeMap>, + pub(super) chairman_personality_raw_u8: BTreeMap, + pub(super) near_city_acquisition_regions: Vec, + pub(super) near_city_acquisition_sites: Vec, + pub(super) has_near_city_projection: bool, + pub(super) candidate_availability: BTreeMap, + pub(super) named_locomotive_availability: BTreeMap, + pub(super) locomotive_catalog: Option>, + pub(super) cargo_catalog: Option>, + pub(super) named_locomotive_cost: BTreeMap, + pub(super) all_cargo_price_override: Option, + pub(super) named_cargo_price_overrides: BTreeMap, + pub(super) all_cargo_production_override: Option, + pub(super) factory_cargo_production_override: Option, + pub(super) farm_mine_cargo_production_override: Option, + pub(super) named_cargo_production_overrides: BTreeMap, + pub(super) cargo_production_overrides: BTreeMap, + pub(super) world_scalar_overrides: BTreeMap, + pub(super) special_conditions: BTreeMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum SaveSliceProjectionMode { + Standalone, + Overlay, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct RuntimeImportContext { + pub(super) known_company_ids: BTreeSet, + pub(super) selected_company_id: Option, + pub(super) has_complete_company_controller_context: bool, + pub(super) known_player_ids: BTreeSet, + pub(super) selected_player_id: Option, + pub(super) has_complete_player_controller_context: bool, + pub(super) known_chairman_profile_ids: BTreeSet, + pub(super) selected_chairman_profile_id: Option, + pub(super) known_territory_ids: BTreeSet, + pub(super) has_territory_context: bool, + pub(super) territory_name_to_id: BTreeMap, + pub(super) has_train_context: bool, + pub(super) has_train_territory_context: bool, + pub(super) locomotive_catalog_names_by_id: BTreeMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum RuntimeImportBlocker { + MissingCompanyContext, + MissingSelectionContext, + MissingCompanyRoleContext, + MissingPlayerContext, + MissingPlayerSelectionContext, + MissingPlayerRoleContext, + MissingChairmanContext, + ChairmanTargetScope, + MissingConditionContext, + MissingPlayerConditionContext, + CompanyConditionScopeDisabled, + MissingTerritoryContext, + NamedTerritoryBinding, + UnmappedOrdinaryCondition, + UnmappedWorldCondition, + EvidenceBlockedDescriptor, + MissingTrainContext, + MissingTrainTerritoryContext, + MissingLocomotiveCatalogContext, +} + +impl RuntimeImportContext { + pub(super) fn standalone() -> Self { + Self { + known_company_ids: BTreeSet::new(), + selected_company_id: None, + has_complete_company_controller_context: false, + known_player_ids: BTreeSet::new(), + selected_player_id: None, + has_complete_player_controller_context: false, + known_chairman_profile_ids: BTreeSet::new(), + selected_chairman_profile_id: None, + known_territory_ids: BTreeSet::new(), + has_territory_context: false, + territory_name_to_id: BTreeMap::new(), + has_train_context: false, + has_train_territory_context: false, + locomotive_catalog_names_by_id: BTreeMap::new(), + } + } + + pub(super) fn from_runtime_state(state: &RuntimeState) -> Self { + Self { + known_company_ids: state + .companies + .iter() + .map(|company| company.company_id) + .collect(), + selected_company_id: state.selected_company_id, + has_complete_company_controller_context: !state.companies.is_empty() + && state.companies.iter().all(|company| { + company.controller_kind != RuntimeCompanyControllerKind::Unknown + }), + known_player_ids: state + .players + .iter() + .map(|player| player.player_id) + .collect(), + selected_player_id: state.selected_player_id, + has_complete_player_controller_context: !state.players.is_empty() + && state + .players + .iter() + .all(|player| player.controller_kind != RuntimeCompanyControllerKind::Unknown), + known_chairman_profile_ids: state + .chairman_profiles + .iter() + .map(|profile| profile.profile_id) + .collect(), + selected_chairman_profile_id: state.selected_chairman_profile_id, + known_territory_ids: state + .territories + .iter() + .map(|territory| territory.territory_id) + .collect(), + has_territory_context: !state.territories.is_empty(), + territory_name_to_id: state + .territories + .iter() + .filter_map(|territory| { + territory + .name + .as_ref() + .map(|name| (name.clone(), territory.territory_id)) + }) + .collect(), + has_train_context: !state.trains.is_empty(), + has_train_territory_context: state + .trains + .iter() + .any(|train| train.territory_id.is_some()), + locomotive_catalog_names_by_id: state + .locomotive_catalog + .iter() + .map(|entry| (entry.locomotive_id, entry.name.clone())) + .collect(), + } + } +} diff --git a/crates/rrt-runtime/src/documents/project/actors.rs b/crates/rrt-runtime/src/documents/project/actors.rs new file mode 100644 index 0000000..60c8b4e --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/actors.rs @@ -0,0 +1,273 @@ +use std::collections::BTreeMap; + +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::{ + RuntimeChairmanProfile, RuntimeCompany, RuntimeCompanyMarketState, + RuntimeCompanyPeriodicSideLatchState, +}; + +pub(super) struct CompanyProjection { + pub(super) companies: Vec, + pub(super) has_company_projection: bool, + pub(super) has_company_selection_override: bool, + pub(super) selected_company_id: Option, + pub(super) company_market_state: BTreeMap, + pub(super) company_periodic_side_latch_state: + BTreeMap, + pub(super) world_issue_opinion_base_terms_raw_i32: Vec, + pub(super) has_company_market_projection: bool, +} + +pub(super) struct ChairmanProjection { + pub(super) chairman_profiles: Vec, + pub(super) has_chairman_projection: bool, + pub(super) has_chairman_selection_override: bool, + pub(super) selected_chairman_profile_id: Option, + pub(super) chairman_issue_opinion_terms_raw_i32: BTreeMap>, + pub(super) chairman_personality_raw_u8: BTreeMap, +} + +pub(super) fn project_company_projection( + save_slice: &SmpLoadedSaveSlice, + metadata: &mut BTreeMap, +) -> CompanyProjection { + let ( + companies, + has_company_projection, + has_company_selection_override, + selected_company_id, + company_market_state, + company_periodic_side_latch_state, + world_issue_opinion_base_terms_raw_i32, + has_company_market_projection, + ) = if let Some(roster) = &save_slice.company_roster { + metadata.insert( + "save_slice.company_roster_source_kind".to_string(), + roster.source_kind.clone(), + ); + metadata.insert( + "save_slice.company_roster_semantic_family".to_string(), + roster.semantic_family.clone(), + ); + metadata.insert( + "save_slice.company_roster_entry_count".to_string(), + roster.observed_entry_count.to_string(), + ); + let market_state = roster + .entries + .iter() + .filter_map(|entry| { + entry + .market_state + .as_ref() + .map(|state| (entry.company_id, state.clone())) + }) + .collect::>(); + let periodic_side_latch_state = roster + .entries + .iter() + .map(|entry| { + ( + entry.company_id, + RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: entry + .preferred_locomotive_engine_type_raw_u8, + city_connection_latch: entry + .market_state + .as_ref() + .map(|state| state.city_connection_latch) + .unwrap_or(false), + linked_transit_latch: entry + .market_state + .as_ref() + .map(|state| state.linked_transit_latch) + .unwrap_or(false), + }, + ) + }) + .collect::>(); + metadata.insert( + "save_slice.company_market_state_owner_count".to_string(), + market_state.len().to_string(), + ); + if let Some(selected_company_id) = roster.selected_company_id { + metadata.insert( + "save_slice.selected_company_id".to_string(), + selected_company_id.to_string(), + ); + } + if roster.entries.is_empty() { + ( + Vec::new(), + false, + roster.selected_company_id.is_some(), + roster.selected_company_id, + BTreeMap::new(), + periodic_side_latch_state, + save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.issue_opinion_base_terms_raw_i32.clone()) + .unwrap_or_default(), + false, + ) + } else { + ( + roster + .entries + .iter() + .map(|entry| RuntimeCompany { + company_id: entry.company_id, + current_cash: entry.current_cash, + debt: entry.debt, + credit_rating_score: entry.credit_rating_score, + prime_rate: entry.prime_rate, + active: entry.active, + available_track_laying_capacity: entry.available_track_laying_capacity, + controller_kind: entry.controller_kind, + linked_chairman_profile_id: entry.linked_chairman_profile_id, + book_value_per_share: entry.book_value_per_share, + investor_confidence: entry.investor_confidence, + management_attitude: entry.management_attitude, + takeover_cooldown_year: entry.takeover_cooldown_year, + merger_cooldown_year: entry.merger_cooldown_year, + track_piece_counts: entry.track_piece_counts, + }) + .collect::>(), + true, + roster.selected_company_id.is_some(), + roster.selected_company_id, + market_state, + periodic_side_latch_state, + save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.issue_opinion_base_terms_raw_i32.clone()) + .unwrap_or_default(), + true, + ) + } + } else { + ( + Vec::new(), + false, + false, + None, + BTreeMap::new(), + BTreeMap::new(), + save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.issue_opinion_base_terms_raw_i32.clone()) + .unwrap_or_default(), + false, + ) + }; + + CompanyProjection { + companies, + has_company_projection, + has_company_selection_override, + selected_company_id, + company_market_state, + company_periodic_side_latch_state, + world_issue_opinion_base_terms_raw_i32, + has_company_market_projection, + } +} + +pub(super) fn project_chairman_projection( + save_slice: &SmpLoadedSaveSlice, + metadata: &mut BTreeMap, +) -> ChairmanProjection { + let ( + chairman_profiles, + has_chairman_projection, + has_chairman_selection_override, + selected_chairman_profile_id, + chairman_issue_opinion_terms_raw_i32, + chairman_personality_raw_u8, + ) = if let Some(table) = &save_slice.chairman_profile_table { + metadata.insert( + "save_slice.chairman_profile_table_source_kind".to_string(), + table.source_kind.clone(), + ); + metadata.insert( + "save_slice.chairman_profile_table_semantic_family".to_string(), + table.semantic_family.clone(), + ); + metadata.insert( + "save_slice.chairman_profile_table_entry_count".to_string(), + table.observed_entry_count.to_string(), + ); + if let Some(selected_chairman_profile_id) = table.selected_chairman_profile_id { + metadata.insert( + "save_slice.selected_chairman_profile_id".to_string(), + selected_chairman_profile_id.to_string(), + ); + } + if table.entries.is_empty() { + ( + Vec::new(), + false, + table.selected_chairman_profile_id.is_some(), + table.selected_chairman_profile_id, + BTreeMap::new(), + BTreeMap::new(), + ) + } else { + ( + table + .entries + .iter() + .map(|entry| RuntimeChairmanProfile { + profile_id: entry.profile_id, + name: entry.name.clone(), + active: entry.active, + current_cash: entry.current_cash, + linked_company_id: entry.linked_company_id, + company_holdings: entry.company_holdings.clone(), + holdings_value_total: entry.holdings_value_total, + net_worth_total: entry.net_worth_total, + purchasing_power_total: entry.purchasing_power_total, + }) + .collect::>(), + true, + table.selected_chairman_profile_id.is_some(), + table.selected_chairman_profile_id, + table + .entries + .iter() + .map(|entry| (entry.profile_id, entry.issue_opinion_terms_raw_i32.clone())) + .collect::>(), + table + .entries + .iter() + .filter_map(|entry| { + entry + .personality_byte_0x291 + .map(|value| (entry.profile_id, value)) + }) + .collect::>(), + ) + } + } else { + ( + Vec::new(), + false, + false, + None, + BTreeMap::new(), + BTreeMap::new(), + ) + }; + + ChairmanProjection { + chairman_profiles, + has_chairman_projection, + has_chairman_selection_override, + selected_chairman_profile_id, + chairman_issue_opinion_terms_raw_i32, + chairman_personality_raw_u8, + } +} diff --git a/crates/rrt-runtime/src/documents/project/entrypoints.rs b/crates/rrt-runtime/src/documents/project/entrypoints.rs new file mode 100644 index 0000000..43ebeab --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/entrypoints.rs @@ -0,0 +1,62 @@ +use super::super::{RuntimeImportContext, RuntimeStateInput, SaveSliceProjectionMode}; +use super::{ + build_overlay_runtime_state, build_standalone_runtime_state, project_save_slice_components, +}; +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::RuntimeState; + +pub fn build_runtime_state_input_from_save_slice( + save_slice: &SmpLoadedSaveSlice, + input_id: &str, + description: Option, +) -> Result { + if input_id.trim().is_empty() { + return Err("input_id must not be empty".to_string()); + } + let projection = project_save_slice_components( + save_slice, + &RuntimeImportContext::standalone(), + SaveSliceProjectionMode::Standalone, + )?; + + let mut state = build_standalone_runtime_state(projection); + state.refresh_derived_world_state(); + state.refresh_derived_market_state(); + state.validate()?; + + Ok(RuntimeStateInput { + input_id: input_id.to_string(), + description, + state, + }) +} + +pub fn build_runtime_state_input_from_overlay( + base_state: &RuntimeState, + save_slice: &SmpLoadedSaveSlice, + input_id: &str, + description: Option, +) -> Result { + if input_id.trim().is_empty() { + return Err("input_id must not be empty".to_string()); + } + base_state.validate()?; + + let runtime_context = RuntimeImportContext::from_runtime_state(base_state); + let projection = project_save_slice_components( + save_slice, + &runtime_context, + SaveSliceProjectionMode::Overlay, + )?; + + let mut state = build_overlay_runtime_state(base_state, projection); + state.refresh_derived_world_state(); + state.refresh_derived_market_state(); + state.validate()?; + + Ok(RuntimeStateInput { + input_id: input_id.to_string(), + description, + state, + }) +} diff --git a/crates/rrt-runtime/src/documents/project/events.rs b/crates/rrt-runtime/src/documents/project/events.rs new file mode 100644 index 0000000..d7bbc49 --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/events.rs @@ -0,0 +1,96 @@ +use std::collections::BTreeMap; + +use super::super::RuntimeImportContext; +use super::super::lowering::project_packed_event_collection; +use super::{ChairmanProjection, CompanyProjection}; +use crate::event::packed::RuntimePackedEventCollectionSummary; +use crate::event::records::RuntimeEventRecord; +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::{ + RuntimeCargoCatalogEntry, RuntimeCompanyControllerKind, RuntimeLocomotiveCatalogEntry, +}; + +pub(super) struct PackedEventProjection { + pub(super) packed_event_collection: Option, + pub(super) event_runtime_records: Vec, +} + +pub(super) fn project_event_runtime( + save_slice: &SmpLoadedSaveSlice, + runtime_context: &RuntimeImportContext, + company_projection: &CompanyProjection, + chairman_projection: &ChairmanProjection, + locomotive_catalog: Option<&[RuntimeLocomotiveCatalogEntry]>, + cargo_catalog: Option<&[RuntimeCargoCatalogEntry]>, + metadata: &mut BTreeMap, +) -> Result { + let companies = &company_projection.companies; + let has_company_projection = company_projection.has_company_projection; + let has_company_selection_override = company_projection.has_company_selection_override; + let selected_company_id = company_projection.selected_company_id; + let chairman_profiles = &chairman_projection.chairman_profiles; + let has_chairman_projection = chairman_projection.has_chairman_projection; + let has_chairman_selection_override = chairman_projection.has_chairman_selection_override; + let selected_chairman_profile_id = chairman_projection.selected_chairman_profile_id; + let locomotive_catalog = locomotive_catalog.map(|value| value.to_vec()); + let mut event_import_context = runtime_context.clone(); + if has_company_projection { + event_import_context.known_company_ids = + companies.iter().map(|company| company.company_id).collect(); + event_import_context.selected_company_id = selected_company_id; + event_import_context.has_complete_company_controller_context = !companies.is_empty() + && companies + .iter() + .all(|company| company.controller_kind != RuntimeCompanyControllerKind::Unknown); + } else if has_company_selection_override { + event_import_context.selected_company_id = selected_company_id; + } + if has_chairman_projection { + event_import_context.known_chairman_profile_ids = chairman_profiles + .iter() + .map(|profile| profile.profile_id) + .collect(); + event_import_context.selected_chairman_profile_id = selected_chairman_profile_id; + } else if has_chairman_selection_override { + event_import_context.selected_chairman_profile_id = selected_chairman_profile_id; + } + if let Some(catalog) = &locomotive_catalog { + event_import_context.locomotive_catalog_names_by_id = catalog + .iter() + .map(|entry| (entry.locomotive_id, entry.name.clone())) + .collect(); + } + + let (packed_event_collection, event_runtime_records) = project_packed_event_collection( + save_slice, + &event_import_context, + cargo_catalog.as_deref().unwrap_or(&[]), + )?; + if let Some(summary) = &save_slice.event_runtime_collection { + metadata.insert( + "save_slice.event_runtime_collection_source_kind".to_string(), + summary.source_kind.clone(), + ); + metadata.insert( + "save_slice.event_runtime_collection_version_hex".to_string(), + summary.packed_state_version_hex.clone(), + ); + metadata.insert( + "save_slice.event_runtime_collection_record_count".to_string(), + summary.live_record_count.to_string(), + ); + metadata.insert( + "save_slice.event_runtime_collection_decoded_record_count".to_string(), + summary.decoded_record_count.to_string(), + ); + metadata.insert( + "save_slice.event_runtime_collection_imported_runtime_record_count".to_string(), + event_runtime_records.len().to_string(), + ); + } + + Ok(PackedEventProjection { + packed_event_collection, + event_runtime_records, + }) +} diff --git a/crates/rrt-runtime/src/documents/project/metadata.rs b/crates/rrt-runtime/src/documents/project/metadata.rs new file mode 100644 index 0000000..4bcc936 --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/metadata.rs @@ -0,0 +1,413 @@ +use std::collections::BTreeMap; + +use super::super::SaveSliceProjectionMode; +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; + +pub(super) struct ProjectionMetadata { + pub(super) world_flags: BTreeMap, + pub(super) metadata: BTreeMap, +} + +pub(super) fn build_projection_metadata( + save_slice: &SmpLoadedSaveSlice, + mode: SaveSliceProjectionMode, +) -> ProjectionMetadata { + let mut world_flags: BTreeMap = BTreeMap::new(); + world_flags.insert( + "save_slice.profile_present".to_string(), + save_slice.profile.is_some(), + ); + world_flags.insert( + "save_slice.candidate_availability_present".to_string(), + save_slice.candidate_availability_table.is_some(), + ); + world_flags.insert( + "save_slice.special_conditions_present".to_string(), + save_slice.special_conditions_table.is_some(), + ); + world_flags.insert( + "save_slice.named_locomotive_availability_present".to_string(), + save_slice.named_locomotive_availability_table.is_some(), + ); + world_flags.insert( + "save_slice.locomotive_catalog_present".to_string(), + save_slice.locomotive_catalog.is_some() + || save_slice.named_locomotive_availability_table.is_some(), + ); + world_flags.insert( + "save_slice.cargo_catalog_present".to_string(), + save_slice.cargo_catalog.is_some(), + ); + world_flags.insert( + "save_slice.world_issue_37_state_present".to_string(), + save_slice.world_issue_37_state.is_some(), + ); + world_flags.insert( + "save_slice.world_economic_tuning_state_present".to_string(), + save_slice.world_economic_tuning_state.is_some(), + ); + world_flags.insert( + "save_slice.world_finance_neighborhood_state_present".to_string(), + save_slice.world_finance_neighborhood_state.is_some(), + ); + world_flags.insert( + "save_slice.event_runtime_collection_present".to_string(), + save_slice.event_runtime_collection.is_some(), + ); + world_flags.insert( + "save_slice.mechanism_confidence_grounded".to_string(), + save_slice.mechanism_confidence == "grounded", + ); + if let Some(profile) = &save_slice.profile { + world_flags.insert( + "save_slice.profile_byte_0x82_nonzero".to_string(), + profile.profile_byte_0x82 != 0, + ); + world_flags.insert( + "save_slice.profile_byte_0x97_nonzero".to_string(), + profile.profile_byte_0x97 != 0, + ); + world_flags.insert( + "save_slice.profile_byte_0xc5_nonzero".to_string(), + profile.profile_byte_0xc5 != 0, + ); + } + + let mut metadata: BTreeMap = BTreeMap::new(); + metadata.insert( + "save_slice.import_projection".to_string(), + match mode { + SaveSliceProjectionMode::Standalone => "partial-runtime-restore-v1", + SaveSliceProjectionMode::Overlay => "overlay-runtime-restore-v1", + } + .to_string(), + ); + metadata.insert( + "save_slice.calendar_source".to_string(), + match mode { + SaveSliceProjectionMode::Standalone => "default-1830-placeholder", + SaveSliceProjectionMode::Overlay => "base-snapshot-preserved", + } + .to_string(), + ); + metadata.insert( + "save_slice.selected_year_seed_tuple_source".to_string(), + "raw-lane-via-0x51d3f0".to_string(), + ); + metadata.insert( + "save_slice.selected_year_absolute_counter_source".to_string(), + if save_slice.world_finance_neighborhood_state.is_some() { + "save-direct-world-block-absolute-counter".to_string() + } else { + "mode-adjusted-lane-via-0x51d390-0x409e80".to_string() + }, + ); + metadata.insert( + "save_slice.selected_year_absolute_counter_reconstructible_from_save".to_string(), + save_slice + .world_finance_neighborhood_state + .is_some() + .to_string(), + ); + metadata.insert( + "save_slice.disable_cargo_economy_special_condition_slot".to_string(), + "30".to_string(), + ); + metadata.insert( + "save_slice.disable_cargo_economy_special_condition_reconstructible_from_save".to_string(), + "true".to_string(), + ); + metadata.insert( + "save_slice.disable_cargo_economy_special_condition_write_side_grounded".to_string(), + "true".to_string(), + ); + metadata.insert( + "save_slice.selected_year_absolute_counter_adjustment_context".to_string(), + if save_slice.world_finance_neighborhood_state.is_some() { + "save-direct-world-block-0x32c8".to_string() + } else { + "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30" + .to_string() + }, + ); + metadata.insert( + "save_slice.mechanism_family".to_string(), + save_slice.mechanism_family.clone(), + ); + metadata.insert( + "save_slice.mechanism_confidence".to_string(), + save_slice.mechanism_confidence.clone(), + ); + if let Some(family) = &save_slice.container_profile_family { + metadata.insert( + "save_slice.container_profile_family".to_string(), + family.clone(), + ); + } + if let Some(family) = &save_slice.trailer_family { + metadata.insert("save_slice.trailer_family".to_string(), family.clone()); + } + if let Some(family) = &save_slice.bridge_family { + metadata.insert("save_slice.bridge_family".to_string(), family.clone()); + } + if let Some(issue_state) = &save_slice.world_issue_37_state { + metadata.insert( + "save_slice.world_issue_37_source_kind".to_string(), + issue_state.source_kind.clone(), + ); + metadata.insert( + "save_slice.world_issue_37_semantic_family".to_string(), + issue_state.semantic_family.clone(), + ); + metadata.insert( + "save_slice.world_issue_37_value".to_string(), + issue_state.issue_value.to_string(), + ); + metadata.insert( + "save_slice.world_issue_37_value_hex".to_string(), + issue_state.issue_value_hex.clone(), + ); + metadata.insert( + "save_slice.world_issue_38_value".to_string(), + issue_state.issue_38_value.to_string(), + ); + metadata.insert( + "save_slice.world_issue_38_value_hex".to_string(), + issue_state.issue_38_value_hex.clone(), + ); + metadata.insert( + "save_slice.world_issue_39_value".to_string(), + issue_state.issue_39_value.to_string(), + ); + metadata.insert( + "save_slice.world_issue_39_value_hex".to_string(), + issue_state.issue_39_value_hex.clone(), + ); + metadata.insert( + "save_slice.world_issue_3a_value".to_string(), + issue_state.issue_3a_value.to_string(), + ); + metadata.insert( + "save_slice.world_issue_3a_value_hex".to_string(), + issue_state.issue_3a_value_hex.clone(), + ); + metadata.insert( + "save_slice.world_issue_37_multiplier_raw_hex".to_string(), + issue_state.multiplier_raw_hex.clone(), + ); + metadata.insert( + "save_slice.world_issue_37_multiplier_value_f32".to_string(), + issue_state.multiplier_value_f32_text.clone(), + ); + } + if let Some(tuning_state) = &save_slice.world_economic_tuning_state { + metadata.insert( + "save_slice.world_economic_tuning_source_kind".to_string(), + tuning_state.source_kind.clone(), + ); + metadata.insert( + "save_slice.world_economic_tuning_semantic_family".to_string(), + tuning_state.semantic_family.clone(), + ); + metadata.insert( + "save_slice.world_economic_tuning_mirror_raw_hex".to_string(), + tuning_state.mirror_raw_hex.clone(), + ); + metadata.insert( + "save_slice.world_economic_tuning_mirror_value_f32".to_string(), + tuning_state.mirror_value_f32_text.clone(), + ); + metadata.insert( + "save_slice.world_economic_tuning_lane_count".to_string(), + tuning_state.lane_raw_u32.len().to_string(), + ); + for (index, value) in tuning_state.lane_value_f32_text.iter().enumerate() { + metadata.insert( + format!("save_slice.world_economic_tuning_lane_{index}_f32"), + value.clone(), + ); + } + } + if let Some(finance_state) = &save_slice.world_finance_neighborhood_state { + metadata.insert( + "save_slice.world_finance_neighborhood_source_kind".to_string(), + finance_state.source_kind.clone(), + ); + metadata.insert( + "save_slice.world_finance_neighborhood_semantic_family".to_string(), + finance_state.semantic_family.clone(), + ); + metadata.insert( + "save_slice.world_finance_neighborhood_candidate_count".to_string(), + finance_state.raw_u32.len().to_string(), + ); + for (index, label) in finance_state.labels.iter().enumerate() { + metadata.insert( + format!("save_slice.world_finance_neighborhood_label_{index}"), + label.clone(), + ); + } + } + + ProjectionMetadata { + world_flags, + metadata, + } +} + +pub(super) fn append_projection_collection_metadata( + save_slice: &SmpLoadedSaveSlice, + metadata: &mut BTreeMap, +) { + if let Some(collection) = &save_slice.placed_structure_collection { + metadata.insert( + "save_slice.placed_structure_collection_source_kind".to_string(), + collection.source_kind.clone(), + ); + metadata.insert( + "save_slice.placed_structure_collection_semantic_family".to_string(), + collection.semantic_family.clone(), + ); + metadata.insert( + "save_slice.placed_structure_collection_entry_count".to_string(), + collection.observed_entry_count.to_string(), + ); + metadata.insert( + "save_slice.placed_structure_collection_farm_growth_stage_count".to_string(), + collection + .entries + .iter() + .filter(|entry| entry.farm_growth_stage_index.is_some()) + .count() + .to_string(), + ); + metadata.insert( + "save_slice.placed_structure_collection_nondefault_status_count".to_string(), + collection + .entries + .iter() + .filter(|entry| entry.profile_status_kind != "unset") + .count() + .to_string(), + ); + } + if let Some(summary) = &save_slice.placed_structure_dynamic_side_buffer_summary { + metadata.insert( + "save_slice.placed_structure_dynamic_side_buffer_source_kind".to_string(), + summary.source_kind.clone(), + ); + metadata.insert( + "save_slice.placed_structure_dynamic_side_buffer_semantic_family".to_string(), + summary.semantic_family.clone(), + ); + metadata.insert( + "save_slice.placed_structure_dynamic_side_buffer_entry_count".to_string(), + summary.observed_entry_count.to_string(), + ); + metadata.insert( + "save_slice.placed_structure_dynamic_side_buffer_name_pair_count".to_string(), + summary.unique_embedded_name_pair_count.to_string(), + ); + metadata.insert( + "save_slice.placed_structure_dynamic_side_buffer_triplet_overlap_count".to_string(), + summary.triplet_alignment_overlap_count.to_string(), + ); + metadata.insert( + "save_slice.placed_structure_dynamic_side_buffer_side_buffer_only_name_pair_count" + .to_string(), + summary + .triplet_alignment_side_buffer_only_name_pair_count + .to_string(), + ); + } + if let Some(collection) = &save_slice.region_collection { + metadata.insert( + "save_slice.region_collection_source_kind".to_string(), + collection.source_kind.clone(), + ); + metadata.insert( + "save_slice.region_collection_semantic_family".to_string(), + collection.semantic_family.clone(), + ); + metadata.insert( + "save_slice.region_collection_entry_count".to_string(), + collection.observed_entry_count.to_string(), + ); + metadata.insert( + "save_slice.region_collection_profile_entry_count".to_string(), + collection + .entries + .iter() + .map(|entry| { + entry + .profile_collection + .as_ref() + .map(|collection| collection.entries.len()) + .unwrap_or_default() + }) + .sum::() + .to_string(), + ); + metadata.insert( + "save_slice.region_collection_nonzero_prefix_count".to_string(), + collection + .entries + .iter() + .filter(|entry| entry.pre_name_prefix_len != 0) + .count() + .to_string(), + ); + metadata.insert( + "save_slice.region_collection_nonzero_reserved_policy_count".to_string(), + collection + .entries + .iter() + .filter(|entry| entry.policy_reserved_dwords.iter().any(|raw| *raw != 0)) + .count() + .to_string(), + ); + } + if let Some(summary) = &save_slice.region_fixed_row_run_summary { + metadata.insert( + "save_slice.region_fixed_row_run_source_kind".to_string(), + summary.source_kind.clone(), + ); + metadata.insert( + "save_slice.region_fixed_row_run_semantic_family".to_string(), + summary.semantic_family.clone(), + ); + metadata.insert( + "save_slice.region_fixed_row_run_candidate_count".to_string(), + summary.candidates.len().to_string(), + ); + metadata.insert( + "save_slice.region_fixed_row_run_target_row_stride_hex".to_string(), + summary.target_row_stride_hex.clone(), + ); + metadata.insert( + "save_slice.region_fixed_row_run_best_rows_offset_hex".to_string(), + summary + .candidates + .first() + .map(|candidate| candidate.rows_offset_hex.clone()) + .unwrap_or_default(), + ); + metadata.insert( + "save_slice.region_fixed_row_run_best_shape_signature".to_string(), + summary + .candidates + .first() + .map(|candidate| candidate.shape_signature.clone()) + .unwrap_or_default(), + ); + } +} + +pub(super) fn append_projection_note_metadata( + save_slice: &SmpLoadedSaveSlice, + metadata: &mut BTreeMap, +) { + for (index, note) in save_slice.notes.iter().enumerate() { + metadata.insert(format!("save_slice.note.{index}"), note.clone()); + } +} diff --git a/crates/rrt-runtime/src/documents/project/mod.rs b/crates/rrt-runtime/src/documents/project/mod.rs new file mode 100644 index 0000000..ee892a3 --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/mod.rs @@ -0,0 +1,22 @@ +mod actors; +mod entrypoints; +mod events; +mod metadata; +mod near_city; +mod profile_world; +mod projection; +mod state_build; + +use actors::{ + ChairmanProjection, CompanyProjection, project_chairman_projection, project_company_projection, +}; +pub use entrypoints::*; +use events::{PackedEventProjection, project_event_runtime}; +use metadata::{ + ProjectionMetadata, append_projection_collection_metadata, append_projection_note_metadata, + build_projection_metadata, +}; +use near_city::project_near_city_projection; +use profile_world::{ProfileWorldProjection, project_profile_world}; +use projection::project_save_slice_components; +use state_build::{build_overlay_runtime_state, build_standalone_runtime_state}; diff --git a/crates/rrt-runtime/src/documents/project/near_city.rs b/crates/rrt-runtime/src/documents/project/near_city.rs new file mode 100644 index 0000000..476d758 --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/near_city.rs @@ -0,0 +1,306 @@ +use std::collections::BTreeMap; + +use super::CompanyProjection; +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::inspect::smp::structures::SmpLoadedPlacedStructureEntry; +use crate::state::{ + RuntimeNearCityAcquisitionRegion, RuntimeNearCityAcquisitionSite, + RuntimeNearCityAcquisitionValueProvenance, +}; + +pub(super) struct NearCityProjection { + pub(super) regions: Vec, + pub(super) sites: Vec, + pub(super) has_projection: bool, +} + +pub(super) fn project_near_city_projection( + save_slice: &SmpLoadedSaveSlice, + company_projection: &CompanyProjection, + metadata: &mut BTreeMap, +) -> NearCityProjection { + let regions = project_near_city_regions(save_slice); + let preferred_company_id = preferred_near_city_company_id(company_projection); + let sites = project_near_city_sites(save_slice, ®ions, preferred_company_id); + if !regions.is_empty() { + metadata.insert( + "save_slice.near_city_region_projection_count".to_string(), + regions.len().to_string(), + ); + } + if !sites.is_empty() { + metadata.insert( + "save_slice.near_city_site_projection_count".to_string(), + sites.len().to_string(), + ); + } + + NearCityProjection { + has_projection: !regions.is_empty() || !sites.is_empty(), + regions, + sites, + } +} + +fn project_near_city_regions( + save_slice: &SmpLoadedSaveSlice, +) -> Vec { + let best_candidate = save_slice + .region_fixed_row_run_summary + .as_ref() + .and_then(|summary| summary.candidates.first()); + save_slice + .region_collection + .as_ref() + .map(|collection| { + collection + .entries + .iter() + .map(|entry| RuntimeNearCityAcquisitionRegion { + region_id: entry.record_index as u32, + name: entry.name.clone(), + profile_names: entry + .profile_collection + .as_ref() + .map(|collection| { + collection + .entries + .iter() + .map(|profile| profile.name.clone()) + .collect() + }) + .unwrap_or_default(), + fixed_row_shape_family_signature: best_candidate + .map(|candidate| candidate.shape_family_signature.clone()), + fixed_row_best_density_lane_relative_offset_hex: best_candidate.and_then( + |candidate| { + candidate + .best_probable_density_lane_relative_offset_hex + .clone() + }, + ), + }) + .collect() + }) + .unwrap_or_default() +} + +fn project_near_city_sites( + save_slice: &SmpLoadedSaveSlice, + regions: &[RuntimeNearCityAcquisitionRegion], + preferred_company_id: Option, +) -> Vec { + let side_buffer = save_slice + .placed_structure_dynamic_side_buffer_summary + .as_ref(); + let fallback_lane_0 = side_buffer + .and_then(|summary| parse_hex_u32(&summary.owner_shared_dword_hex)) + .unwrap_or_default(); + let fallback_lane_1 = save_slice + .region_fixed_row_run_summary + .as_ref() + .and_then(|summary| summary.candidates.first()) + .map(|candidate| candidate.row_stride as u32) + .unwrap_or_default(); + let fallback_lane_2 = save_slice + .region_fixed_row_run_summary + .as_ref() + .and_then(|summary| summary.candidates.first()) + .and_then(|candidate| { + candidate + .best_probable_density_lane_relative_offset_hex + .as_ref() + .and_then(|hex| parse_hex_u32(hex)) + }) + .unwrap_or_default(); + + save_slice + .placed_structure_collection + .as_ref() + .map(|collection| { + collection + .entries + .iter() + .map(|entry| { + let region_id = match_near_city_region_id(entry, regions); + let subtype_label = classify_near_city_site_subtype(entry); + let industrial_like = matches!( + subtype_label.as_str(), + "farm" | "mine" | "industry" | "industry_like" + ); + let matched_pair = side_buffer.and_then(|summary| { + summary.name_pair_summaries.iter().find(|pair| { + normalize_name(&pair.primary_name) + == normalize_name(&entry.primary_name) + && normalize_name(&pair.secondary_name) + == normalize_name(&entry.secondary_name) + }) + }); + let cached_tri_lane_0 = matched_pair + .map(|pair| pair.dominant_prefix_leading_dword) + .unwrap_or(fallback_lane_0); + let cached_tri_lane_1 = matched_pair + .map(|pair| pair.dominant_prefix_trailing_word as u32) + .unwrap_or(fallback_lane_1); + let cached_tri_lane_2 = if fallback_lane_2 != 0 { + fallback_lane_2 + } else { + entry.profile_payload_dword + }; + let cached_tri_lane_provenance = + if matched_pair.is_some() && fallback_lane_2 != 0 { + RuntimeNearCityAcquisitionValueProvenance::Grounded + } else { + RuntimeNearCityAcquisitionValueProvenance::BestEffortGuess + }; + let owner_company_id = if industrial_like { + preferred_company_id + } else { + None + }; + + RuntimeNearCityAcquisitionSite { + site_id: entry.record_index as u32, + primary_name: entry.primary_name.clone(), + secondary_name: entry.secondary_name.clone(), + region_id, + owner_company_id, + preferred_company_id, + owner_company_id_provenance: if owner_company_id.is_some() { + RuntimeNearCityAcquisitionValueProvenance::BestEffortGuess + } else { + RuntimeNearCityAcquisitionValueProvenance::Grounded + }, + self_id: entry.record_index as u32, + self_id_provenance: RuntimeNearCityAcquisitionValueProvenance::Grounded, + candidate_subtype_label: subtype_label, + candidate_subtype_provenance: + RuntimeNearCityAcquisitionValueProvenance::Grounded, + cached_tri_lane_0, + cached_tri_lane_1, + cached_tri_lane_2, + cached_tri_lane_provenance, + nontransport_source_label: if owner_company_id.is_some() { + "best_effort_company_market_latch_projection".to_string() + } else { + "transport_or_unknown_subtype".to_string() + }, + tri_lane_source_label: if matched_pair.is_some() && fallback_lane_2 != 0 { + "matched_side_buffer_name_pair_plus_region_fixed_row_candidate" + .to_string() + } else if matched_pair.is_some() { + "matched_side_buffer_name_pair_plus_profile_payload_fallback" + .to_string() + } else { + "owner_shared_dword_and_region_row_fallback".to_string() + }, + } + }) + .collect() + }) + .unwrap_or_default() +} + +fn preferred_near_city_company_id(company_projection: &CompanyProjection) -> Option { + let selected_company_id = company_projection.selected_company_id; + if selected_company_id.is_some_and(|company_id| { + company_projection + .companies + .iter() + .any(|company| company.company_id == company_id && company.active) + }) { + return selected_company_id; + } + + company_projection + .companies + .iter() + .filter(|company| company.active) + .max_by_key(|company| { + company_projection + .company_market_state + .get(&company.company_id) + .map(|market_state| { + let mut score = 0_u8; + if market_state.linked_transit_latch { + score += 4; + } + if market_state.city_connection_latch { + score += 2; + } + if market_state.linked_transit_route_anchor_entry_id.is_some() { + score += 1; + } + score + }) + .unwrap_or_default() + }) + .map(|company| company.company_id) +} + +fn match_near_city_region_id( + entry: &SmpLoadedPlacedStructureEntry, + regions: &[RuntimeNearCityAcquisitionRegion], +) -> Option { + let primary_name = normalize_name(&entry.primary_name); + let secondary_name = normalize_name(&entry.secondary_name); + regions + .iter() + .find(|region| { + normalize_name(®ion.name) == primary_name + || region + .profile_names + .iter() + .any(|name| normalize_name(name) == primary_name) + || region + .profile_names + .iter() + .any(|name| normalize_name(name) == secondary_name) + }) + .map(|region| region.region_id) + .or_else(|| regions.first().map(|region| region.region_id)) +} + +fn classify_near_city_site_subtype(entry: &SmpLoadedPlacedStructureEntry) -> String { + let primary = normalize_name(&entry.primary_name); + let secondary = normalize_name(&entry.secondary_name); + let combined = format!("{primary} {secondary}"); + if combined.contains("farm") { + "farm".to_string() + } else if combined.contains("mine") { + "mine".to_string() + } else if combined.contains("factory") + || combined.contains("plant") + || combined.contains("mill") + || combined.contains("works") + || combined.contains("auto") + || combined.contains("refinery") + { + "industry".to_string() + } else if combined.contains("station") + || combined.contains("depot") + || combined.contains("port") + || combined.contains("warehouse") + { + "transport".to_string() + } else if entry.profile_status_kind.contains("farm") { + "farm".to_string() + } else if entry.profile_payload_dword == 0x00005dc1 + || entry.profile_companion_byte_u8 == Some(7) + { + "industry_like".to_string() + } else { + "unknown".to_string() + } +} + +fn normalize_name(name: &str) -> String { + name.chars() + .filter(|ch| ch.is_ascii_alphanumeric()) + .flat_map(|ch| ch.to_lowercase()) + .collect() +} + +fn parse_hex_u32(text: &str) -> Option { + u32::from_str_radix(text.trim_start_matches("0x"), 16).ok() +} diff --git a/crates/rrt-runtime/src/documents/project/profile_world/availability.rs b/crates/rrt-runtime/src/documents/project/profile_world/availability.rs new file mode 100644 index 0000000..a942dda --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/profile_world/availability.rs @@ -0,0 +1,93 @@ +use std::collections::BTreeMap; + +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; + +pub(super) struct AvailabilityProjection { + pub(super) candidate_availability: BTreeMap, + pub(super) named_locomotive_availability: BTreeMap, + pub(super) special_conditions: BTreeMap, +} + +pub(super) fn project_availability( + save_slice: &SmpLoadedSaveSlice, + metadata: &mut BTreeMap, +) -> AvailabilityProjection { + let mut candidate_availability = BTreeMap::new(); + if let Some(table) = &save_slice.candidate_availability_table { + metadata.insert( + "save_slice.candidate_table_source_kind".to_string(), + table.source_kind.clone(), + ); + metadata.insert( + "save_slice.candidate_table_semantic_family".to_string(), + table.semantic_family.clone(), + ); + metadata.insert( + "save_slice.candidate_table_entry_count".to_string(), + table.observed_entry_count.to_string(), + ); + metadata.insert( + "save_slice.candidate_table_zero_count".to_string(), + table.zero_availability_count.to_string(), + ); + for entry in &table.entries { + candidate_availability.insert(entry.text.clone(), entry.availability_dword); + } + } + + let mut special_conditions = BTreeMap::new(); + if let Some(table) = &save_slice.special_conditions_table { + metadata.insert( + "save_slice.special_conditions_source_kind".to_string(), + table.source_kind.clone(), + ); + metadata.insert( + "save_slice.special_conditions_table_offset".to_string(), + table.table_offset.to_string(), + ); + metadata.insert( + "save_slice.special_conditions_enabled_visible_count".to_string(), + table.enabled_visible_count.to_string(), + ); + for entry in &table.entries { + if !entry.hidden { + special_conditions.insert(entry.label.clone(), entry.value); + } + } + } + + let mut named_locomotive_availability = BTreeMap::new(); + if let Some(table) = &save_slice.named_locomotive_availability_table { + metadata.insert( + "save_slice.named_locomotive_availability_source_kind".to_string(), + table.source_kind.clone(), + ); + metadata.insert( + "save_slice.named_locomotive_availability_semantic_family".to_string(), + table.semantic_family.clone(), + ); + metadata.insert( + "save_slice.named_locomotive_availability_entry_count".to_string(), + table.observed_entry_count.to_string(), + ); + metadata.insert( + "save_slice.named_locomotive_availability_zero_count".to_string(), + table.zero_availability_count.to_string(), + ); + if let Some(header_offset) = table.header_offset { + metadata.insert( + "save_slice.named_locomotive_availability_header_offset".to_string(), + header_offset.to_string(), + ); + } + for entry in &table.entries { + named_locomotive_availability.insert(entry.text.clone(), entry.availability_dword); + } + } + + AvailabilityProjection { + candidate_availability, + named_locomotive_availability, + special_conditions, + } +} diff --git a/crates/rrt-runtime/src/documents/project/profile_world/catalogs.rs b/crates/rrt-runtime/src/documents/project/profile_world/catalogs.rs new file mode 100644 index 0000000..28dd8d1 --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/profile_world/catalogs.rs @@ -0,0 +1,122 @@ +use std::collections::BTreeMap; + +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::{RuntimeCargoCatalogEntry, RuntimeLocomotiveCatalogEntry}; + +pub(super) struct CatalogProjection { + pub(super) locomotive_catalog: Option>, + pub(super) cargo_catalog: Option>, +} + +pub(super) fn project_catalogs( + save_slice: &SmpLoadedSaveSlice, + metadata: &mut BTreeMap, +) -> CatalogProjection { + let locomotive_catalog = if let Some(catalog) = &save_slice.locomotive_catalog { + metadata.insert( + "save_slice.locomotive_catalog_source_kind".to_string(), + catalog.source_kind.clone(), + ); + metadata.insert( + "save_slice.locomotive_catalog_semantic_family".to_string(), + catalog.semantic_family.clone(), + ); + metadata.insert( + "save_slice.locomotive_catalog_entry_count".to_string(), + catalog.observed_entry_count.to_string(), + ); + if let Some(entries_offset) = catalog.entries_offset { + metadata.insert( + "save_slice.locomotive_catalog_entries_offset".to_string(), + entries_offset.to_string(), + ); + } + Some( + catalog + .entries + .iter() + .map(|entry| RuntimeLocomotiveCatalogEntry { + locomotive_id: entry.locomotive_id, + name: entry.name.clone(), + }) + .collect::>(), + ) + } else if let Some(table) = &save_slice.named_locomotive_availability_table { + metadata.insert( + "save_slice.locomotive_catalog_source_kind".to_string(), + "derived-from-named-locomotive-availability-table".to_string(), + ); + metadata.insert( + "save_slice.locomotive_catalog_semantic_family".to_string(), + "scenario-save-derived-locomotive-catalog".to_string(), + ); + metadata.insert( + "save_slice.locomotive_catalog_entry_count".to_string(), + table.observed_entry_count.to_string(), + ); + if let Some(entries_offset) = table.entries_offset { + metadata.insert( + "save_slice.locomotive_catalog_entries_offset".to_string(), + entries_offset.to_string(), + ); + } + Some( + table + .entries + .iter() + .enumerate() + .map(|(index, entry)| RuntimeLocomotiveCatalogEntry { + locomotive_id: (index + 1) as u32, + name: entry.text.clone(), + }) + .collect::>(), + ) + } else { + None + }; + + let cargo_catalog = if let Some(catalog) = &save_slice.cargo_catalog { + metadata.insert( + "save_slice.cargo_catalog_source_kind".to_string(), + catalog.source_kind.clone(), + ); + metadata.insert( + "save_slice.cargo_catalog_semantic_family".to_string(), + catalog.semantic_family.clone(), + ); + metadata.insert( + "save_slice.cargo_catalog_entry_count".to_string(), + catalog.observed_entry_count.to_string(), + ); + if let Some(root_offset) = catalog.root_offset { + metadata.insert( + "save_slice.cargo_catalog_root_offset".to_string(), + root_offset.to_string(), + ); + } + Some( + catalog + .entries + .iter() + .map(|entry| RuntimeCargoCatalogEntry { + slot_id: entry.slot_id, + label: entry.label.clone(), + cargo_class: entry.cargo_class, + supplied_token_stem: entry + .supplied_cargo_token_probable_high16_ascii_stem + .clone(), + demanded_token_stem: entry + .demanded_cargo_token_probable_high16_ascii_stem + .clone(), + }) + .collect::>(), + ) + } else { + None + }; + + CatalogProjection { + locomotive_catalog, + cargo_catalog, + } +} diff --git a/crates/rrt-runtime/src/documents/project/profile_world/mod.rs b/crates/rrt-runtime/src/documents/project/profile_world/mod.rs new file mode 100644 index 0000000..a95db2e --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/profile_world/mod.rs @@ -0,0 +1,85 @@ +mod availability; +mod catalogs; +mod overrides; +mod save_profile; +mod world_restore; + +use std::collections::BTreeMap; + +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::{ + RuntimeCargoCatalogEntry, RuntimeLocomotiveCatalogEntry, RuntimeSaveProfileState, + RuntimeWorldRestoreState, +}; + +pub(super) struct ProfileWorldProjection { + pub(super) save_profile: RuntimeSaveProfileState, + pub(super) world_restore: RuntimeWorldRestoreState, + pub(super) candidate_availability: BTreeMap, + pub(super) named_locomotive_availability: BTreeMap, + pub(super) locomotive_catalog: Option>, + pub(super) cargo_catalog: Option>, + pub(super) named_locomotive_cost: BTreeMap, + pub(super) all_cargo_price_override: Option, + pub(super) named_cargo_price_overrides: BTreeMap, + pub(super) all_cargo_production_override: Option, + pub(super) factory_cargo_production_override: Option, + pub(super) farm_mine_cargo_production_override: Option, + pub(super) named_cargo_production_overrides: BTreeMap, + pub(super) cargo_production_overrides: BTreeMap, + pub(super) world_scalar_overrides: BTreeMap, + pub(super) special_conditions: BTreeMap, +} + +use availability::{AvailabilityProjection, project_availability}; +use catalogs::{CatalogProjection, project_catalogs}; +use overrides::{OverrideProjection, project_overrides}; +use save_profile::project_save_profile; +use world_restore::project_world_restore; + +pub(super) fn project_profile_world( + save_slice: &SmpLoadedSaveSlice, + metadata: &mut BTreeMap, +) -> ProfileWorldProjection { + let save_profile = project_save_profile(save_slice, metadata); + let world_restore = project_world_restore(save_slice); + let AvailabilityProjection { + candidate_availability, + named_locomotive_availability, + special_conditions, + } = project_availability(save_slice, metadata); + let CatalogProjection { + locomotive_catalog, + cargo_catalog, + } = project_catalogs(save_slice, metadata); + let OverrideProjection { + named_locomotive_cost, + all_cargo_price_override, + named_cargo_price_overrides, + all_cargo_production_override, + factory_cargo_production_override, + farm_mine_cargo_production_override, + named_cargo_production_overrides, + cargo_production_overrides, + world_scalar_overrides, + } = project_overrides(save_slice, metadata); + + ProfileWorldProjection { + save_profile, + world_restore, + candidate_availability, + named_locomotive_availability, + locomotive_catalog, + cargo_catalog, + named_locomotive_cost, + all_cargo_price_override, + named_cargo_price_overrides, + all_cargo_production_override, + factory_cargo_production_override, + farm_mine_cargo_production_override, + named_cargo_production_overrides, + cargo_production_overrides, + world_scalar_overrides, + special_conditions, + } +} diff --git a/crates/rrt-runtime/src/documents/project/profile_world/overrides.rs b/crates/rrt-runtime/src/documents/project/profile_world/overrides.rs new file mode 100644 index 0000000..a07f6ad --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/profile_world/overrides.rs @@ -0,0 +1,32 @@ +use std::collections::BTreeMap; + +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; + +pub(super) struct OverrideProjection { + pub(super) named_locomotive_cost: BTreeMap, + pub(super) all_cargo_price_override: Option, + pub(super) named_cargo_price_overrides: BTreeMap, + pub(super) all_cargo_production_override: Option, + pub(super) factory_cargo_production_override: Option, + pub(super) farm_mine_cargo_production_override: Option, + pub(super) named_cargo_production_overrides: BTreeMap, + pub(super) cargo_production_overrides: BTreeMap, + pub(super) world_scalar_overrides: BTreeMap, +} + +pub(super) fn project_overrides( + _save_slice: &SmpLoadedSaveSlice, + _metadata: &mut BTreeMap, +) -> OverrideProjection { + OverrideProjection { + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + } +} diff --git a/crates/rrt-runtime/src/documents/project/profile_world/save_profile.rs b/crates/rrt-runtime/src/documents/project/profile_world/save_profile.rs new file mode 100644 index 0000000..b0abc26 --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/profile_world/save_profile.rs @@ -0,0 +1,73 @@ +use std::collections::BTreeMap; + +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::RuntimeSaveProfileState; + +pub(super) fn project_save_profile( + save_slice: &SmpLoadedSaveSlice, + metadata: &mut BTreeMap, +) -> RuntimeSaveProfileState { + let Some(profile) = &save_slice.profile else { + return RuntimeSaveProfileState::default(); + }; + + metadata.insert( + "save_slice.profile_kind".to_string(), + profile.profile_kind.clone(), + ); + metadata.insert( + "save_slice.profile_family".to_string(), + profile.profile_family.clone(), + ); + metadata.insert( + "save_slice.packed_profile_offset".to_string(), + profile.packed_profile_offset.to_string(), + ); + metadata.insert( + "save_slice.packed_profile_len".to_string(), + profile.packed_profile_len.to_string(), + ); + metadata.insert( + "save_slice.leading_word_0_hex".to_string(), + profile.leading_word_0_hex.clone(), + ); + metadata.insert( + "save_slice.profile_byte_0x77_hex".to_string(), + profile.profile_byte_0x77_hex.clone(), + ); + metadata.insert( + "save_slice.profile_byte_0x82_hex".to_string(), + profile.profile_byte_0x82_hex.clone(), + ); + metadata.insert( + "save_slice.profile_byte_0x97_hex".to_string(), + profile.profile_byte_0x97_hex.clone(), + ); + metadata.insert( + "save_slice.profile_byte_0xc5_hex".to_string(), + profile.profile_byte_0xc5_hex.clone(), + ); + if let Some(header_flag_word_3_hex) = &profile.header_flag_word_3_hex { + metadata.insert( + "save_slice.header_flag_word_3_hex".to_string(), + header_flag_word_3_hex.clone(), + ); + } + if let Some(map_path) = &profile.map_path { + metadata.insert("save_slice.map_path".to_string(), map_path.clone()); + } + if let Some(display_name) = &profile.display_name { + metadata.insert("save_slice.display_name".to_string(), display_name.clone()); + } + + RuntimeSaveProfileState { + profile_kind: Some(profile.profile_kind.clone()), + profile_family: Some(profile.profile_family.clone()), + map_path: profile.map_path.clone(), + display_name: profile.display_name.clone(), + selected_year_profile_lane: Some(profile.profile_byte_0x77), + sandbox_enabled: Some(profile.profile_byte_0x82 != 0), + campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0), + staged_profile_copy_on_restore: Some(profile.profile_byte_0x97 != 0), + } +} diff --git a/crates/rrt-runtime/src/documents/project/profile_world/world_restore.rs b/crates/rrt-runtime/src/documents/project/profile_world/world_restore.rs new file mode 100644 index 0000000..7eb5fcc --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/profile_world/world_restore.rs @@ -0,0 +1,268 @@ +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; +use crate::state::{RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldRestoreState}; + +fn special_condition_enabled(save_slice: &SmpLoadedSaveSlice, slot_index: u8) -> Option { + save_slice.special_conditions_table.as_ref().map(|table| { + table + .entries + .iter() + .find(|entry| entry.slot_index == slot_index) + .map(|entry| entry.value != 0) + .unwrap_or(false) + }) +} + +pub(super) fn project_world_restore(save_slice: &SmpLoadedSaveSlice) -> RuntimeWorldRestoreState { + let Some(profile) = &save_slice.profile else { + return RuntimeWorldRestoreState::default(); + }; + let disable_cargo_economy_special_condition_enabled = special_condition_enabled(save_slice, 30); + + RuntimeWorldRestoreState { + selected_year_profile_lane: Some(profile.profile_byte_0x77), + campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0), + sandbox_enabled: Some(profile.profile_byte_0x82 != 0), + seed_tuple_written_from_raw_lane: Some(true), + absolute_counter_requires_shell_context: Some( + save_slice.world_finance_neighborhood_state.is_none(), + ), + absolute_counter_reconstructible_from_save: Some( + save_slice.world_finance_neighborhood_state.is_some(), + ), + packed_year_word_raw_u16: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.packed_year_word_raw_u16), + partial_year_progress_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.partial_year_progress_raw_u8), + current_calendar_tuple_word_raw_u32: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.current_calendar_tuple_word_raw_u32), + current_calendar_tuple_word_2_raw_u32: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.current_calendar_tuple_word_2_raw_u32), + absolute_counter_raw_u32: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.absolute_counter_raw_u32), + absolute_counter_mirror_raw_u32: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.absolute_counter_mirror_raw_u32), + disable_cargo_economy_special_condition_slot: Some(30), + disable_cargo_economy_special_condition_reconstructible_from_save: Some(true), + disable_cargo_economy_special_condition_write_side_grounded: Some(true), + disable_cargo_economy_special_condition_enabled, + use_bio_accelerator_cars_enabled: special_condition_enabled(save_slice, 29), + use_wartime_cargos_enabled: special_condition_enabled(save_slice, 31), + disable_train_crashes_enabled: special_condition_enabled(save_slice, 32), + disable_train_crashes_and_breakdowns_enabled: special_condition_enabled(save_slice, 33), + ai_ignore_territories_at_startup_enabled: special_condition_enabled(save_slice, 34), + limited_track_building_amount: None, + economic_status_code: None, + territory_access_cost: None, + linked_site_removal_follow_on_gate_raw_u8: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.linked_site_removal_follow_on_gate_raw_u8), + linked_site_removal_follow_on_gate_enabled: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.linked_site_removal_follow_on_gate_raw_u8) + .map(|raw| raw != 0), + auto_show_grade_during_track_lay_raw_u8: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.auto_show_grade_during_track_lay_raw_u8), + starting_building_density_level_raw_u8: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.starting_building_density_level_raw_u8), + post_text_building_density_growth_raw_u8: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.building_density_growth_raw_u8), + leftover_simulation_time_accumulator_raw_u32: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.leftover_simulation_time_accumulator_raw_u32), + leftover_simulation_time_accumulator_value_f32_text: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| { + state + .leftover_simulation_time_accumulator_value_f32_text + .clone() + }), + selected_year_lane_snapshot_raw_u8: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.selected_year_lane_snapshot_raw_u8), + all_steam_locomotives_available_raw_u8: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.all_steam_locomotives_available_raw_u8), + all_steam_locomotives_available_enabled: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.all_steam_locomotives_available_raw_u8) + .map(|raw| raw != 0), + all_diesel_locomotives_available_raw_u8: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.all_diesel_locomotives_available_raw_u8), + all_diesel_locomotives_available_enabled: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.all_diesel_locomotives_available_raw_u8) + .map(|raw| raw != 0), + all_electric_locomotives_available_raw_u8: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.all_electric_locomotives_available_raw_u8), + all_electric_locomotives_available_enabled: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.all_electric_locomotives_available_raw_u8) + .map(|raw| raw != 0), + issue_37_value: save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.issue_value), + issue_38_value: save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.issue_38_value), + issue_39_value: save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.issue_39_value), + issue_3a_value: save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.issue_3a_value), + issue_37_multiplier_raw_u32: save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.multiplier_raw_u32), + issue_37_multiplier_value_f32_text: save_slice + .world_issue_37_state + .as_ref() + .map(|state| state.multiplier_value_f32_text.clone()), + stock_issue_and_buyback_policy_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.stock_policy_raw_u8), + bond_issue_and_repayment_policy_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.bond_policy_raw_u8), + bankruptcy_policy_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.bankruptcy_policy_raw_u8), + dividend_policy_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.dividend_policy_raw_u8), + building_density_growth_setting_raw_u32: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.building_density_growth_setting_raw_u32), + stock_issue_and_buyback_allowed: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.stock_policy_raw_u8 == 0), + bond_issue_and_repayment_allowed: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.bond_policy_raw_u8 == 0), + bankruptcy_allowed: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.bankruptcy_policy_raw_u8 == 0), + dividend_adjustment_allowed: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.dividend_policy_raw_u8 == 0), + finance_neighborhood_candidates: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| { + state + .labels + .iter() + .enumerate() + .map(|(index, label)| RuntimeWorldFinanceNeighborhoodCandidate { + label: label.clone(), + relative_offset: state.relative_offsets[index], + relative_offset_hex: state.relative_offset_hex[index].clone(), + raw_u32: state.raw_u32[index], + raw_u32_hex: state.raw_hex[index].clone(), + value_i32: state.value_i32[index], + value_f32_text: state.value_f32_text[index].clone(), + }) + .collect::>() + }) + .unwrap_or_default(), + economic_tuning_mirror_raw_u32: save_slice + .world_economic_tuning_state + .as_ref() + .map(|state| state.mirror_raw_u32), + economic_tuning_mirror_value_f32_text: save_slice + .world_economic_tuning_state + .as_ref() + .map(|state| state.mirror_value_f32_text.clone()), + economic_tuning_lane_raw_u32: save_slice + .world_economic_tuning_state + .as_ref() + .map(|state| state.lane_raw_u32.clone()) + .unwrap_or_default(), + economic_tuning_lane_value_f32_text: save_slice + .world_economic_tuning_state + .as_ref() + .map(|state| state.lane_value_f32_text.clone()) + .unwrap_or_default(), + cached_available_locomotive_rating_raw_u32: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| state.cached_available_locomotive_rating_raw_u32), + cached_available_locomotive_rating_value_f32_text: save_slice + .world_locomotive_policy_state + .as_ref() + .and_then(|state| { + state + .cached_available_locomotive_rating_value_f32_text + .clone() + }), + selected_year_bucket_direct_lane_raw_u32: Vec::new(), + selected_year_bucket_direct_lane_value_f32_text: Vec::new(), + selected_year_bucket_complement_lane_raw_u32: Vec::new(), + selected_year_bucket_complement_lane_value_f32_text: Vec::new(), + selected_year_bucket_scaled_companion_lane_raw_u32: Vec::new(), + selected_year_bucket_scaled_companion_lane_value_f32_text: Vec::new(), + selected_year_bucket_scalar_raw_u32: None, + selected_year_bucket_scalar_value_f32_text: None, + selected_year_gap_scalar_raw_u32: None, + selected_year_gap_scalar_value_f32_text: None, + absolute_counter_restore_kind: Some( + if save_slice.world_finance_neighborhood_state.is_some() { + "save-direct-world-absolute-counter".to_string() + } else { + "mode-adjusted-selected-year-lane".to_string() + }, + ), + absolute_counter_adjustment_context: Some( + if save_slice.world_finance_neighborhood_state.is_some() { + "save-direct-world-block-0x32c8".to_string() + } else { + "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30" + .to_string() + }, + ), + } +} diff --git a/crates/rrt-runtime/src/documents/project/projection.rs b/crates/rrt-runtime/src/documents/project/projection.rs new file mode 100644 index 0000000..e56bb46 --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/projection.rs @@ -0,0 +1,100 @@ +use super::super::{RuntimeImportContext, SaveSliceProjection, SaveSliceProjectionMode}; +use super::{ + PackedEventProjection, ProfileWorldProjection, ProjectionMetadata, + append_projection_collection_metadata, append_projection_note_metadata, + build_projection_metadata, project_chairman_projection, project_company_projection, + project_event_runtime, project_near_city_projection, project_profile_world, +}; +use crate::inspect::smp::save_load::SmpLoadedSaveSlice; + +pub(super) fn project_save_slice_components( + save_slice: &SmpLoadedSaveSlice, + runtime_context: &RuntimeImportContext, + mode: SaveSliceProjectionMode, +) -> Result { + let ProjectionMetadata { + world_flags, + mut metadata, + } = build_projection_metadata(save_slice, mode); + let ProfileWorldProjection { + save_profile, + world_restore, + candidate_availability, + named_locomotive_availability, + locomotive_catalog, + cargo_catalog, + named_locomotive_cost, + all_cargo_price_override, + named_cargo_price_overrides, + all_cargo_production_override, + factory_cargo_production_override, + farm_mine_cargo_production_override, + named_cargo_production_overrides, + cargo_production_overrides, + world_scalar_overrides, + special_conditions, + } = project_profile_world(save_slice, &mut metadata); + let company_projection = project_company_projection(save_slice, &mut metadata); + let chairman_projection = project_chairman_projection(save_slice, &mut metadata); + let near_city_projection = + project_near_city_projection(save_slice, &company_projection, &mut metadata); + + append_projection_collection_metadata(save_slice, &mut metadata); + + let PackedEventProjection { + packed_event_collection, + event_runtime_records, + } = project_event_runtime( + save_slice, + runtime_context, + &company_projection, + &chairman_projection, + locomotive_catalog.as_deref(), + cargo_catalog.as_deref(), + &mut metadata, + )?; + + append_projection_note_metadata(save_slice, &mut metadata); + + Ok(SaveSliceProjection { + world_flags, + save_profile, + world_restore, + metadata, + packed_event_collection, + event_runtime_records, + companies: company_projection.companies, + has_company_projection: company_projection.has_company_projection, + has_company_selection_override: company_projection.has_company_selection_override, + selected_company_id: company_projection.selected_company_id, + company_market_state: company_projection.company_market_state, + company_periodic_side_latch_state: company_projection.company_periodic_side_latch_state, + has_company_market_projection: company_projection.has_company_market_projection, + world_issue_opinion_base_terms_raw_i32: company_projection + .world_issue_opinion_base_terms_raw_i32, + chairman_profiles: chairman_projection.chairman_profiles, + has_chairman_projection: chairman_projection.has_chairman_projection, + has_chairman_selection_override: chairman_projection.has_chairman_selection_override, + selected_chairman_profile_id: chairman_projection.selected_chairman_profile_id, + chairman_issue_opinion_terms_raw_i32: chairman_projection + .chairman_issue_opinion_terms_raw_i32, + chairman_personality_raw_u8: chairman_projection.chairman_personality_raw_u8, + near_city_acquisition_regions: near_city_projection.regions, + near_city_acquisition_sites: near_city_projection.sites, + has_near_city_projection: near_city_projection.has_projection, + candidate_availability, + named_locomotive_availability, + locomotive_catalog, + cargo_catalog, + named_locomotive_cost, + all_cargo_price_override, + named_cargo_price_overrides, + all_cargo_production_override, + factory_cargo_production_override, + farm_mine_cargo_production_override, + named_cargo_production_overrides, + cargo_production_overrides, + world_scalar_overrides, + special_conditions, + }) +} diff --git a/crates/rrt-runtime/src/documents/project/state_build.rs b/crates/rrt-runtime/src/documents/project/state_build.rs new file mode 100644 index 0000000..bfad190 --- /dev/null +++ b/crates/rrt-runtime/src/documents/project/state_build.rs @@ -0,0 +1,202 @@ +use std::collections::BTreeMap; + +use super::super::SaveSliceProjection; +use crate::state::{RuntimeServiceState, RuntimeWorldRestoreState}; +use crate::{CalendarPoint, RuntimeState}; + +pub(super) fn build_standalone_runtime_state(projection: SaveSliceProjection) -> RuntimeState { + RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: projection.world_flags, + save_profile: projection.save_profile, + world_restore: projection.world_restore, + metadata: projection.metadata, + companies: projection.companies, + selected_company_id: if projection.has_company_projection { + projection.selected_company_id + } else { + None + }, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: projection.chairman_profiles, + selected_chairman_profile_id: if projection.has_chairman_projection { + projection.selected_chairman_profile_id + } else { + None + }, + trains: Vec::new(), + locomotive_catalog: projection.locomotive_catalog.unwrap_or_default(), + cargo_catalog: projection.cargo_catalog.unwrap_or_default(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: projection.packed_event_collection, + event_runtime_records: projection.event_runtime_records, + candidate_availability: projection.candidate_availability, + named_locomotive_availability: projection.named_locomotive_availability, + named_locomotive_cost: projection.named_locomotive_cost, + all_cargo_price_override: projection.all_cargo_price_override, + named_cargo_price_overrides: projection.named_cargo_price_overrides, + all_cargo_production_override: projection.all_cargo_production_override, + factory_cargo_production_override: projection.factory_cargo_production_override, + farm_mine_cargo_production_override: projection.farm_mine_cargo_production_override, + named_cargo_production_overrides: projection.named_cargo_production_overrides, + cargo_production_overrides: projection.cargo_production_overrides, + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: projection.world_scalar_overrides, + special_conditions: projection.special_conditions, + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: projection + .world_issue_opinion_base_terms_raw_i32, + company_market_state: projection.company_market_state, + company_periodic_side_latch_state: projection.company_periodic_side_latch_state, + chairman_issue_opinion_terms_raw_i32: projection.chairman_issue_opinion_terms_raw_i32, + chairman_personality_raw_u8: projection.chairman_personality_raw_u8, + near_city_acquisition_regions: projection.near_city_acquisition_regions, + near_city_acquisition_sites: projection.near_city_acquisition_sites, + ..RuntimeServiceState::default() + }, + } +} + +pub(super) fn build_overlay_runtime_state( + base_state: &RuntimeState, + projection: SaveSliceProjection, +) -> RuntimeState { + let mut world_flags = base_state.world_flags.clone(); + world_flags.retain(|key, _| !key.starts_with("save_slice.")); + world_flags.extend(projection.world_flags); + + let mut metadata = base_state.metadata.clone(); + metadata.retain(|key, _| !key.starts_with("save_slice.")); + metadata.extend(projection.metadata); + + RuntimeState { + calendar: base_state.calendar, + world_flags, + save_profile: projection.save_profile, + world_restore: RuntimeWorldRestoreState { + territory_access_cost: base_state.world_restore.territory_access_cost, + ..projection.world_restore + }, + metadata, + companies: if projection.has_company_projection { + projection.companies + } else { + base_state.companies.clone() + }, + selected_company_id: if projection.has_company_projection + || projection.has_company_selection_override + { + projection.selected_company_id + } else { + base_state.selected_company_id + }, + players: base_state.players.clone(), + selected_player_id: base_state.selected_player_id, + chairman_profiles: if projection.has_chairman_projection { + projection.chairman_profiles + } else { + base_state.chairman_profiles.clone() + }, + selected_chairman_profile_id: if projection.has_chairman_projection + || projection.has_chairman_selection_override + { + projection.selected_chairman_profile_id + } else { + base_state.selected_chairman_profile_id + }, + trains: base_state.trains.clone(), + locomotive_catalog: projection + .locomotive_catalog + .unwrap_or_else(|| base_state.locomotive_catalog.clone()), + cargo_catalog: projection + .cargo_catalog + .unwrap_or_else(|| base_state.cargo_catalog.clone()), + territories: base_state.territories.clone(), + company_territory_track_piece_counts: base_state + .company_territory_track_piece_counts + .clone(), + company_territory_access: base_state.company_territory_access.clone(), + packed_event_collection: projection.packed_event_collection, + event_runtime_records: projection.event_runtime_records, + candidate_availability: projection.candidate_availability, + named_locomotive_availability: projection.named_locomotive_availability, + named_locomotive_cost: base_state.named_locomotive_cost.clone(), + all_cargo_price_override: base_state.all_cargo_price_override, + named_cargo_price_overrides: base_state.named_cargo_price_overrides.clone(), + all_cargo_production_override: base_state.all_cargo_production_override, + factory_cargo_production_override: base_state.factory_cargo_production_override, + farm_mine_cargo_production_override: base_state.farm_mine_cargo_production_override, + named_cargo_production_overrides: base_state.named_cargo_production_overrides.clone(), + cargo_production_overrides: base_state.cargo_production_overrides.clone(), + world_runtime_variables: base_state.world_runtime_variables.clone(), + company_runtime_variables: base_state.company_runtime_variables.clone(), + player_runtime_variables: base_state.player_runtime_variables.clone(), + territory_runtime_variables: base_state.territory_runtime_variables.clone(), + world_scalar_overrides: base_state.world_scalar_overrides.clone(), + special_conditions: projection.special_conditions, + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: if projection + .world_issue_opinion_base_terms_raw_i32 + .is_empty() + { + base_state + .service_state + .world_issue_opinion_base_terms_raw_i32 + .clone() + } else { + projection.world_issue_opinion_base_terms_raw_i32 + }, + company_market_state: if projection.has_company_market_projection { + projection.company_market_state + } else { + base_state.service_state.company_market_state.clone() + }, + company_periodic_side_latch_state: if projection.has_company_market_projection { + projection.company_periodic_side_latch_state + } else { + base_state + .service_state + .company_periodic_side_latch_state + .clone() + }, + chairman_issue_opinion_terms_raw_i32: if projection.has_chairman_projection { + projection.chairman_issue_opinion_terms_raw_i32 + } else { + base_state + .service_state + .chairman_issue_opinion_terms_raw_i32 + .clone() + }, + chairman_personality_raw_u8: if projection.has_chairman_projection { + projection.chairman_personality_raw_u8 + } else { + base_state.service_state.chairman_personality_raw_u8.clone() + }, + near_city_acquisition_regions: if projection.has_near_city_projection { + projection.near_city_acquisition_regions + } else { + base_state + .service_state + .near_city_acquisition_regions + .clone() + }, + near_city_acquisition_sites: if projection.has_near_city_projection { + projection.near_city_acquisition_sites + } else { + base_state.service_state.near_city_acquisition_sites.clone() + }, + ..base_state.service_state.clone() + }, + } +} diff --git a/crates/rrt-runtime/src/documents/tests/mod.rs b/crates/rrt-runtime/src/documents/tests/mod.rs new file mode 100644 index 0000000..8d99772 --- /dev/null +++ b/crates/rrt-runtime/src/documents/tests/mod.rs @@ -0,0 +1,26 @@ +use super::*; +pub(super) use std::collections::BTreeMap; + +use crate::event::conditions::RuntimeConditionComparator; +pub(super) use crate::event::conditions::*; +pub(super) use crate::event::effects::*; +pub(super) use crate::event::metrics::*; +pub(super) use crate::event::records::*; +pub(super) use crate::event::targets::*; +use crate::inspect::smp::{ + events::*, profiles::*, regions::*, save_load::*, special_conditions::*, structures::*, + world::*, +}; +pub(super) use crate::state::*; +pub(super) use crate::validation::*; +pub(super) use crate::{CalendarPoint, RuntimeState, StepCommand, execute_step_command}; + +mod overlay_import; +mod packed_events; +mod profile_world; +mod projection; +mod roundtrip; +mod support; +mod validation; + +use support::*; diff --git a/crates/rrt-runtime/src/documents/tests/overlay_import.rs b/crates/rrt-runtime/src/documents/tests/overlay_import.rs new file mode 100644 index 0000000..e48f8dd --- /dev/null +++ b/crates/rrt-runtime/src/documents/tests/overlay_import.rs @@ -0,0 +1,580 @@ +use super::*; + +#[test] +fn overlay_replaces_base_company_and_chairman_context_from_save_slice() { + let base_state = RuntimeState { + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + current_cash: 5, + debt: 1, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: Some(1), + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 10, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(42), + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 9, + name: "Base Chairman".to_string(), + active: true, + current_cash: 10, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(9), + territories: vec![crate::state::RuntimeTerritory { + territory_id: 7, + name: Some("Base Territory".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + ..state() + }; + let 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(), + 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: Some(save_company_roster()), + chairman_profile_table: Some(save_chairman_profile_table()), + 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![], + }; + + let input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "overlay-save-native-company-chairman", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.companies.len(), 2); + assert_eq!(input.state.selected_company_id, Some(1)); + assert_eq!(input.state.chairman_profiles.len(), 2); + assert_eq!(input.state.selected_chairman_profile_id, Some(1)); + assert_eq!(input.state.territories, base_state.territories); + assert_eq!( + input + .state + .service_state + .company_market_state + .get(&2) + .map(|state| state.linked_transit_latch), + Some(true) + ); +} + +#[test] +fn overlay_replaces_base_near_city_runtime_slice_from_save_collections() { + let mut base_state = state(); + base_state.service_state.near_city_acquisition_regions = + vec![crate::state::RuntimeNearCityAcquisitionRegion { + region_id: 9, + name: "Base Region".to_string(), + profile_names: vec!["Base".to_string()], + fixed_row_shape_family_signature: None, + fixed_row_best_density_lane_relative_offset_hex: None, + }]; + base_state.service_state.near_city_acquisition_sites = + vec![crate::state::RuntimeNearCityAcquisitionSite { + site_id: 9, + primary_name: "BaseSite".to_string(), + secondary_name: "BaseSet".to_string(), + region_id: Some(9), + owner_company_id: None, + preferred_company_id: None, + owner_company_id_provenance: + crate::state::RuntimeNearCityAcquisitionValueProvenance::Grounded, + self_id: 9, + self_id_provenance: crate::state::RuntimeNearCityAcquisitionValueProvenance::Grounded, + candidate_subtype_label: "industry".to_string(), + candidate_subtype_provenance: + crate::state::RuntimeNearCityAcquisitionValueProvenance::Grounded, + cached_tri_lane_0: 1, + cached_tri_lane_1: 2, + cached_tri_lane_2: 3, + cached_tri_lane_provenance: + crate::state::RuntimeNearCityAcquisitionValueProvenance::Grounded, + nontransport_source_label: "base".to_string(), + tri_lane_source_label: "base".to_string(), + }]; + + let save_slice = SmpLoadedSaveSlice { + file_extension_hint: Some("gms".to_string()), + container_profile_family: Some("rt3-105-save-container-v1".to_string()), + mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), + mechanism_confidence: "mixed".to_string(), + trailer_family: Some("rt3-105-save-trailer-v1".to_string()), + bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), + 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: Some(save_company_roster()), + chairman_profile_table: Some(save_chairman_profile_table()), + region_collection: Some(save_region_collection()), + region_fixed_row_run_summary: Some(save_region_fixed_row_run_summary()), + placed_structure_collection: Some(save_placed_structure_collection()), + placed_structure_dynamic_side_buffer_summary: Some( + save_placed_structure_dynamic_side_buffer_summary(), + ), + special_conditions_table: None, + event_runtime_collection: None, + notes: vec![], + }; + + let input = + build_runtime_state_input_from_overlay(&base_state, &save_slice, "overlay-near-city", None) + .expect("overlay input should project near-city slice"); + + assert_eq!( + input + .state + .service_state + .near_city_acquisition_regions + .len(), + 2 + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites.len(), + 2 + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites[0].owner_company_id, + Some(1) + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites[0].tri_lane_source_label, + "matched_side_buffer_name_pair_plus_region_fixed_row_candidate" + ); +} + +#[test] +fn overlay_applies_selection_only_company_and_chairman_context_from_save_slice() { + let base_state = RuntimeState { + companies: vec![ + crate::state::RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + linked_chairman_profile_id: Some(1), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + crate::state::RuntimeCompany { + company_id: 42, + current_cash: 200, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + linked_chairman_profile_id: Some(9), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + ], + selected_company_id: Some(42), + chairman_profiles: vec![ + crate::state::RuntimeChairmanProfile { + profile_id: 1, + name: "Selected".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + crate::state::RuntimeChairmanProfile { + profile_id: 9, + name: "Base".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(42), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + ], + selected_chairman_profile_id: Some(9), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 42, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 30_000, + bond_count: 3, + live_bond_slots: Vec::new(), + largest_live_bond_principal: Some(750_000), + highest_coupon_live_bond_principal: Some(500_000), + mutable_support_scalar_raw_u32: 0x3f19999a, + young_company_support_scalar_raw_u32: 0x42580000, + support_progress_word: 8, + recent_per_share_cache_absolute_counter: 0, + recent_per_share_cached_value_bits: 0, + recent_per_share_subscore_raw_u32: 0x42000000, + cached_share_price_raw_u32: 0x42180000, + chairman_salary_baseline: 21, + chairman_salary_current: 24, + chairman_bonus_year: 1836, + chairman_bonus_amount: 600, + founding_year: 1834, + last_bankruptcy_year: 0, + last_dividend_year: 1838, + current_issue_calendar_word: 4, + current_issue_calendar_word_2: 5, + prior_issue_calendar_word: 3, + prior_issue_calendar_word_2: 4, + city_connection_latch: false, + linked_transit_latch: true, + linked_transit_route_anchor_entry_id: Some(91), + linked_transit_route_anchor_fallback_counts: vec![2, 4, 6], + stat_band_root_0cfb_candidates: Vec::new(), + stat_band_root_0d7f_candidates: Vec::new(), + stat_band_root_1c47_candidates: Vec::new(), + year_stat_family_qword_bits: Vec::new(), + special_stat_family_232a_qword_bits: Vec::new(), + issue_opinion_terms_raw_i32: Vec::new(), + direct_control_transfer_float_fields_raw_u32: BTreeMap::new(), + direct_control_transfer_int_fields_raw_u32: BTreeMap::new(), + }, + )]), + world_issue_opinion_base_terms_raw_i32: Vec::new(), + chairman_issue_opinion_terms_raw_i32: BTreeMap::new(), + ..RuntimeServiceState::default() + }, + ..state() + }; + let save_slice = SmpLoadedSaveSlice { + file_extension_hint: Some("gms".to_string()), + container_profile_family: Some("rt3-105-save-container-v1".to_string()), + mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), + mechanism_confidence: "mixed".to_string(), + trailer_family: Some("rt3-105-save-trailer-v1".to_string()), + bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), + 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: Some(SmpLoadedCompanyRoster { + source_kind: "save-direct-world-block-company-selection-only".to_string(), + semantic_family: "scenario-selected-company-context".to_string(), + observed_entry_count: 0, + selected_company_id: Some(1), + entries: Vec::new(), + }), + chairman_profile_table: Some(SmpLoadedChairmanProfileTable { + source_kind: "save-direct-world-block-chairman-selection-only".to_string(), + semantic_family: "scenario-selected-chairman-context".to_string(), + observed_entry_count: 0, + selected_chairman_profile_id: Some(1), + entries: Vec::new(), + }), + 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![], + }; + + let input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "overlay-save-selection-only-context", + None, + ) + .expect("overlay input should project"); + + let mut expected_companies = base_state.companies.clone(); + expected_companies[1].investor_confidence = 38; + assert_eq!(input.state.companies, expected_companies); + assert_eq!(input.state.selected_company_id, Some(1)); + assert_eq!(input.state.chairman_profiles, base_state.chairman_profiles); + assert_eq!(input.state.selected_chairman_profile_id, Some(1)); + assert_eq!( + input.state.service_state.company_market_state, + base_state.service_state.company_market_state + ); +} + +#[test] +fn overlays_save_slice_events_onto_base_company_context() { + let base_state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 2, + phase_slot: 1, + tick_slot: 3, + }, + world_flags: BTreeMap::from([("base.only".to_string(), true)]), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::from([("base.note".to_string(), "kept".to_string())]), + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 1, + trigger_kind: 1, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![], + }], + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + periodic_boundary_calls: 9, + annual_finance_service_calls: 0, + periodic_route_preference_override_apply_count: 0, + periodic_route_preference_override_restore_count: 0, + trigger_dispatch_counts: BTreeMap::new(), + total_event_record_services: 4, + dirty_rerun_count: 2, + world_issue_opinion_base_terms_raw_i32: Vec::new(), + company_market_state: BTreeMap::new(), + company_periodic_side_latch_state: BTreeMap::new(), + active_periodic_route_preference_override: None, + last_periodic_route_preference_override: None, + annual_finance_last_actions: BTreeMap::new(), + annual_finance_action_counts: BTreeMap::new(), + annual_dividend_adjustment_commit_count: 0, + annual_bond_last_retired_principal_total: 0, + annual_bond_last_issued_principal_total: 0, + annual_stock_repurchase_last_share_count: 0, + annual_stock_issue_last_share_count: 0, + annual_finance_last_news_family_candidates: BTreeMap::new(), + annual_finance_last_news_events: Vec::new(), + ..RuntimeServiceState::default() + }, + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 42, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(48), + decode_status: "parity_only".to_string(), + payload_family: "synthetic_harness".to_string(), + trigger_kind: Some(7), + active: Some(true), + marks_collection_dirty: Some(false), + one_shot: Some(false), + compact_control: None, + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: vec![], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { + target: crate::event::targets::RuntimeCompanyTarget::Ids { ids: vec![42] }, + delta: 50, + }], + executable_import_ready: false, + notes: vec!["needs company context".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "overlay-smoke", + Some("overlay test".to_string()), + ) + .expect("overlay input should project"); + + assert_eq!(input.state.calendar, base_state.calendar); + assert_eq!(input.state.companies, base_state.companies); + assert_eq!(input.state.service_state, base_state.service_state); + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .map(|summary| summary.imported_runtime_record_count), + Some(1) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.import_projection") + .map(String::as_str), + Some("overlay-runtime-restore-v1") + ); + assert_eq!( + input.state.metadata.get("base.note").map(String::as_str), + Some("kept") + ); + assert_eq!(input.state.world_flags.get("base.only"), Some(&true)); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("overlay-imported company-targeted record should run"); + + assert_eq!(input.state.companies[0].current_cash, 550); +} diff --git a/crates/rrt-runtime/src/documents/tests/packed_events.rs b/crates/rrt-runtime/src/documents/tests/packed_events.rs new file mode 100644 index 0000000..8b35096 --- /dev/null +++ b/crates/rrt-runtime/src/documents/tests/packed_events.rs @@ -0,0 +1,8145 @@ +use super::*; + +#[test] +fn projects_executable_packed_records_into_runtime_and_services_follow_on() { + let 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(), + 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: Some(save_cargo_catalog(&[ + (1, crate::event::targets::RuntimeCargoClass::Factory), + (5, crate::event::targets::RuntimeCargoClass::FarmMine), + (9, crate::event::targets::RuntimeCargoClass::Other), + ])), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(64), + decode_status: "executable".to_string(), + payload_family: "synthetic_harness".to_string(), + trigger_kind: Some(7), + active: Some(true), + marks_collection_dirty: Some(true), + one_shot: Some(false), + compact_control: None, + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 1, 0, 0], + grouped_effect_rows: vec![], + decoded_conditions: Vec::new(), + decoded_actions: vec![ + RuntimeEffect::SetWorldFlag { + key: "from_packed_root".to_string(), + value: true, + }, + RuntimeEffect::AppendEventRecord { + record: Box::new(RuntimeEventRecordTemplate { + record_id: 99, + trigger_kind: 0x0a, + active: true, + marks_collection_dirty: false, + one_shot: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetSpecialCondition { + label: "Imported Follow-On".to_string(), + value: 1, + }], + }), + }, + ], + executable_import_ready: true, + notes: vec!["decoded test record".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-exec", + Some("test packed event input".to_string()), + ) + .expect("save slice should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .map(|summary| summary.imported_runtime_record_count), + Some(1) + ); + + let result = execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("trigger service should succeed"); + + assert_eq!(result.final_summary.event_runtime_record_count, 2); + assert_eq!(result.final_summary.total_event_record_service_count, 2); + assert_eq!(result.final_summary.total_trigger_dispatch_count, 2); + assert_eq!(result.final_summary.dirty_rerun_count, 1); + assert_eq!(input.state.world_flags.get("from_packed_root"), Some(&true)); + assert_eq!( + input.state.special_conditions.get("Imported Follow-On"), + Some(&1) + ); + assert_eq!(input.state.event_runtime_records[0].service_count, 1); + assert_eq!(input.state.event_runtime_records[1].record_id, 99); + assert_eq!(input.state.event_runtime_records[1].service_count, 1); +} + +#[test] +fn leaves_parity_only_packed_records_out_of_runtime_event_records() { + let 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(), + 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: Some(save_cargo_catalog(&[ + (1, crate::event::targets::RuntimeCargoClass::Factory), + (5, crate::event::targets::RuntimeCargoClass::FarmMine), + (9, crate::event::targets::RuntimeCargoClass::Other), + ])), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(48), + decode_status: "parity_only".to_string(), + payload_family: "synthetic_harness".to_string(), + trigger_kind: Some(7), + active: Some(true), + marks_collection_dirty: Some(false), + one_shot: Some(false), + compact_control: None, + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: vec![], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { + target: crate::event::targets::RuntimeCompanyTarget::Ids { ids: vec![42] }, + delta: 50, + }], + executable_import_ready: false, + notes: vec!["decoded but not importable".to_string()], + }], + }), + notes: vec![], + }; + + let input = + build_runtime_state_input_from_save_slice(&save_slice, "packed-events-parity-only", None) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .map(|summary| summary.decoded_record_count), + Some(1) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .map(|summary| summary.imported_runtime_record_count), + Some(0) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_company_context") + ); +} + +#[test] +fn classifies_symbolic_company_target_blockers_for_standalone_import() { + let 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(), + 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: Some(save_cargo_catalog(&[ + (1, crate::event::targets::RuntimeCargoClass::Factory), + (5, crate::event::targets::RuntimeCargoClass::FarmMine), + (9, crate::event::targets::RuntimeCargoClass::Other), + ])), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 12, + live_record_count: 3, + live_entry_ids: vec![10, 11, 12], + decoded_record_count: 3, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![ + synthetic_packed_record( + 0, + 10, + RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::SelectedCompany, + delta: 1, + }, + ), + synthetic_packed_record( + 1, + 11, + RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::HumanCompanies, + delta: 2, + }, + ), + synthetic_packed_record( + 2, + 12, + RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::ConditionTrueCompany, + delta: 3, + }, + ), + ], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice(&save_slice, "symbolic-blockers", None) + .expect("standalone projection should succeed"); + + assert!(input.state.event_runtime_records.is_empty()); + let outcomes = input + .state + .packed_event_collection + .as_ref() + .expect("packed event collection should be present") + .records + .iter() + .map(|record| record.import_outcome.clone()) + .collect::>(); + assert_eq!( + outcomes, + vec![ + Some("blocked_missing_selection_context".to_string()), + Some("blocked_missing_company_role_context".to_string()), + Some("blocked_missing_condition_context".to_string()), + ] + ); +} + +#[test] +fn overlays_symbolic_company_targets_into_executable_runtime_records() { + let base_state = RuntimeState { + companies: vec![ + crate::state::RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 10, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + crate::state::RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 50, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + selected_company_id: Some(1), + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 22, + live_record_count: 2, + live_entry_ids: vec![21, 22], + decoded_record_count: 2, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![ + synthetic_packed_record( + 0, + 21, + RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::SelectedCompany, + delta: 15, + }, + ), + synthetic_packed_record( + 1, + 22, + RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::AiCompanies, + delta: 4, + }, + ), + ], + }), + notes: vec![], + }; + + let mut input = + build_runtime_state_input_from_overlay(&base_state, &save_slice, "symbolic-overlay", None) + .expect("overlay projection should succeed"); + + assert_eq!(input.state.event_runtime_records.len(), 2); + let outcomes = input + .state + .packed_event_collection + .as_ref() + .expect("packed event collection should be present") + .records + .iter() + .map(|record| record.import_outcome.clone()) + .collect::>(); + assert_eq!( + outcomes, + vec![Some("imported".to_string()), Some("imported".to_string())] + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("symbolic overlay dispatch should succeed"); + + assert_eq!(input.state.companies[0].current_cash, 115); + assert_eq!(input.state.companies[1].debt, 24); +} + +#[test] +fn leaves_real_records_without_compact_control_blocked_missing_compact_control() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::AllCompanies, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 7, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-structural-only", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_compact_control") + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .map(|summary| summary.records[0].standalone_condition_rows.len()), + Some(1) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .map(|summary| summary.records[0].grouped_effect_rows.len()), + Some(1) + ); +} + +#[test] +fn lowers_negative_sentinel_company_scopes_into_runtime_company_targets() { + let base_state = RuntimeState { + companies: vec![ + crate::state::RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 10, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + crate::state::RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 50, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + crate::state::RuntimeCompany { + company_id: 3, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 70, + debt: 30, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + selected_company_id: Some(3), + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 11, + live_record_count: 5, + live_entry_ids: vec![7, 8, 9, 10, 11], + decoded_record_count: 5, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![ + SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::AllCompanies, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 7, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }, + SmpLoadedPackedEventRecordSummary { + record_index: 1, + live_entry_id: 8, + payload_offset: Some(0x7282), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::SelectedCompanyOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 8, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }, + SmpLoadedPackedEventRecordSummary { + record_index: 2, + live_entry_id: 9, + payload_offset: Some(0x7302), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::AiCompaniesOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 9, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }, + SmpLoadedPackedEventRecordSummary { + record_index: 3, + live_entry_id: 10, + payload_offset: Some(0x7382), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::HumanCompaniesOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 10, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }, + SmpLoadedPackedEventRecordSummary { + record_index: 4, + live_entry_id: 11, + payload_offset: Some(0x7402), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::Disabled, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 11, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }, + ], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "packed-events-real-descriptor-frontier", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.event_runtime_records.len(), 4); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].compact_control.as_ref()) + .map(|control| control.mode_byte_0x7ef), + Some(6) + ); + let effects = input + .state + .event_runtime_records + .iter() + .map(|record| record.effects[0].clone()) + .collect::>(); + assert_eq!( + effects, + vec![ + RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::AllActive, + value: 7, + }, + RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::SelectedCompany, + value: 8, + }, + RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::AiCompanies, + value: 9, + }, + RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::HumanCompanies, + value: 10, + }, + ] + ); + assert_eq!( + input.state.packed_event_collection.as_ref().map(|summary| { + summary + .records + .iter() + .map(|record| record.import_outcome.clone()) + .collect::>() + }), + Some(vec![ + Some("imported".to_string()), + Some("imported".to_string()), + Some("imported".to_string()), + Some("imported".to_string()), + Some("blocked_company_condition_scope_disabled".to_string()), + ]) + ); +} + +#[test] +fn blocks_player_scoped_effects_without_player_runtime_context() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(player_negative_sentinel_scope()), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetPlayerCash { + target: RuntimePlayerTarget::ConditionTruePlayer, + value: 7, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "negative-sentinel-player-scope", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_player_context") + ); +} + +#[test] +fn blocks_named_or_aggregate_territory_conditions_without_territory_context() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2313, + subtype: 0, + flag_bytes: vec![ + 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + candidate_name: None, + comparator: Some("ge".to_string()), + metric: Some("Territory Track Pieces".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Territory Track Pieces >= 10".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: Vec::new(), + }], + negative_sentinel_scope: Some(territory_negative_sentinel_scope()), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: vec![RuntimeCondition::TerritoryNumericThreshold { + target: RuntimeTerritoryTarget::AllTerritories, + metric: crate::event::metrics::RuntimeTerritoryMetric::TrackPiecesTotal, + comparator: crate::event::conditions::RuntimeConditionComparator::Ge, + value: 10, + }], + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 7, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "negative-sentinel-territory-scope", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_territory_context") + ); +} + +#[test] +fn leaves_real_records_with_unclassified_scope_blocked_unmapped_real_descriptor() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control_without_symbolic_company_scope()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-real-descriptor-frontier", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_unmapped_real_descriptor") + ); +} + +#[test] +fn leaves_recovered_shell_owned_descriptor_rows_on_explicit_shell_owned_frontier() { + let 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(), + 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: Some(save_company_roster()), + chairman_profile_table: Some(save_chairman_profile_table()), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 8, + live_record_count: 1, + live_entry_ids: vec![8], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 8, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control_without_symbolic_company_scope()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![2, 0, 0, 0], + grouped_effect_rows: vec![ + real_stock_prices_shell_row(120), + real_merger_premium_shell_row(25), + ], + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + notes: vec!["synthetic shell-owned descriptor test record".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-shell-owned-descriptor-frontier", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_shell_owned_descriptor") + ); +} + +#[test] +fn imports_credit_rating_descriptor_from_save_slice_company_context() { + let 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(), + 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: Some(save_company_roster()), + chairman_profile_table: Some(save_chairman_profile_table()), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 9, + live_record_count: 1, + live_entry_ids: vec![9], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 9, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control_without_symbolic_company_scope()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_credit_rating_row(640)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::SelectedCompany, + metric: crate::event::metrics::RuntimeCompanyMetric::CreditRating, + value: 640, + }], + executable_import_ready: true, + notes: vec!["synthetic governance descriptor test record".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-credit-rating-descriptor", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .event_runtime_records + .first() + .map(|record| record.effects.clone()), + Some(vec![RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::SelectedCompany, + metric: crate::event::metrics::RuntimeCompanyMetric::CreditRating, + value: 640, + }]) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); +} + +#[test] +fn blocks_scalar_locomotive_availability_rows_without_catalog_context() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 31, + live_record_count: 1, + live_entry_ids: vec![31], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 31, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 250, + descriptor_label: Some("Big Boy 4-8-8-4 Availability".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("locomotive_availability_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: 42, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some("Set Big Boy 4-8-8-4 Availability to 42".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: Some(10), + locomotive_name: None, + notes: vec![], + }], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "scalar locomotive availability rows still need catalog context".to_string(), + ], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-recovered-locomotive-availability-frontier", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_locomotive_catalog_context") + ); +} + +#[test] +fn blocks_boolean_locomotive_availability_rows_without_catalog_context() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 32, + live_record_count: 1, + live_entry_ids: vec![32], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 32, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_locomotive_availability_row(250, 1)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "boolean locomotive availability row still needs catalog context".to_string(), + ], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-locomotive-availability-missing-catalog", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_locomotive_catalog_context") + ); +} + +#[test] +fn imports_scalar_locomotive_availability_rows_with_save_derived_catalog_context() { + let 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(), + mechanism_confidence: "grounded".to_string(), + trailer_family: None, + bridge_family: None, + profile: None, + candidate_availability_table: None, + named_locomotive_availability_table: Some(save_named_locomotive_table(61)), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 33, + live_record_count: 1, + live_entry_ids: vec![33], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 33, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![2, 0, 0, 0], + grouped_effect_rows: vec![ + real_locomotive_availability_row(250, 42), + real_locomotive_availability_row(301, 7), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "scalar locomotive availability rows use save-derived catalog context" + .to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "save-derived-locomotive-availability", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.locomotive_catalog.len(), 61); + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("save-derived locomotive availability record should run"); + + assert_eq!( + input + .state + .named_locomotive_availability + .get("Big Boy 4-8-8-4"), + Some(&42) + ); + assert_eq!( + input.state.named_locomotive_availability.get("Zephyr"), + Some(&7) + ); +} + +#[test] +fn overlays_scalar_locomotive_availability_rows_into_named_availability_effects() { + let base_state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 2, + phase_slot: 1, + tick_slot: 3, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: vec![ + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 10, + name: "Big Boy 4-8-8-4".to_string(), + }, + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 61, + name: "Zephyr".to_string(), + }, + ], + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::from([ + ("Big Boy 4-8-8-4".to_string(), 0), + ("Zephyr".to_string(), 1), + ]), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 33, + live_record_count: 1, + live_entry_ids: vec![33], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 33, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![2, 0, 0, 0], + grouped_effect_rows: vec![ + real_locomotive_availability_row(250, 42), + real_locomotive_availability_row(301, 7), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "scalar locomotive availability rows use overlay catalog context".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "overlay-locomotive-availability", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("overlay-imported locomotive availability record should run"); + + assert_eq!( + input + .state + .named_locomotive_availability + .get("Big Boy 4-8-8-4"), + Some(&42) + ); + assert_eq!( + input.state.named_locomotive_availability.get("Zephyr"), + Some(&7) + ); +} + +#[test] +fn blocks_recovered_locomotive_cost_rows_without_catalog_context_lower_band() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 34, + live_record_count: 1, + live_entry_ids: vec![34], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 34, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_locomotive_cost_row(352, 250000)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["scalar locomotive cost row still needs catalog context".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-locomotive-cost-frontier-lower-band", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_locomotive_catalog_context") + ); +} + +#[test] +fn blocks_recovered_locomotive_cost_rows_without_catalog_context() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 35, + live_record_count: 1, + live_entry_ids: vec![35], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 35, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_locomotive_cost_row(352, 250000)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["scalar locomotive cost row still needs catalog context".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-locomotive-cost-missing-catalog", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_locomotive_catalog_context") + ); +} + +#[test] +fn imports_scalar_locomotive_cost_rows_with_save_derived_catalog_context() { + let 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(), + mechanism_confidence: "grounded".to_string(), + trailer_family: None, + bridge_family: None, + profile: None, + candidate_availability_table: None, + named_locomotive_availability_table: Some(save_named_locomotive_table(61)), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 41, + live_record_count: 1, + live_entry_ids: vec![41], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 41, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![2, 0, 0, 0], + grouped_effect_rows: vec![ + real_locomotive_cost_row(352, 250000), + real_locomotive_cost_row(412, 325000), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "scalar locomotive cost rows use save-derived catalog context".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "save-derived-locomotive-cost", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.locomotive_catalog.len(), 61); + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("save-derived locomotive cost record should run"); + + assert_eq!( + input.state.named_locomotive_cost.get("2-D-2"), + Some(&250000) + ); + assert_eq!( + input.state.named_locomotive_cost.get("Zephyr"), + Some(&325000) + ); +} + +#[test] +fn overlays_scalar_locomotive_cost_rows_into_named_cost_effects() { + let base_state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 2, + phase_slot: 1, + tick_slot: 3, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: vec![ + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 1, + name: "2-D-2".to_string(), + }, + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 61, + name: "Zephyr".to_string(), + }, + ], + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::from([ + ("2-D-2".to_string(), 100000), + ("Zephyr".to_string(), 200000), + ]), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 36, + live_record_count: 1, + live_entry_ids: vec![36], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 36, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![2, 0, 0, 0], + grouped_effect_rows: vec![ + real_locomotive_cost_row(352, 250000), + real_locomotive_cost_row(412, 325000), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["scalar locomotive cost rows use overlay catalog context".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "overlay-locomotive-cost", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("overlay-imported locomotive cost record should run"); + + assert_eq!( + input.state.named_locomotive_cost.get("2-D-2"), + Some(&250000) + ); + assert_eq!( + input.state.named_locomotive_cost.get("Zephyr"), + Some(&325000) + ); +} + +#[test] +fn keeps_negative_locomotive_cost_rows_parity_only() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 37, + live_record_count: 1, + live_entry_ids: vec![37], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 37, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_locomotive_cost_row(352, -1)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["negative locomotive cost rows remain parity-only".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-negative-locomotive-cost-frontier", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_variant_or_scope_blocked_descriptor") + ); +} + +#[test] +fn imports_recovered_cargo_production_rows_into_runtime_records() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 35, + live_record_count: 1, + live_entry_ids: vec![35], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 35, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_cargo_production_row(230, 125)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["cargo production rows now input through world overrides".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-cargo-production-frontier", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("cargo production runtime record should run"); + + assert_eq!(input.state.cargo_production_overrides.get(&1), Some(&125)); +} + +#[test] +fn imports_aggregate_cargo_economics_rows_into_runtime_records() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 38, + live_record_count: 1, + live_entry_ids: vec![38], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 38, + payload_offset: Some(0x7202), + payload_len: Some(144), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![4, 0, 0, 0], + grouped_effect_rows: vec![ + real_all_cargo_price_row(180), + real_aggregate_cargo_production_row(177, 210), + real_aggregate_cargo_production_row(178, 225), + real_aggregate_cargo_production_row(179, 175), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "grounded aggregate cargo economics descriptors input through bounded override surfaces" + .to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-aggregate-cargo-economics", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("aggregate cargo economics runtime record should run"); + + assert_eq!(input.state.all_cargo_price_override, Some(180)); + assert_eq!(input.state.all_cargo_production_override, Some(210)); + assert_eq!(input.state.factory_cargo_production_override, Some(225)); + assert_eq!(input.state.farm_mine_cargo_production_override, Some(175)); +} + +#[test] +fn imports_named_cargo_price_rows_when_binding_is_grounded() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 39, + live_record_count: 1, + live_entry_ids: vec![39], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 39, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_named_cargo_price_row(106, 140)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCargoPriceOverride { + target: RuntimeCargoPriceTarget::Named { + name: "Alcohol".to_string(), + }, + value: 140, + }], + executable_import_ready: true, + notes: vec![ + "named cargo price descriptors now input through named cargo overrides" + .to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-named-cargo-price-input", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("named cargo price runtime record should run"); + + assert_eq!( + input.state.named_cargo_price_overrides.get("Alcohol"), + Some(&140) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].grouped_effect_rows.first()) + .and_then(|row| row.descriptor_label.as_deref()), + Some("Alcohol Price") + ); +} + +#[test] +fn imports_named_cargo_production_rows_when_binding_is_grounded() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 40, + live_record_count: 1, + live_entry_ids: vec![40], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 40, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_named_cargo_production_row(180, 160)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::Named { + name: "Alcohol".to_string(), + }, + value: 160, + }], + executable_import_ready: true, + notes: vec![ + "named cargo production descriptors now input through named cargo overrides" + .to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-named-cargo-production-parity", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("named cargo production runtime record should run"); + + assert_eq!( + input.state.named_cargo_production_overrides.get("Alcohol"), + Some(&160) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); +} + +#[test] +fn keeps_negative_all_cargo_price_rows_variant_blocked() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 41, + live_record_count: 1, + live_entry_ids: vec![41], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 41, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_all_cargo_price_row(-1)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "negative aggregate cargo price variants remain parity-only".to_string(), + ], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-negative-all-cargo-price", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_variant_or_scope_blocked_descriptor") + ); +} + +#[test] +fn imports_recovered_territory_access_cost_rows_into_runtime_records() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 36, + live_record_count: 1, + live_entry_ids: vec![36], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 36, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_territory_access_cost_row(750000)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "territory access cost rows now input through world restore".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "packed-events-territory-access-cost-frontier", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("territory access cost runtime record should run"); + + assert_eq!( + input.state.world_restore.territory_access_cost, + Some(750000) + ); +} + +#[test] +fn overlays_real_company_cash_descriptor_into_executable_runtime_record() { + let base_state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 2, + phase_slot: 1, + tick_slot: 3, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 9, + live_record_count: 1, + live_entry_ids: vec![9], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 9, + payload_offset: Some(0x7202), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 2, + descriptor_label: Some("Company Cash".to_string()), + target_mask_bits: Some(0x01), + parameter_family: Some("company_finance_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 8, + raw_scalar_value: 250, + value_byte_0x09: 1, + value_dword_0x0d: 12, + value_byte_0x11: 2, + value_byte_0x12: 3, + value_word_0x14: 24, + value_word_0x16: 36, + row_shape: "multivalue_scalar".to_string(), + semantic_family: Some("multivalue_scalar".to_string()), + semantic_preview: Some( + "Set Company Cash to 250 with aux [2, 3, 24, 36]".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: Some("Mikado".to_string()), + notes: vec![ + "grouped effect row carries locomotive-name side string".to_string(), + ], + }], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::SelectedCompany, + value: 250, + }], + executable_import_ready: true, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-company-cash-overlay", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real company-cash descriptor should execute through the normal trigger path"); + + assert_eq!(input.state.companies[0].current_cash, 250); +} + +#[test] +fn overlays_real_territory_access_descriptor_into_executable_runtime_record() { + let base_state = RuntimeState { + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + territories: vec![crate::state::RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 11, + live_record_count: 1, + live_entry_ids: vec![11], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 11, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 12, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_territory_access_row(true, vec![])], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyTerritoryAccess { + target: RuntimeCompanyTarget::SelectedCompany, + territory: RuntimeTerritoryTarget::Ids { ids: vec![7] }, + value: true, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-territory-access-overlay", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("real territory-access descriptor should execute"); + + assert_eq!( + input.state.company_territory_access, + vec![crate::state::RuntimeCompanyTerritoryAccess { + company_id: 42, + territory_id: 7, + }] + ); +} + +#[test] +fn keeps_real_territory_access_false_row_parity_only() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 12, + live_record_count: 1, + live_entry_ids: vec![12], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 12, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 12, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_territory_access_row(false, vec![])], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = + build_runtime_state_input_from_save_slice(&save_slice, "real-territory-access-false", None) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_territory_access_variant") + ); +} + +#[test] +fn keeps_real_territory_access_missing_scope_row_parity_only() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 13, + live_record_count: 1, + live_entry_ids: vec![13], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 13, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 12, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![9, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_territory_access_row( + true, + vec!["territory access row is missing company or territory scope".to_string()], + )], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "real-territory-access-missing-scope", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_territory_access_scope") + ); +} + +#[test] +fn overlays_real_deactivate_company_descriptor_into_executable_runtime_record() { + let base_state = RuntimeState { + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 13, + live_record_count: 1, + live_entry_ids: vec![13], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 13, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_deactivate_company_row(true)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::DeactivateCompany { + target: RuntimeCompanyTarget::SelectedCompany, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-deactivate-company-overlay", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real deactivate-company descriptor should execute"); + + assert!(!input.state.companies[0].active); + assert_eq!(input.state.selected_company_id, None); +} + +#[test] +fn overlays_real_deactivate_player_descriptor_into_executable_runtime_record() { + let base_state = RuntimeState { + players: vec![ + crate::state::RuntimePlayer { + player_id: 7, + current_cash: 500, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + }, + crate::state::RuntimePlayer { + player_id: 8, + current_cash: 250, + active: true, + controller_kind: RuntimeCompanyControllerKind::Ai, + }, + ], + selected_player_id: Some(7), + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 18, + live_record_count: 1, + live_entry_ids: vec![18], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 18, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 2, + grouped_target_scope_ordinals_0x7fb: vec![0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + }), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_deactivate_player_row(true)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::DeactivatePlayer { + target: RuntimePlayerTarget::ConditionTruePlayer, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-deactivate-player-overlay", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real deactivate-player descriptor should execute"); + + assert!(!input.state.players[0].active); + assert!(input.state.players[1].active); + assert_eq!(input.state.selected_player_id, None); +} + +#[test] +fn keeps_real_deactivate_player_false_row_parity_only() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 19, + live_record_count: 1, + live_entry_ids: vec![19], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 19, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 2, + grouped_target_scope_ordinals_0x7fb: vec![0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + }), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_deactivate_player_row(false)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "real-deactivate-player-false", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_variant_or_scope_blocked_descriptor") + ); +} + +#[test] +fn keeps_real_deactivate_company_false_row_parity_only() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 14, + live_record_count: 1, + live_entry_ids: vec![14], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 14, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_deactivate_company_row(false)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "real-deactivate-company-false", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_variant_or_scope_blocked_descriptor") + ); +} + +#[test] +fn overlays_real_track_capacity_descriptor_into_executable_runtime_record() { + let base_state = RuntimeState { + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 16, + live_record_count: 1, + live_entry_ids: vec![16], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 16, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_track_capacity_row(18)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { + target: RuntimeCompanyTarget::SelectedCompany, + value: Some(18), + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-track-capacity-overlay", + None, + ) + .expect("overlay input should project"); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real track-capacity descriptor should execute"); + + assert_eq!( + input.state.companies[0].available_track_laying_capacity, + Some(18) + ); +} + +#[test] +fn overlays_real_economic_status_descriptor_into_executable_runtime_record() { + let base_state = state(); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 18, + live_record_count: 1, + live_entry_ids: vec![18], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 18, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_economic_status_row(2)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetEconomicStatusCode { value: 2 }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-economic-status-overlay", + None, + ) + .expect("overlay input should project"); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real economic-status descriptor should execute"); + + assert_eq!(input.state.world_restore.economic_status_code, Some(2)); +} + +#[test] +fn imports_real_limited_track_building_amount_descriptor_into_executable_runtime_record() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 52, + live_record_count: 1, + live_entry_ids: vec![52], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 52, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_limited_track_building_amount_row(18)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "real-limited-track-building-amount-save-slice", + None, + ) + .expect("save-slice input should project"); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("real limited-track-building-amount descriptor should execute"); + + assert_eq!( + input.state.world_restore.limited_track_building_amount, + Some(18) + ); +} + +#[test] +fn overlays_real_special_condition_descriptor_into_executable_runtime_record() { + let base_state = state(); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 21, + live_record_count: 1, + live_entry_ids: vec![21], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 21, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_special_condition_row(1)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetSpecialCondition { + label: "Use Wartime Cargos".to_string(), + value: 1, + }], + executable_import_ready: true, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "whole-game descriptor labels and parameter families come from the checked-in effect table".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-special-condition-overlay", + None, + ) + .expect("overlay input should project"); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real special-condition descriptor should execute"); + + assert_eq!( + input.state.special_conditions.get("Use Wartime Cargos"), + Some(&1) + ); +} + +#[test] +fn overlays_real_candidate_availability_descriptor_into_executable_runtime_record() { + let base_state = state(); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 22, + live_record_count: 1, + live_entry_ids: vec![22], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 22, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_candidate_availability_row(1)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCandidateAvailability { + name: "Turbo Diesel".to_string(), + value: 1, + }], + executable_import_ready: true, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "whole-game descriptor labels and parameter families come from the checked-in effect table".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-candidate-availability-overlay", + None, + ) + .expect("overlay input should project"); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real candidate-availability descriptor should execute"); + + assert_eq!( + input.state.candidate_availability.get("Turbo Diesel"), + Some(&1) + ); +} + +#[test] +fn overlays_real_world_flag_descriptor_into_executable_runtime_record() { + let base_state = state(); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 23, + live_record_count: 1, + live_entry_ids: vec![23], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 23, + payload_offset: Some(0x7200), + payload_len: Some(120), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.disable_stock_buying_and_selling".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "world-flag descriptor identity and keyed runtime mapping are checked in" + .to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-world-flag-overlay", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("trigger service should execute"); + assert_eq!( + input + .state + .world_flags + .get("world.disable_stock_buying_and_selling"), + Some(&true) + ); +} + +#[test] +fn overlays_real_world_flag_false_variant_into_executable_runtime_record() { + let base_state = state(); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 24, + live_record_count: 1, + live_entry_ids: vec![24], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 24, + payload_offset: Some(0x7200), + payload_len: Some(120), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + false, + )], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.disable_stock_buying_and_selling".to_string(), + value: false, + }], + executable_import_ready: true, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "world-flag descriptor identity and keyed runtime mapping are checked in" + .to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-world-flag-false-overlay", + None, + ) + .expect("overlay input should project"); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("trigger service should execute"); + assert_eq!( + input + .state + .world_flags + .get("world.disable_stock_buying_and_selling"), + Some(&false) + ); +} + +#[test] +fn overlays_recovered_locomotive_policy_descriptors_into_executable_runtime_record() { + let base_state = state(); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 29, + live_record_count: 1, + live_entry_ids: vec![29], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 29, + payload_offset: Some(0x7200), + payload_len: Some(160), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![3, 0, 0, 0], + grouped_effect_rows: vec![ + real_world_flag_row(454, "All Steam Locos Avail.", true), + real_world_flag_row(455, "All Diesel Locos Avail.", false), + real_world_flag_row(456, "All Electric Locos Avail.", true), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![ + RuntimeEffect::SetWorldFlag { + key: "world.all_steam_locos_available".to_string(), + value: true, + }, + RuntimeEffect::SetWorldFlag { + key: "world.all_diesel_locos_available".to_string(), + value: false, + }, + RuntimeEffect::SetWorldFlag { + key: "world.all_electric_locos_available".to_string(), + value: true, + }, + ], + executable_import_ready: true, + notes: vec![ + "recovered locomotive policy descriptor band now imports as keyed world flags" + .to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-locomotive-policy-overlay", + None, + ) + .expect("overlay input should project"); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("trigger service should execute"); + assert_eq!( + input + .state + .world_flags + .get("world.all_steam_locos_available"), + Some(&true) + ); + assert_eq!( + input + .state + .world_flags + .get("world.all_diesel_locos_available"), + Some(&false) + ); + assert_eq!( + input + .state + .world_flags + .get("world.all_electric_locos_available"), + Some(&true) + ); + assert_eq!( + input + .state + .world_restore + .all_steam_locomotives_available_raw_u8, + Some(1) + ); + assert_eq!( + input + .state + .world_restore + .all_diesel_locomotives_available_raw_u8, + Some(0) + ); + assert_eq!( + input + .state + .world_restore + .all_electric_locomotives_available_raw_u8, + Some(1) + ); +} + +#[test] +fn overlays_real_world_flag_condition_into_executable_runtime_record() { + let mut base_state = state(); + base_state + .world_flags + .insert("world.disable_stock_buying_and_selling".to_string(), true); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 27, + live_record_count: 1, + live_entry_ids: vec![27], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 27, + payload_offset: Some(0x7200), + payload_len: Some(152), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2535, + subtype: 0, + flag_bytes: vec![ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("World Flag: Disable Stock Buying and Selling".to_string()), + semantic_family: Some("world_flag_equals".to_string()), + semantic_preview: Some( + "Test Disable Stock Buying and Selling == TRUE".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec!["checked-in whole-game condition metadata sample".to_string()], + }], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 109, + descriptor_label: Some("Turbo Diesel Availability".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("candidate_availability_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some("Set Turbo Diesel Availability to 1".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec!["checked-in whole-game grouped-effect sample".to_string()], + }], + decoded_conditions: vec![RuntimeCondition::WorldFlagEquals { + key: "world.disable_stock_buying_and_selling".to_string(), + value: true, + }], + decoded_actions: vec![RuntimeEffect::SetCandidateAvailability { + name: "Turbo Diesel".to_string(), + value: 1, + }], + executable_import_ready: true, + notes: vec!["world-flag condition gates a world-side effect".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-world-flag-condition-overlay", + None, + ) + .expect("overlay input should project"); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("trigger service should execute"); + assert_eq!( + input.state.candidate_availability.get("Turbo Diesel"), + Some(&1) + ); +} + +#[test] +fn imports_and_executes_world_scalar_conditions_through_runtime_state() { + let 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(), + 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: Some(save_cargo_catalog(&[ + (1, crate::event::targets::RuntimeCargoClass::Factory), + (5, crate::event::targets::RuntimeCargoClass::FarmMine), + (9, crate::event::targets::RuntimeCargoClass::Other), + ])), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7800, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 45, + live_record_count: 2, + live_entry_ids: vec![41, 45], + decoded_record_count: 2, + imported_runtime_record_count: 2, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![ + SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 41, + payload_offset: Some(0x7200), + payload_len: Some(192), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![7, 0, 0, 0], + grouped_effect_rows: vec![ + real_locomotive_availability_row(250, 42), + real_locomotive_cost_row(352, 250000), + real_cargo_production_row(230, 125), + real_cargo_production_row(234, 75), + real_cargo_production_row(238, 30), + real_limited_track_building_amount_row(18), + real_territory_access_cost_row(750000), + ], + decoded_conditions: vec![], + decoded_actions: vec![ + RuntimeEffect::SetNamedLocomotiveAvailabilityValue { + name: "Big Boy".to_string(), + value: 42, + }, + RuntimeEffect::SetNamedLocomotiveCost { + name: "Locomotive 1".to_string(), + value: 250000, + }, + RuntimeEffect::SetCargoProductionSlot { + slot: 1, + value: 125, + }, + RuntimeEffect::SetCargoProductionSlot { slot: 5, value: 75 }, + RuntimeEffect::SetCargoProductionSlot { slot: 9, value: 30 }, + RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }, + RuntimeEffect::SetTerritoryAccessCost { value: 750000 }, + ], + executable_import_ready: true, + notes: vec!["world-scalar setup record".to_string()], + }, + SmpLoadedPackedEventRecordSummary { + record_index: 1, + live_entry_id: 45, + payload_offset: Some(0x7300), + payload_len: Some(184), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 9, + standalone_condition_rows: vec![ + SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2422, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&42_i32.to_le_bytes()); + bytes + }, + candidate_name: Some("Big Boy".to_string()), + comparator: Some("eq".to_string()), + metric: Some("Named Locomotive Availability: Big Boy".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Named Locomotive Availability: Big Boy == 42".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }, + SmpLoadedPackedEventConditionRowSummary { + row_index: 1, + raw_condition_id: 2423, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&250000_i32.to_le_bytes()); + bytes + }, + candidate_name: Some("Locomotive 1".to_string()), + comparator: Some("eq".to_string()), + metric: Some("Named Locomotive Cost: Locomotive 1".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Named Locomotive Cost: Locomotive 1 == 250000".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }, + SmpLoadedPackedEventConditionRowSummary { + row_index: 2, + raw_condition_id: 200, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&125_i32.to_le_bytes()); + bytes + }, + candidate_name: Some("Cargo Production Slot 1".to_string()), + comparator: Some("eq".to_string()), + metric: Some("Cargo Production: Cargo Production Slot 1".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Cargo Production: Cargo Production Slot 1 == 125".to_string(), + ), + recovered_cargo_slot: Some(1), + recovered_cargo_class: Some("factory".to_string()), + requires_candidate_name_binding: false, + notes: vec![], + }, + SmpLoadedPackedEventConditionRowSummary { + row_index: 3, + raw_condition_id: 2418, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&125_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Cargo Production Total".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Cargo Production Total == 230".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }, + SmpLoadedPackedEventConditionRowSummary { + row_index: 4, + raw_condition_id: 2419, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&125_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Factory Production Total".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Factory Production Total == 125".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: Some("factory".to_string()), + requires_candidate_name_binding: false, + notes: vec![], + }, + SmpLoadedPackedEventConditionRowSummary { + row_index: 5, + raw_condition_id: 2420, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&75_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Farm/Mine Production Total".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Farm/Mine Production Total == 75".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: Some("farm_mine".to_string()), + requires_candidate_name_binding: false, + notes: vec![], + }, + SmpLoadedPackedEventConditionRowSummary { + row_index: 6, + raw_condition_id: 2421, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&30_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Other Cargo Production Total".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Other Cargo Production Total == 30".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: Some("other".to_string()), + requires_candidate_name_binding: false, + notes: vec![], + }, + SmpLoadedPackedEventConditionRowSummary { + row_index: 7, + raw_condition_id: 2547, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&18_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Limited Track Building Amount".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Limited Track Building Amount == 18".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }, + SmpLoadedPackedEventConditionRowSummary { + row_index: 8, + raw_condition_id: 1516, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&750000_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Territory Access Cost".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Territory Access Cost == 750000".to_string(), + ), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }, + ], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: vec![ + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Big Boy".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 42, + }, + RuntimeCondition::NamedLocomotiveCostThreshold { + name: "Locomotive 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 250000, + }, + RuntimeCondition::CargoProductionSlotThreshold { + slot: 1, + label: "Cargo Production Slot 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, + RuntimeCondition::CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 230, + }, + RuntimeCondition::FactoryProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, + RuntimeCondition::FarmMineProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 75, + }, + RuntimeCondition::OtherCargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 30, + }, + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 18, + }, + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 750000, + }, + ], + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.world_scalar_conditions_passed".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec!["world-scalar conditions gate a world-side effect".to_string()], + }, + ], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "world-scalar-condition-save-slice", + None, + ) + .expect("save-slice input should project"); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("setup trigger should execute"); + assert_eq!( + input.state.named_locomotive_availability.get("Big Boy"), + Some(&42) + ); + assert_eq!( + input.state.named_locomotive_cost.get("Locomotive 1"), + Some(&250000) + ); + assert_eq!(input.state.cargo_production_overrides.get(&1), Some(&125)); + assert_eq!(input.state.cargo_production_overrides.get(&5), Some(&75)); + assert_eq!(input.state.cargo_production_overrides.get(&9), Some(&30)); + assert_eq!(input.state.cargo_catalog.len(), 3); + assert_eq!( + input.state.cargo_catalog[0].cargo_class, + crate::event::targets::RuntimeCargoClass::Factory + ); + assert_eq!( + input.state.cargo_catalog[1].cargo_class, + crate::event::targets::RuntimeCargoClass::FarmMine + ); + assert_eq!( + input.state.cargo_catalog[2].cargo_class, + crate::event::targets::RuntimeCargoClass::Other + ); + assert_eq!(input.state.event_runtime_records.len(), 2); + assert_eq!( + input.state.event_runtime_records[1].conditions, + vec![ + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Big Boy".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 42, + }, + RuntimeCondition::NamedLocomotiveCostThreshold { + name: "Locomotive 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 250000, + }, + RuntimeCondition::CargoProductionSlotThreshold { + slot: 1, + label: "Cargo Production Slot 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, + RuntimeCondition::CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 230, + }, + RuntimeCondition::FactoryProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, + RuntimeCondition::FarmMineProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 75, + }, + RuntimeCondition::OtherCargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 30, + }, + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 18, + }, + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 750000, + }, + ] + ); + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("gated trigger should execute"); + + assert_eq!( + input + .state + .world_flags + .get("world.world_scalar_conditions_passed"), + Some(&true) + ); +} + +#[test] +fn overlays_selected_chairman_conditions_into_imported_runtime_records() { + let base_state = RuntimeState { + calendar: CalendarPoint { + year: 1840, + month_slot: 1, + phase_slot: 2, + tick_slot: 3, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(1), + book_value_per_share: 2620, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1839), + merger_cooldown_year: Some(1838), + }], + selected_company_id: Some(1), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![ + crate::state::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 500, + linked_company_id: Some(1), + company_holdings: BTreeMap::new(), + holdings_value_total: 700, + net_worth_total: 1200, + purchasing_power_total: 1500, + }, + crate::state::RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 200, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 400, + net_worth_total: 600, + purchasing_power_total: 800, + }, + ], + selected_chairman_profile_id: Some(1), + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 71, + live_record_count: 1, + live_entry_ids: vec![71], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 71, + payload_offset: Some(0x7202), + payload_len: Some(136), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 1, + standalone_condition_rows: vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2218, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&500_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Player Cash".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Player Cash == 500".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }], + negative_sentinel_scope: Some(selected_chairman_negative_sentinel_scope()), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: vec![RuntimeCondition::ChairmanNumericThreshold { + target: RuntimeChairmanTarget::SelectedChairman, + metric: crate::event::metrics::RuntimeChairmanMetric::CurrentCash, + comparator: RuntimeConditionComparator::Eq, + value: 500, + }], + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.chairman_condition_imported".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec!["chairman metric condition gates a world-side effect".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "chairman-condition-overlay", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input.state.event_runtime_records[0].conditions, + vec![RuntimeCondition::ChairmanNumericThreshold { + target: RuntimeChairmanTarget::SelectedChairman, + metric: crate::event::metrics::RuntimeChairmanMetric::CurrentCash, + comparator: RuntimeConditionComparator::Eq, + value: 500, + }] + ); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("chairman-gated trigger should execute"); + + assert_eq!( + input + .state + .world_flags + .get("world.chairman_condition_imported"), + Some(&true) + ); +} + +#[test] +fn overlays_book_value_conditions_into_imported_runtime_records() { + let base_state = RuntimeState { + calendar: CalendarPoint { + year: 1840, + month_slot: 1, + phase_slot: 2, + tick_slot: 3, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 2620, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1839), + merger_cooldown_year: Some(1838), + }], + selected_company_id: Some(1), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 72, + live_record_count: 1, + live_entry_ids: vec![72], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 72, + payload_offset: Some(0x7202), + payload_len: Some(136), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 1, + standalone_condition_rows: vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2620, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Book Value Per Share".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Book Value Per Share == 2620".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }], + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::SelectedCompanyOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric: crate::event::metrics::RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }], + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.book_value_condition_imported".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec!["book value per share condition gates a world-side effect".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "company-book-value-condition-overlay", + None, + ) + .expect("overlay input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input.state.event_runtime_records[0].conditions, + vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: crate::event::metrics::RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }] + ); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("company-governance trigger should execute"); + + assert_eq!( + input + .state + .world_flags + .get("world.book_value_condition_imported"), + Some(&true) + ); +} + +#[test] +fn imports_investor_confidence_condition_from_save_slice_company_context() { + let 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(), + 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: Some(save_company_roster()), + chairman_profile_table: Some(save_chairman_profile_table()), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 73, + live_record_count: 1, + live_entry_ids: vec![73], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 73, + payload_offset: Some(0x7202), + payload_len: Some(136), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 1, + standalone_condition_rows: vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2366, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&37_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Investor Confidence".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Investor Confidence == 37".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }], + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::SelectedCompanyOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric: crate::event::metrics::RuntimeCompanyMetric::InvestorConfidence, + comparator: RuntimeConditionComparator::Eq, + value: 37, + }], + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.investor_confidence_condition_imported".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec!["investor confidence condition gates a world-side effect".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "company-investor-confidence-condition", + None, + ) + .expect("save-slice input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input.state.event_runtime_records[0].conditions, + vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: crate::event::metrics::RuntimeCompanyMetric::InvestorConfidence, + comparator: RuntimeConditionComparator::Eq, + value: 37, + }] + ); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("investor-confidence trigger should execute"); + + assert_eq!( + input + .state + .world_flags + .get("world.investor_confidence_condition_imported"), + Some(&true) + ); +} + +#[test] +fn imports_management_attitude_condition_from_save_slice_company_context() { + let 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(), + 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: Some(save_company_roster()), + chairman_profile_table: Some(save_chairman_profile_table()), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 74, + live_record_count: 1, + live_entry_ids: vec![74], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 74, + payload_offset: Some(0x7202), + payload_len: Some(136), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 1, + standalone_condition_rows: vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2369, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&58_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Management Attitude".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Management Attitude == 58".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }], + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::SelectedCompanyOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric: crate::event::metrics::RuntimeCompanyMetric::ManagementAttitude, + comparator: RuntimeConditionComparator::Eq, + value: 58, + }], + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.management_attitude_condition_imported".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec!["management attitude condition gates a world-side effect".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_save_slice( + &save_slice, + "company-management-attitude-condition", + None, + ) + .expect("save-slice input should project"); + + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!( + input.state.event_runtime_records[0].conditions, + vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: crate::event::metrics::RuntimeCompanyMetric::ManagementAttitude, + comparator: RuntimeConditionComparator::Eq, + value: 58, + }] + ); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("management-attitude trigger should execute"); + + assert_eq!( + input + .state + .world_flags + .get("world.management_attitude_condition_imported"), + Some(&true) + ); +} + +#[test] +fn overlays_recovered_world_toggle_batch_into_executable_runtime_record() { + let base_state = state(); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 25, + live_record_count: 1, + live_entry_ids: vec![25], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 25, + payload_offset: Some(0x7200), + payload_len: Some(168), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![3, 0, 0, 0], + grouped_effect_rows: vec![ + real_world_flag_row(111, "Disable Margin Buying/Short Selling Stock", true), + real_world_flag_row(120, "Disable All Track Building", true), + real_world_flag_row(131, "Disable Starting Any Companies", false), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![ + RuntimeEffect::SetWorldFlag { + key: "world.disable_margin_buying_short_selling_stock".to_string(), + value: true, + }, + RuntimeEffect::SetWorldFlag { + key: "world.disable_all_track_building".to_string(), + value: true, + }, + RuntimeEffect::SetWorldFlag { + key: "world.disable_starting_any_companies".to_string(), + value: false, + }, + ], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-world-toggle-batch-overlay", + None, + ) + .expect("overlay input should project"); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("trigger service should execute"); + assert_eq!( + input + .state + .world_flags + .get("world.disable_margin_buying_short_selling_stock"), + Some(&true) + ); + assert_eq!( + input + .state + .world_flags + .get("world.disable_all_track_building"), + Some(&true) + ); + assert_eq!( + input + .state + .world_flags + .get("world.disable_starting_any_companies"), + Some(&false) + ); +} + +#[test] +fn overlays_recovered_late_world_toggle_batch_into_executable_runtime_record() { + let base_state = state(); + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 26, + live_record_count: 1, + live_entry_ids: vec![26], + decoded_record_count: 1, + imported_runtime_record_count: 1, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 26, + payload_offset: Some(0x7200), + payload_len: Some(184), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![5, 0, 0, 0], + grouped_effect_rows: vec![ + real_world_flag_row(139, "Use Bio-Accelerator Cars", true), + real_world_flag_row(140, "Disable Cargo Economy", true), + real_world_flag_row(142, "Disable Train Crashes", false), + real_world_flag_row(143, "Disable Train Crashes AND Breakdowns", true), + real_world_flag_row(144, "AI Ignore Territories At Startup", true), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![ + RuntimeEffect::SetWorldFlag { + key: "world.use_bio_accelerator_cars".to_string(), + value: true, + }, + RuntimeEffect::SetWorldFlag { + key: "world.disable_cargo_economy".to_string(), + value: true, + }, + RuntimeEffect::SetWorldFlag { + key: "world.disable_train_crashes".to_string(), + value: false, + }, + RuntimeEffect::SetWorldFlag { + key: "world.disable_train_crashes_and_breakdowns".to_string(), + value: true, + }, + RuntimeEffect::SetWorldFlag { + key: "world.ai_ignore_territories_at_startup".to_string(), + value: true, + }, + ], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-late-world-toggle-batch-overlay", + None, + ) + .expect("overlay input should project"); + + crate::execute_step_command( + &mut input.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("trigger service should execute"); + assert_eq!( + input + .state + .world_flags + .get("world.use_bio_accelerator_cars"), + Some(&true) + ); + assert_eq!( + input.state.world_flags.get("world.disable_cargo_economy"), + Some(&true) + ); + assert_eq!( + input.state.world_flags.get("world.disable_train_crashes"), + Some(&false) + ); + assert_eq!( + input + .state + .world_flags + .get("world.disable_train_crashes_and_breakdowns"), + Some(&true) + ); + assert_eq!( + input + .state + .world_flags + .get("world.ai_ignore_territories_at_startup"), + Some(&true) + ); +} + +#[test] +fn overlays_real_confiscate_all_descriptor_into_executable_runtime_record() { + let base_state = RuntimeState { + companies: vec![ + crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + crate::state::RuntimeCompany { + company_id: 7, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 90, + debt: 10, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + selected_company_id: Some(42), + trains: vec![ + RuntimeTrain { + train_id: 1, + owner_company_id: 42, + territory_id: None, + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 2, + owner_company_id: 7, + territory_id: None, + locomotive_name: Some("Orca".to_string()), + active: true, + retired: false, + }, + ], + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 19, + live_record_count: 1, + live_entry_ids: vec![19], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 19, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_confiscate_all_row(true)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::ConfiscateCompanyAssets { + target: RuntimeCompanyTarget::SelectedCompany, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-confiscate-all-overlay", + None, + ) + .expect("overlay input should project"); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real confiscate-all descriptor should execute"); + + assert_eq!(input.state.companies[0].current_cash, 0); + assert_eq!(input.state.companies[0].debt, 0); + assert!(!input.state.companies[0].active); + assert_eq!(input.state.selected_company_id, None); + assert!(!input.state.trains[0].active); + assert!(input.state.trains[0].retired); + assert!(input.state.trains[1].active); + assert!(!input.state.trains[1].retired); +} + +#[test] +fn keeps_real_confiscate_all_false_row_parity_only() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 20, + live_record_count: 1, + live_entry_ids: vec![20], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 20, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_confiscate_all_row(false)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = + build_runtime_state_input_from_save_slice(&save_slice, "real-confiscate-all-false", None) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_confiscation_variant") + ); +} + +#[test] +fn blocks_confiscate_all_without_train_context() { + let base_state = RuntimeState { + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 24, + live_record_count: 1, + live_entry_ids: vec![24], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 24, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_confiscate_all_row(true)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::ConfiscateCompanyAssets { + target: RuntimeCompanyTarget::SelectedCompany, + }], + executable_import_ready: true, + notes: vec![], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-confiscate-all-missing-trains", + None, + ) + .expect("overlay input should project"); + + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_train_context") + ); + assert!(input.state.event_runtime_records.is_empty()); +} + +#[test] +fn overlays_real_retire_train_descriptor_by_company_scope() { + let base_state = RuntimeState { + companies: vec![ + crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + crate::state::RuntimeCompany { + company_id: 7, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 90, + debt: 10, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + selected_company_id: Some(42), + trains: vec![ + RuntimeTrain { + train_id: 1, + owner_company_id: 42, + territory_id: Some(7), + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 2, + owner_company_id: 42, + territory_id: Some(8), + locomotive_name: Some("Orca".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 3, + owner_company_id: 7, + territory_id: Some(7), + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + ], + territories: vec![ + crate::state::RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + crate::state::RuntimeTerritory { + territory_id: 8, + name: Some("Great Plains".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + ], + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 21, + live_record_count: 1, + live_entry_ids: vec![21], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 21, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_retire_train_row(true, None, vec![])], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::RetireTrains { + company_target: Some(RuntimeCompanyTarget::SelectedCompany), + territory_target: None, + locomotive_name: None, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-retire-train-company-overlay", + None, + ) + .expect("overlay input should project"); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real retire-train descriptor should execute"); + + assert!(input.state.trains[0].retired); + assert!(input.state.trains[1].retired); + assert!(!input.state.trains[2].retired); +} + +#[test] +fn overlays_real_retire_train_descriptor_by_territory_and_locomotive_scope() { + let base_state = RuntimeState { + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + trains: vec![ + RuntimeTrain { + train_id: 1, + owner_company_id: 42, + territory_id: Some(7), + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 2, + owner_company_id: 42, + territory_id: Some(7), + locomotive_name: Some("Orca".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 3, + owner_company_id: 42, + territory_id: Some(8), + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + ], + territories: vec![ + crate::state::RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + crate::state::RuntimeTerritory { + territory_id: 8, + name: Some("Great Plains".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + ], + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 22, + live_record_count: 1, + live_entry_ids: vec![22], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 22, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![8, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_retire_train_row(true, Some("Mikado"), vec![])], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::RetireTrains { + company_target: None, + territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }), + locomotive_name: Some("Mikado".to_string()), + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-retire-train-territory-overlay", + None, + ) + .expect("overlay input should project"); + + execute_step_command( + &mut input.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("territory-scoped retire-train descriptor should execute"); + + assert!(input.state.trains[0].retired); + assert!(!input.state.trains[1].retired); + assert!(!input.state.trains[2].retired); +} + +#[test] +fn keeps_real_retire_train_missing_scope_parity_only() { + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 23, + live_record_count: 1, + live_entry_ids: vec![23], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 23, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![8, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_retire_train_row( + true, + Some("Mikado"), + vec!["retire train row is missing company and territory scope".to_string()], + )], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "real-retire-train-missing-scope", + None, + ) + .expect("save slice should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_retire_train_scope") + ); +} + +#[test] +fn blocks_retire_train_without_train_territory_context() { + let base_state = RuntimeState { + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + trains: vec![RuntimeTrain { + train_id: 1, + owner_company_id: 42, + territory_id: None, + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }], + territories: vec![crate::state::RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 25, + live_record_count: 1, + live_entry_ids: vec![25], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 25, + payload_offset: Some(0x7202), + payload_len: Some(120), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![8, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_retire_train_row(true, Some("Mikado"), vec![])], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::RetireTrains { + company_target: None, + territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }), + locomotive_name: Some("Mikado".to_string()), + }], + executable_import_ready: true, + notes: vec![], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "real-retire-train-missing-train-territory", + None, + ) + .expect("overlay input should project"); + + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_missing_train_territory_context") + ); + assert!(input.state.event_runtime_records.is_empty()); +} + +#[test] +fn keeps_mixed_real_records_out_of_event_runtime_records() { + let base_state = RuntimeState { + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + ..state() + }; + let 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 17, + live_record_count: 1, + live_entry_ids: vec![17], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 17, + payload_offset: Some(0x7202), + payload_len: Some(160), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 1, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 1, 0, 0], + grouped_effect_rows: vec![ + real_track_capacity_row(18), + unsupported_real_grouped_row(), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { + target: RuntimeCompanyTarget::SelectedCompany, + value: Some(18), + }], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let input = build_runtime_state_input_from_overlay( + &base_state, + &save_slice, + "mixed-real-record-overlay", + None, + ) + .expect("overlay input should project"); + + assert!(input.state.event_runtime_records.is_empty()); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_confiscation_variant") + ); +} diff --git a/crates/rrt-runtime/src/documents/tests/profile_world.rs b/crates/rrt-runtime/src/documents/tests/profile_world.rs new file mode 100644 index 0000000..b6c16c2 --- /dev/null +++ b/crates/rrt-runtime/src/documents/tests/profile_world.rs @@ -0,0 +1,223 @@ +use super::*; + +#[test] +fn projects_company_and_chairman_context_from_save_slice() { + let 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(), + 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: Some(save_company_roster()), + chairman_profile_table: Some(save_chairman_profile_table()), + 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![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "save-native-company-chairman", + None, + ) + .expect("save slice should project"); + + assert_eq!(input.state.companies.len(), 2); + assert_eq!(input.state.selected_company_id, Some(1)); + assert_eq!(input.state.chairman_profiles.len(), 2); + assert_eq!(input.state.selected_chairman_profile_id, Some(1)); + assert_eq!(input.state.companies[0].book_value_per_share, 2620); + assert_eq!(input.state.chairman_profiles[0].current_cash, 500); + assert_eq!(input.state.service_state.company_market_state.len(), 2); + assert_eq!( + input + .state + .service_state + .company_market_state + .get(&1) + .map(|state| state.cached_share_price_raw_u32), + Some(0x42200000) + ); +} + +#[test] +fn projects_placed_structure_collection_metadata_from_save_slice() { + let 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(), + 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: Some(save_region_collection()), + region_fixed_row_run_summary: Some(save_region_fixed_row_run_summary()), + placed_structure_collection: Some(save_placed_structure_collection()), + placed_structure_dynamic_side_buffer_summary: Some( + save_placed_structure_dynamic_side_buffer_summary(), + ), + special_conditions_table: None, + event_runtime_collection: None, + notes: vec![], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "save-native-placed-structures", + None, + ) + .expect("save slice should project"); + + assert_eq!( + input + .state + .metadata + .get("save_slice.region_collection_source_kind") + .map(String::as_str), + Some("save-region-record-triplets") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.region_collection_entry_count") + .map(String::as_str), + Some("2") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.region_collection_profile_entry_count") + .map(String::as_str), + Some("3") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.region_collection_nonzero_prefix_count") + .map(String::as_str), + Some("1") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.region_collection_nonzero_reserved_policy_count") + .map(String::as_str), + Some("1") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.region_fixed_row_run_source_kind") + .map(String::as_str), + Some("save-region-fixed-row-run-candidates") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.region_fixed_row_run_candidate_count") + .map(String::as_str), + Some("1") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.region_fixed_row_run_best_rows_offset_hex") + .map(String::as_str), + Some("0x5310") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.region_fixed_row_run_best_shape_signature") + .map(String::as_str), + Some("dword0:f32,dword1:zero") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.placed_structure_collection_source_kind") + .map(String::as_str), + Some("save-placed-structure-record-triplets") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.placed_structure_collection_entry_count") + .map(String::as_str), + Some("2") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.placed_structure_collection_farm_growth_stage_count") + .map(String::as_str), + Some("1") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.placed_structure_collection_nondefault_status_count") + .map(String::as_str), + Some("2") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.placed_structure_dynamic_side_buffer_source_kind") + .map(String::as_str), + Some("save-placed-structure-dynamic-side-buffer-records") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.placed_structure_dynamic_side_buffer_name_pair_count") + .map(String::as_str), + Some("9") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.placed_structure_dynamic_side_buffer_triplet_overlap_count") + .map(String::as_str), + Some("2") + ); +} diff --git a/crates/rrt-runtime/src/documents/tests/projection.rs b/crates/rrt-runtime/src/documents/tests/projection.rs new file mode 100644 index 0000000..b67a17c --- /dev/null +++ b/crates/rrt-runtime/src/documents/tests/projection.rs @@ -0,0 +1,813 @@ +use super::*; + +#[test] +fn projects_save_slice_into_runtime_state_import() { + let save_slice = SmpLoadedSaveSlice { + file_extension_hint: Some("gms".to_string()), + container_profile_family: Some("rt3-105-save-container-v1".to_string()), + mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), + mechanism_confidence: "mixed".to_string(), + trailer_family: Some("rt3-105-save-trailer-v1".to_string()), + bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), + profile: Some(SmpLoadedProfile { + profile_kind: "rt3-105-packed-profile".to_string(), + profile_family: "rt3-105-save-container-v1".to_string(), + packed_profile_offset: 0x73c0, + packed_profile_len: 0x108, + packed_profile_len_hex: "0x108".to_string(), + leading_word_0: 3, + leading_word_0_hex: "0x00000003".to_string(), + header_flag_word_3: Some(0x01000000), + header_flag_word_3_hex: Some("0x01000000".to_string()), + map_path: Some("Alternate USA.gmp".to_string()), + 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(), + }), + candidate_availability_table: Some(SmpLoadedCandidateAvailabilityTable { + source_kind: "save-bridge-secondary-block".to_string(), + semantic_family: "scenario-named-candidate-availability-table".to_string(), + header_offset: 0x6a70, + entries_offset: 0x6ad1, + entries_end_offset: 0x73b7, + observed_entry_count: 2, + zero_availability_count: 1, + zero_availability_names: vec!["Uranium Mine".to_string()], + footer_progress_hex_words: vec!["0x000032dc".to_string(), "0x00003714".to_string()], + entries: vec![ + SmpRt3105SaveNameTableEntry { + index: 0, + offset: 0x6ad1, + text: "AutoPlant".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + SmpRt3105SaveNameTableEntry { + index: 1, + offset: 0x6af3, + text: "Uranium Mine".to_string(), + availability_dword: 0, + availability_dword_hex: "0x00000000".to_string(), + trailer_word: 0, + trailer_word_hex: "0x00000000".to_string(), + }, + ], + }), + named_locomotive_availability_table: Some(SmpLoadedNamedLocomotiveAvailabilityTable { + source_kind: "runtime-save-direct-serializer".to_string(), + semantic_family: "scenario-named-locomotive-availability-table".to_string(), + header_offset: None, + entries_offset: None, + entries_end_offset: None, + observed_entry_count: 2, + zero_availability_count: 1, + zero_availability_names: vec!["Big Boy".to_string()], + entries: vec![ + SmpRt3105SaveNameTableEntry { + index: 0, + offset: 0, + text: "Big Boy".to_string(), + availability_dword: 0, + availability_dword_hex: "0x00000000".to_string(), + trailer_word: 0, + trailer_word_hex: "0x00000000".to_string(), + }, + SmpRt3105SaveNameTableEntry { + index: 1, + offset: 0x41, + text: "GP7".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + ], + }), + locomotive_catalog: None, + cargo_catalog: Some(save_cargo_catalog(&[ + (1, crate::event::targets::RuntimeCargoClass::Factory), + (5, crate::event::targets::RuntimeCargoClass::FarmMine), + (9, crate::event::targets::RuntimeCargoClass::Other), + ])), + world_issue_37_state: Some(SmpLoadedWorldIssue37State { + source_kind: "save-fixed-world-block".to_string(), + semantic_family: "world-issue-0x37".to_string(), + issue_value: 3, + issue_value_hex: "0x00000003".to_string(), + issue_38_value: 1, + issue_38_value_hex: "0x01".to_string(), + issue_39_value: 2, + issue_39_value_hex: "0x02".to_string(), + issue_3a_value: 4, + issue_3a_value_hex: "0x04".to_string(), + multiplier_raw_u32: 0x3d75c28f, + multiplier_raw_hex: "0x3d75c28f".to_string(), + multiplier_value_f32_text: "0.060000".to_string(), + issue_opinion_base_terms_raw_i32: Vec::new(), + }), + world_economic_tuning_state: Some(SmpLoadedWorldEconomicTuningState { + source_kind: "save-fixed-world-block".to_string(), + semantic_family: "world-economic-tuning".to_string(), + mirror_raw_u32: 0x3f46d093, + mirror_raw_hex: "0x3f46d093".to_string(), + mirror_value_f32_text: "0.776620".to_string(), + lane_raw_u32: vec![0x3f400000, 0x3be56042], + lane_raw_hex: vec!["0x3f400000".to_string(), "0x3be56042".to_string()], + lane_value_f32_text: vec!["0.750000".to_string(), "0.007000".to_string()], + }), + world_finance_neighborhood_state: Some(SmpLoadedWorldFinanceNeighborhoodState { + source_kind: "save-fixed-world-block".to_string(), + semantic_family: "world-finance-neighborhood".to_string(), + packed_year_word_raw_u16: 0x0201, + packed_year_word_raw_hex: "0x0201".to_string(), + partial_year_progress_raw_u8: 3, + partial_year_progress_raw_hex: "0x03".to_string(), + current_calendar_tuple_word_raw_u32: 1, + current_calendar_tuple_word_raw_hex: "0x00000001".to_string(), + current_calendar_tuple_word_2_raw_u32: 2, + current_calendar_tuple_word_2_raw_hex: "0x00000002".to_string(), + absolute_counter_raw_u32: 3, + absolute_counter_raw_hex: "0x00000003".to_string(), + absolute_counter_mirror_raw_u32: 4, + absolute_counter_mirror_raw_hex: "0x00000004".to_string(), + stock_policy_raw_u8: 0, + stock_policy_raw_hex: "0x00".to_string(), + bond_policy_raw_u8: 1, + bond_policy_raw_hex: "0x01".to_string(), + bankruptcy_policy_raw_u8: 0, + bankruptcy_policy_raw_hex: "0x00".to_string(), + dividend_policy_raw_u8: 1, + dividend_policy_raw_hex: "0x01".to_string(), + building_density_growth_setting_raw_u32: 1, + building_density_growth_setting_raw_hex: "0x00000001".to_string(), + labels: vec![ + "current_calendar_tuple_word".to_string(), + "current_calendar_tuple_word_2".to_string(), + "absolute_calendar_counter".to_string(), + "absolute_calendar_counter_mirror".to_string(), + ], + relative_offsets: vec![0x0d, 0x11, 0x15, 0x19], + relative_offset_hex: vec![ + "0xd".to_string(), + "0x11".to_string(), + "0x15".to_string(), + "0x19".to_string(), + ], + raw_u32: vec![1, 2, 3, 4], + raw_hex: vec![ + "0x00000001".to_string(), + "0x00000002".to_string(), + "0x00000003".to_string(), + "0x00000004".to_string(), + ], + value_i32: vec![1, 2, 3, 4], + value_f32_text: vec![ + "0.000000".to_string(), + "0.000000".to_string(), + "0.000000".to_string(), + "0.000000".to_string(), + ], + }), + world_locomotive_policy_state: Some(SmpLoadedWorldLocomotivePolicyState { + source_kind: "save-fixed-world-block".to_string(), + semantic_family: "world-locomotive-policy".to_string(), + selected_year_gap_scalar_raw_u32: Some(0x3eaaaaab), + selected_year_gap_scalar_raw_hex: Some("0x3eaaaaab".to_string()), + selected_year_gap_scalar_value_f32_text: Some("0.333333".to_string()), + linked_site_removal_follow_on_gate_raw_u8: Some(1), + linked_site_removal_follow_on_gate_raw_hex: Some("0x01".to_string()), + auto_show_grade_during_track_lay_raw_u8: Some(2), + auto_show_grade_during_track_lay_raw_hex: Some("0x02".to_string()), + starting_building_density_level_raw_u8: Some(3), + starting_building_density_level_raw_hex: Some("0x03".to_string()), + building_density_growth_raw_u8: Some(1), + building_density_growth_raw_hex: Some("0x01".to_string()), + leftover_simulation_time_accumulator_raw_u32: Some(0x3f000000), + leftover_simulation_time_accumulator_raw_hex: Some("0x3f000000".to_string()), + leftover_simulation_time_accumulator_value_f32_text: Some("0.500000".to_string()), + selected_year_lane_snapshot_raw_u8: Some(7), + selected_year_lane_snapshot_raw_hex: Some("0x07".to_string()), + all_steam_locomotives_available_raw_u8: Some(1), + all_steam_locomotives_available_raw_hex: Some("0x01".to_string()), + all_diesel_locomotives_available_raw_u8: Some(0), + all_diesel_locomotives_available_raw_hex: Some("0x00".to_string()), + all_electric_locomotives_available_raw_u8: Some(1), + all_electric_locomotives_available_raw_hex: Some("0x01".to_string()), + cached_available_locomotive_rating_raw_u32: Some(0x41a00000), + cached_available_locomotive_rating_raw_hex: Some("0x41a00000".to_string()), + cached_available_locomotive_rating_value_f32_text: Some("20.000000".to_string()), + }), + 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: Some(SmpLoadedSpecialConditionsTable { + source_kind: "save-fixed-special-conditions-range".to_string(), + table_offset: 0x0d64, + table_len: 36 * 4, + enabled_visible_count: 0, + enabled_visible_labels: vec![], + entries: vec![ + SmpSpecialConditionEntry { + slot_index: 30, + hidden: false, + label_id: 3722, + help_id: 3723, + label: "Disable Cargo Economy".to_string(), + value: 0, + value_hex: "0x00000000".to_string(), + }, + SmpSpecialConditionEntry { + slot_index: 35, + hidden: true, + label_id: 3, + help_id: 3, + label: "Hidden sentinel".to_string(), + value: 1, + value_hex: "0x00000001".to_string(), + }, + ], + }), + event_runtime_collection: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), + mechanism_confidence: "mixed".to_string(), + container_profile_family: Some("rt3-105-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 5, + live_record_count: 3, + live_entry_ids: vec![1, 3, 5], + decoded_record_count: 0, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![ + SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 1, + payload_offset: None, + payload_len: None, + decode_status: "unsupported_framing".to_string(), + payload_family: "unsupported_framing".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + notes: vec!["test".to_string()], + }, + SmpLoadedPackedEventRecordSummary { + record_index: 1, + live_entry_id: 3, + payload_offset: None, + payload_len: None, + decode_status: "unsupported_framing".to_string(), + payload_family: "unsupported_framing".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + notes: vec!["test".to_string()], + }, + SmpLoadedPackedEventRecordSummary { + record_index: 2, + live_entry_id: 5, + payload_offset: None, + payload_len: None, + decode_status: "unsupported_framing".to_string(), + payload_family: "unsupported_framing".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + notes: vec!["test".to_string()], + }, + ], + }), + notes: vec!["packed profile recovered".to_string()], + }; + + let input = build_runtime_state_input_from_save_slice( + &save_slice, + "save-input-smoke", + Some("test save input".to_string()), + ) + .expect("save slice should project"); + + assert_eq!(input.input_id, "save-input-smoke"); + assert_eq!( + input + .state + .metadata + .get("save_slice.map_path") + .map(String::as_str), + Some("Alternate USA.gmp") + ); + assert_eq!( + input.state.save_profile.selected_year_profile_lane, + Some(0x07) + ); + assert_eq!(input.state.save_profile.sandbox_enabled, Some(true)); + assert_eq!( + input.state.world_restore.selected_year_profile_lane, + Some(0x07) + ); + assert_eq!(input.state.world_restore.sandbox_enabled, Some(true)); + assert_eq!( + input.state.world_restore.campaign_scenario_enabled, + Some(false) + ); + assert_eq!( + input.state.world_restore.seed_tuple_written_from_raw_lane, + Some(true) + ); + assert_eq!( + input + .state + .world_restore + .absolute_counter_requires_shell_context, + Some(false) + ); + assert_eq!( + input + .state + .world_restore + .absolute_counter_reconstructible_from_save, + Some(true) + ); + assert_eq!( + input + .state + .world_restore + .auto_show_grade_during_track_lay_raw_u8, + Some(2) + ); + assert_eq!( + input + .state + .world_restore + .starting_building_density_level_raw_u8, + Some(3) + ); + assert_eq!( + input + .state + .world_restore + .post_text_building_density_growth_raw_u8, + Some(1) + ); + assert_eq!( + input + .state + .world_restore + .leftover_simulation_time_accumulator_raw_u32, + Some(0x3f000000) + ); + assert_eq!( + input + .state + .world_restore + .leftover_simulation_time_accumulator_value_f32_text + .as_deref(), + Some("0.500000") + ); + assert_eq!( + input.state.world_restore.selected_year_lane_snapshot_raw_u8, + Some(7) + ); + assert_eq!( + input.state.world_restore.packed_year_word_raw_u16, + Some(0x0201) + ); + assert_eq!( + input.state.world_restore.partial_year_progress_raw_u8, + Some(3) + ); + assert_eq!( + input + .state + .world_restore + .current_calendar_tuple_word_raw_u32, + Some(1) + ); + assert_eq!( + input + .state + .world_restore + .current_calendar_tuple_word_2_raw_u32, + Some(2) + ); + assert_eq!(input.state.world_restore.absolute_counter_raw_u32, Some(3)); + assert_eq!( + input.state.world_restore.absolute_counter_mirror_raw_u32, + Some(4) + ); + assert_eq!( + input + .state + .world_restore + .disable_cargo_economy_special_condition_slot, + Some(30) + ); + assert_eq!( + input + .state + .world_restore + .disable_cargo_economy_special_condition_reconstructible_from_save, + Some(true) + ); + assert_eq!( + input + .state + .world_restore + .disable_cargo_economy_special_condition_write_side_grounded, + Some(true) + ); + assert_eq!( + input + .state + .world_restore + .disable_cargo_economy_special_condition_enabled, + Some(false) + ); + assert_eq!( + input.state.world_restore.use_bio_accelerator_cars_enabled, + Some(false) + ); + assert_eq!( + input.state.world_restore.use_wartime_cargos_enabled, + Some(false) + ); + assert_eq!( + input.state.world_restore.disable_train_crashes_enabled, + Some(false) + ); + assert_eq!( + input + .state + .world_restore + .disable_train_crashes_and_breakdowns_enabled, + Some(false) + ); + assert_eq!( + input + .state + .world_restore + .ai_ignore_territories_at_startup_enabled, + Some(false) + ); + assert_eq!(input.state.world_restore.issue_37_value, Some(3)); + assert_eq!(input.state.world_restore.issue_38_value, Some(1)); + assert_eq!(input.state.world_restore.issue_39_value, Some(2)); + assert_eq!(input.state.world_restore.issue_3a_value, Some(4)); + assert_eq!( + input.state.world_restore.issue_37_multiplier_raw_u32, + Some(0x3d75c28f) + ); + assert_eq!( + input + .state + .world_restore + .stock_issue_and_buyback_policy_raw_u8, + Some(0) + ); + assert_eq!( + input + .state + .world_restore + .bond_issue_and_repayment_policy_raw_u8, + Some(1) + ); + assert_eq!(input.state.world_restore.bankruptcy_policy_raw_u8, Some(0)); + assert_eq!(input.state.world_restore.dividend_policy_raw_u8, Some(1)); + assert_eq!( + input.state.world_restore.stock_issue_and_buyback_allowed, + Some(true) + ); + assert_eq!( + input.state.world_restore.bond_issue_and_repayment_allowed, + Some(false) + ); + assert_eq!(input.state.world_restore.bankruptcy_allowed, Some(true)); + assert_eq!( + input.state.world_restore.dividend_adjustment_allowed, + Some(false) + ); + assert_eq!( + input + .state + .world_restore + .issue_37_multiplier_value_f32_text + .as_deref(), + Some("0.060000") + ); + assert_eq!( + input.state.world_restore.economic_tuning_mirror_raw_u32, + Some(0x3f400000) + ); + assert_eq!( + input + .state + .world_restore + .economic_tuning_mirror_value_f32_text + .as_deref(), + Some("0.750000") + ); + assert_eq!( + input.state.world_restore.economic_tuning_lane_raw_u32, + vec![0x3f400000, 0x3be56042] + ); + assert_eq!( + input + .state + .world_restore + .economic_tuning_lane_value_f32_text, + vec!["0.750000".to_string(), "0.007000".to_string()] + ); + assert_eq!( + input + .state + .world_restore + .linked_site_removal_follow_on_gate_raw_u8, + Some(1) + ); + assert_eq!( + input + .state + .world_restore + .all_steam_locomotives_available_enabled, + Some(true) + ); + assert_eq!( + input + .state + .world_restore + .all_diesel_locomotives_available_enabled, + Some(false) + ); + assert_eq!( + input + .state + .world_restore + .all_electric_locomotives_available_enabled, + Some(true) + ); + assert_eq!( + input + .state + .world_restore + .cached_available_locomotive_rating_raw_u32, + Some(0x41a00000) + ); + assert_eq!( + input + .state + .world_restore + .absolute_counter_restore_kind + .as_deref(), + Some("save-direct-world-absolute-counter") + ); + assert_eq!( + input + .state + .world_restore + .absolute_counter_adjustment_context + .as_deref(), + Some("save-direct-world-block-0x32c8") + ); + assert_eq!( + input.state.save_profile.map_path.as_deref(), + Some("Alternate USA.gmp") + ); + assert_eq!( + input.state.candidate_availability.get("Uranium Mine"), + Some(&0) + ); + assert_eq!( + input.state.named_locomotive_availability.get("Big Boy"), + Some(&0) + ); + assert_eq!( + input.state.named_locomotive_availability.get("GP7"), + Some(&1) + ); + assert_eq!(input.state.locomotive_catalog.len(), 2); + assert_eq!(input.state.locomotive_catalog[0].locomotive_id, 1); + assert_eq!(input.state.locomotive_catalog[0].name, "Big Boy"); + assert_eq!(input.state.locomotive_catalog[1].locomotive_id, 2); + assert_eq!(input.state.locomotive_catalog[1].name, "GP7"); + assert_eq!( + input.state.special_conditions.get("Disable Cargo Economy"), + Some(&0) + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.world_issue_37_value") + .map(String::as_str), + Some("3") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.world_issue_39_value") + .map(String::as_str), + Some("2") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.world_economic_tuning_lane_count") + .map(String::as_str), + Some("2") + ); + assert_eq!( + input + .state + .world_flags + .get("save_slice.world_issue_37_state_present"), + Some(&true) + ); + assert_eq!( + input + .state + .world_flags + .get("save_slice.world_economic_tuning_state_present"), + Some(&true) + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.named_locomotive_availability_source_kind") + .map(String::as_str), + Some("runtime-save-direct-serializer") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.named_locomotive_availability_entry_count") + .map(String::as_str), + Some("2") + ); + assert_eq!( + input + .state + .metadata + .get("save_slice.locomotive_catalog_source_kind") + .map(String::as_str), + Some("derived-from-named-locomotive-availability-table") + ); + assert_eq!( + input + .state + .world_flags + .get("save_slice.profile_byte_0x82_nonzero"), + Some(&true) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .map(|summary| summary.live_record_count), + Some(3) + ); + assert_eq!( + input + .state + .packed_event_collection + .as_ref() + .map(|summary| summary.live_entry_ids.clone()), + Some(vec![1, 3, 5]) + ); + assert!(input.state.event_runtime_records.is_empty()); +} + +#[test] +fn projects_near_city_acquisition_runtime_slice_from_save_collections() { + let save_slice = SmpLoadedSaveSlice { + file_extension_hint: Some("gms".to_string()), + container_profile_family: Some("rt3-105-save-container-v1".to_string()), + mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), + mechanism_confidence: "mixed".to_string(), + trailer_family: Some("rt3-105-save-trailer-v1".to_string()), + bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), + 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: Some(save_company_roster()), + chairman_profile_table: Some(save_chairman_profile_table()), + region_collection: Some(save_region_collection()), + region_fixed_row_run_summary: Some(save_region_fixed_row_run_summary()), + placed_structure_collection: Some(save_placed_structure_collection()), + placed_structure_dynamic_side_buffer_summary: Some( + save_placed_structure_dynamic_side_buffer_summary(), + ), + special_conditions_table: None, + event_runtime_collection: None, + notes: vec![], + }; + + let input = + build_runtime_state_input_from_save_slice(&save_slice, "near-city-projection", None) + .expect("save slice projection should succeed"); + + assert_eq!( + input + .state + .service_state + .near_city_acquisition_regions + .len(), + 2 + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites.len(), + 2 + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites[0].region_id, + Some(0) + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites[0].owner_company_id, + Some(1) + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites[0].candidate_subtype_label, + "farm" + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites[0].cached_tri_lane_0, + 0xff0000ff + ); + assert_eq!( + input.state.service_state.near_city_acquisition_sites[0].cached_tri_lane_2, + 0x24 + ); +} diff --git a/crates/rrt-runtime/src/documents/tests/roundtrip.rs b/crates/rrt-runtime/src/documents/tests/roundtrip.rs new file mode 100644 index 0000000..4baf1a6 --- /dev/null +++ b/crates/rrt-runtime/src/documents/tests/roundtrip.rs @@ -0,0 +1,129 @@ +use super::*; +use crate::test_support::write_temp_json; + +#[test] +fn loads_input_document() { + let text = serde_json::to_string(&RuntimeStateInputDocument { + format_version: STATE_INPUT_DOCUMENT_FORMAT_VERSION, + input_id: "input-smoke".to_string(), + source: RuntimeStateInputSource { + description: Some("test input".to_string()), + source_binary: None, + }, + state: state(), + }) + .expect("input document should serialize"); + + let input = + load_runtime_state_input_from_str(&text, "fallback").expect("input document should load"); + assert_eq!(input.input_id, "input-smoke"); + assert_eq!(input.description.as_deref(), Some("test input")); +} + +#[test] +fn loads_bare_runtime_state() { + let text = serde_json::to_string(&state()).expect("state should serialize"); + let input = load_runtime_state_input_from_str(&text, "fallback").expect("state should load"); + assert_eq!(input.input_id, "fallback"); + assert!(input.description.is_none()); +} + +#[test] +fn validates_and_roundtrips_save_slice_document() { + let document = RuntimeSaveSliceDocument { + format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, + save_slice_id: "save-slice-smoke".to_string(), + source: RuntimeSaveSliceDocumentSource { + description: Some("test save slice".to_string()), + original_save_filename: Some("smoke.gms".to_string()), + original_save_sha256: Some("deadbeef".to_string()), + notes: vec!["captured fixture".to_string()], + }, + 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(), + 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![], + }, + }; + assert!(validate_runtime_save_slice_document(&document).is_ok()); + + let path = write_temp_json("save-slice-doc", &document); + save_runtime_save_slice_document(&path, &document).expect("save slice doc should save"); + let loaded = load_runtime_save_slice_document(&path).expect("save slice doc should load"); + assert_eq!(document, loaded); + let _ = std::fs::remove_file(path); +} + +#[test] +fn loads_save_slice_document_as_runtime_state_import() { + let text = serde_json::to_string(&RuntimeSaveSliceDocument { + format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, + save_slice_id: "save-slice-import".to_string(), + source: RuntimeSaveSliceDocumentSource { + description: Some("test save slice import".to_string()), + original_save_filename: Some("import.gms".to_string()), + original_save_sha256: None, + notes: vec![], + }, + 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(), + 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![], + }, + }) + .expect("save slice doc should serialize"); + + let input = load_runtime_state_input_from_str(&text, "fallback") + .expect("save slice document should load as runtime input"); + assert_eq!(input.input_id, "save-slice-import"); + assert_eq!( + input + .state + .metadata + .get("save_slice.import_projection") + .map(String::as_str), + Some("partial-runtime-restore-v1") + ); +} diff --git a/crates/rrt-runtime/src/documents/tests/support.rs b/crates/rrt-runtime/src/documents/tests/support.rs new file mode 100644 index 0000000..80a3653 --- /dev/null +++ b/crates/rrt-runtime/src/documents/tests/support.rs @@ -0,0 +1,1640 @@ +use super::*; +use crate::test_support::empty_runtime_state; + +pub(super) fn state() -> RuntimeState { + empty_runtime_state() +} + +pub(super) fn packed_text_bands() -> Vec { + vec![ + SmpLoadedPackedEventTextBandSummary { + label: "primary_text_band".to_string(), + packed_len: 5, + present: true, + preview: "Alpha".to_string(), + }, + SmpLoadedPackedEventTextBandSummary { + label: "secondary_text_band_0".to_string(), + packed_len: 0, + present: false, + preview: "".to_string(), + }, + SmpLoadedPackedEventTextBandSummary { + label: "secondary_text_band_1".to_string(), + packed_len: 0, + present: false, + preview: "".to_string(), + }, + SmpLoadedPackedEventTextBandSummary { + label: "secondary_text_band_2".to_string(), + packed_len: 0, + present: false, + preview: "".to_string(), + }, + SmpLoadedPackedEventTextBandSummary { + label: "secondary_text_band_3".to_string(), + packed_len: 0, + present: false, + preview: "".to_string(), + }, + SmpLoadedPackedEventTextBandSummary { + label: "secondary_text_band_4".to_string(), + packed_len: 0, + present: false, + preview: "".to_string(), + }, + ] +} + +pub(super) fn real_condition_rows() -> Vec { + vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: -1, + subtype: 4, + flag_bytes: vec![0x30; 25], + candidate_name: Some("AutoPlant".to_string()), + comparator: None, + metric: None, + semantic_family: None, + semantic_preview: None, + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec!["negative sentinel-style condition row id".to_string()], + }] +} + +pub(super) fn synthetic_packed_record( + record_index: usize, + live_entry_id: u32, + effect: RuntimeEffect, +) -> SmpLoadedPackedEventRecordSummary { + SmpLoadedPackedEventRecordSummary { + record_index, + live_entry_id, + payload_offset: Some(0x7200 + (live_entry_id as usize * 0x20)), + payload_len: Some(64), + decode_status: "parity_only".to_string(), + payload_family: "synthetic_harness".to_string(), + trigger_kind: Some(7), + active: Some(true), + marks_collection_dirty: Some(false), + one_shot: Some(false), + compact_control: None, + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: vec![], + decoded_conditions: Vec::new(), + decoded_actions: vec![effect], + executable_import_ready: false, + notes: vec!["synthetic test record".to_string()], + } +} + +pub(super) fn company_negative_sentinel_scope( + company_test_scope: RuntimeCompanyConditionTestScope, +) -> SmpLoadedPackedEventNegativeSentinelScopeSummary { + SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope, + player_test_scope: RuntimePlayerConditionTestScope::Disabled, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + } +} + +pub(super) fn territory_negative_sentinel_scope() -> SmpLoadedPackedEventNegativeSentinelScopeSummary +{ + SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies, + player_test_scope: RuntimePlayerConditionTestScope::Disabled, + territory_scope_selector_is_0x63: true, + source_row_indexes: vec![0], + } +} + +pub(super) fn player_negative_sentinel_scope() -> SmpLoadedPackedEventNegativeSentinelScopeSummary { + SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies, + player_test_scope: RuntimePlayerConditionTestScope::AllPlayers, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + } +} + +pub(super) fn selected_chairman_negative_sentinel_scope() +-> SmpLoadedPackedEventNegativeSentinelScopeSummary { + SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + } +} + +pub(super) fn real_grouped_rows() -> Vec { + vec![SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 2, + descriptor_label: Some("Company Cash".to_string()), + target_mask_bits: Some(0x01), + parameter_family: Some("company_finance_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 8, + raw_scalar_value: 7, + value_byte_0x09: 1, + value_dword_0x0d: 12, + value_byte_0x11: 2, + value_byte_0x12: 3, + value_word_0x14: 24, + value_word_0x16: 36, + row_shape: "multivalue_scalar".to_string(), + semantic_family: Some("multivalue_scalar".to_string()), + semantic_preview: Some("Set Company Cash to 7 with aux [2, 3, 24, 36]".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: Some("Mikado".to_string()), + notes: vec!["grouped effect row carries locomotive-name side string".to_string()], + }] +} + +pub(super) fn real_deactivate_company_row( + enabled: bool, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 13, + descriptor_label: Some("Deactivate Company".to_string()), + target_mask_bits: Some(0x01), + parameter_family: Some("company_lifecycle_toggle".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 1, + raw_scalar_value: if enabled { 1 } else { 0 }, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "bool_toggle".to_string(), + semantic_family: Some("bool_toggle".to_string()), + semantic_preview: Some(format!( + "Set Deactivate Company to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_track_capacity_row(value: i32) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 16, + descriptor_label: Some("Company Track Pieces Buildable".to_string()), + target_mask_bits: Some(0x01), + parameter_family: Some("company_build_limit_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_credit_rating_row(value: i32) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 56, + descriptor_label: Some("Credit Rating".to_string()), + target_mask_bits: Some(0x0b), + parameter_family: Some("company_governance_scalar".to_string()), + grouped_target_subject: Some("company".to_string()), + grouped_target_scope: Some("selected_company".to_string()), + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Credit Rating to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_merger_premium_shell_row( + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 58, + descriptor_label: Some("Merger Premium".to_string()), + target_mask_bits: Some(0x0b), + parameter_family: Some("company_finance_shell_scalar".to_string()), + grouped_target_subject: Some("company".to_string()), + grouped_target_scope: Some("selected_company".to_string()), + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Merger Premium to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![ + "descriptor is recovered in the checked-in effect table as shell_owned parity" + .to_string(), + ], + } +} + +pub(super) fn real_stock_prices_shell_row( + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 55, + descriptor_label: Some("Stock Prices".to_string()), + target_mask_bits: Some(0x0b), + parameter_family: Some("company_finance_shell_scalar".to_string()), + grouped_target_subject: Some("company".to_string()), + grouped_target_scope: Some("selected_company".to_string()), + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Stock Prices to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![ + "descriptor is recovered in the checked-in effect table as shell_owned parity" + .to_string(), + ], + } +} + +pub(super) fn real_deactivate_player_row( + enabled: bool, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 14, + descriptor_label: Some("Deactivate Player".to_string()), + target_mask_bits: Some(0x02), + parameter_family: Some("player_lifecycle_toggle".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 1, + raw_scalar_value: if enabled { 1 } else { 0 }, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "bool_toggle".to_string(), + semantic_family: Some("bool_toggle".to_string()), + semantic_preview: Some(format!( + "Set Deactivate Player to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_territory_access_row( + enabled: bool, + notes: Vec, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 3, + descriptor_label: Some("Territory - Allow All".to_string()), + target_mask_bits: Some(0x05), + parameter_family: Some("territory_access_toggle".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 1, + raw_scalar_value: if enabled { 1 } else { 0 }, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "bool_toggle".to_string(), + semantic_family: Some("bool_toggle".to_string()), + semantic_preview: Some(format!( + "Set Territory - Allow All to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes, + } +} + +pub(super) fn real_economic_status_row(value: i32) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 8, + descriptor_label: Some("Economic Status".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("whole_game_state_enum".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Economic Status to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_limited_track_building_amount_row( + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 122, + descriptor_label: Some("Limited Track Building Amount".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("world_track_build_limit_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Limited Track Building Amount to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_special_condition_row( + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 108, + descriptor_label: Some("Use Wartime Cargos".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("special_condition_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Use Wartime Cargos to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_candidate_availability_row( + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 109, + descriptor_label: Some("Turbo Diesel Availability".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("candidate_availability_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Turbo Diesel Availability to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_locomotive_availability_row( + descriptor_id: u32, + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + fn grounded_locomotive_name(locomotive_id: u32) -> Option<&'static str> { + match locomotive_id { + 1 => Some("2-D-2"), + 2 => Some("E-88"), + 3 => Some("Adler 2-2-2"), + 4 => Some("USA 103"), + 5 => Some("American 4-4-0"), + 6 => Some("Atlantic 4-4-2"), + 7 => Some("Baldwin 0-6-0"), + 8 => Some("Be 5/7"), + 9 => Some("Beuth 2-2-2"), + 10 => Some("Big Boy 4-8-8-4"), + 11 => Some("C55 Deltic"), + 12 => Some("Camelback 0-6-0"), + 13 => Some("Challenger 4-6-6-4"), + 14 => Some("Class 01 4-6-2"), + 15 => Some("Class 103"), + 16 => Some("Class 132"), + 17 => Some("Class 500 4-6-0"), + 18 => Some("Class 9100"), + 19 => Some("Class EF 66"), + 20 => Some("Class 6E"), + 21 => Some("Consolidation 2-8-0"), + 22 => Some("Crampton 4-2-0"), + 23 => Some("DD 080-X"), + 24 => Some("DD40AX"), + 25 => Some("Duke Class 4-4-0"), + 26 => Some("E18"), + 27 => Some("E428"), + 28 => Some("Brenner E412"), + 29 => Some("E60CP"), + 30 => Some("Eight Wheeler 4-4-0"), + 31 => Some("EP-2 Bipolar"), + 32 => Some("ET22"), + 33 => Some("F3"), + 34 => Some("Fairlie 0-6-6-0"), + 35 => Some("Firefly 2-2-2"), + 36 => Some("FP45"), + 37 => Some("Ge 6/6 Crocodile"), + 38 => Some("GG1"), + 39 => Some("GP7"), + 40 => Some("H10 2-8-2"), + 41 => Some("HST 125"), + 42 => Some("Kriegslok 2-10-0"), + 43 => Some("Mallard 4-6-2"), + 44 => Some("Norris 4-2-0"), + 45 => Some("Northern 4-8-4"), + 46 => Some("Orca NX462"), + 47 => Some("Pacific 4-6-2"), + 48 => Some("Planet 2-2-0"), + 49 => Some("Re 6/6"), + 50 => Some("Red Devil 4-8-4"), + 51 => Some("S3 4-4-0"), + 52 => Some("NA-90D"), + 53 => Some("Shay (2-Truck)"), + 54 => Some("Shinkansen Series 0"), + 55 => Some("Stirling 4-2-2"), + 56 => Some("Trans-Euro"), + 57 => Some("V200"), + 58 => Some("VL80T"), + 59 => Some("GP 35"), + 60 => Some("U1"), + 61 => Some("Zephyr"), + _ => None, + } + } + + let recovered_locomotive_id = match descriptor_id { + 241..=351 => Some(descriptor_id - 240), + _ => None, + }; + let descriptor_label = match descriptor_id { + 457..=474 => { + format!( + "Upper-Band Locomotive Availability Slot {}", + descriptor_id - 456 + ) + } + _ => recovered_locomotive_id + .map(|loco_id| { + grounded_locomotive_name(loco_id) + .map(|name| format!("{name} Availability")) + .unwrap_or_else(|| format!("Locomotive {loco_id} Availability")) + }) + .unwrap_or_else(|| "Locomotive Availability".to_string()), + }; + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id, + descriptor_label: Some(descriptor_label.clone()), + target_mask_bits: Some(0x08), + parameter_family: Some("locomotive_availability_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set {descriptor_label} to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_locomotive_cost_row( + descriptor_id: u32, + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + fn grounded_locomotive_name(locomotive_id: u32) -> Option<&'static str> { + match locomotive_id { + 1 => Some("2-D-2"), + 2 => Some("E-88"), + 3 => Some("Adler 2-2-2"), + 4 => Some("USA 103"), + 5 => Some("American 4-4-0"), + 6 => Some("Atlantic 4-4-2"), + 7 => Some("Baldwin 0-6-0"), + 8 => Some("Be 5/7"), + 9 => Some("Beuth 2-2-2"), + 10 => Some("Big Boy 4-8-8-4"), + 11 => Some("C55 Deltic"), + 12 => Some("Camelback 0-6-0"), + 13 => Some("Challenger 4-6-6-4"), + 14 => Some("Class 01 4-6-2"), + 15 => Some("Class 103"), + 16 => Some("Class 132"), + 17 => Some("Class 500 4-6-0"), + 18 => Some("Class 9100"), + 19 => Some("Class EF 66"), + 20 => Some("Class 6E"), + 21 => Some("Consolidation 2-8-0"), + 22 => Some("Crampton 4-2-0"), + 23 => Some("DD 080-X"), + 24 => Some("DD40AX"), + 25 => Some("Duke Class 4-4-0"), + 26 => Some("E18"), + 27 => Some("E428"), + 28 => Some("Brenner E412"), + 29 => Some("E60CP"), + 30 => Some("Eight Wheeler 4-4-0"), + 31 => Some("EP-2 Bipolar"), + 32 => Some("ET22"), + 33 => Some("F3"), + 34 => Some("Fairlie 0-6-6-0"), + 35 => Some("Firefly 2-2-2"), + 36 => Some("FP45"), + 37 => Some("Ge 6/6 Crocodile"), + 38 => Some("GG1"), + 39 => Some("GP7"), + 40 => Some("H10 2-8-2"), + 41 => Some("HST 125"), + 42 => Some("Kriegslok 2-10-0"), + 43 => Some("Mallard 4-6-2"), + 44 => Some("Norris 4-2-0"), + 45 => Some("Northern 4-8-4"), + 46 => Some("Orca NX462"), + 47 => Some("Pacific 4-6-2"), + 48 => Some("Planet 2-2-0"), + 49 => Some("Re 6/6"), + 50 => Some("Red Devil 4-8-4"), + 51 => Some("S3 4-4-0"), + 52 => Some("NA-90D"), + 53 => Some("Shay (2-Truck)"), + 54 => Some("Shinkansen Series 0"), + 55 => Some("Stirling 4-2-2"), + 56 => Some("Trans-Euro"), + 57 => Some("V200"), + 58 => Some("VL80T"), + 59 => Some("GP 35"), + 60 => Some("U1"), + 61 => Some("Zephyr"), + _ => None, + } + } + + let recovered_locomotive_id = match descriptor_id { + 352..=451 => Some(descriptor_id - 351), + _ => None, + }; + let descriptor_label = match descriptor_id { + 475..=502 => format!("Upper-Band Locomotive Cost Slot {}", descriptor_id - 474), + _ => recovered_locomotive_id + .map(|loco_id| { + grounded_locomotive_name(loco_id) + .map(|name| format!("{name} Cost")) + .unwrap_or_else(|| format!("Locomotive {loco_id} Cost")) + }) + .unwrap_or_else(|| "Locomotive Cost".to_string()), + }; + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id, + descriptor_label: Some(descriptor_label.clone()), + target_mask_bits: Some(0x08), + parameter_family: Some("locomotive_cost_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set {descriptor_label} to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn save_named_locomotive_table( + count: usize, +) -> SmpLoadedNamedLocomotiveAvailabilityTable { + fn grounded_locomotive_name(index: usize) -> String { + match index { + 0 => "2-D-2", + 1 => "E-88", + 2 => "Adler 2-2-2", + 3 => "USA 103", + 4 => "American 4-4-0", + 5 => "Atlantic 4-4-2", + 6 => "Baldwin 0-6-0", + 7 => "Be 5/7", + 8 => "Beuth 2-2-2", + 9 => "Big Boy 4-8-8-4", + 10 => "C55 Deltic", + 11 => "Camelback 0-6-0", + 12 => "Challenger 4-6-6-4", + 13 => "Class 01 4-6-2", + 14 => "Class 103", + 15 => "Class 132", + 16 => "Class 500 4-6-0", + 17 => "Class 9100", + 18 => "Class EF 66", + 19 => "Class 6E", + 20 => "Consolidation 2-8-0", + 21 => "Crampton 4-2-0", + 22 => "DD 080-X", + 23 => "DD40AX", + 24 => "Duke Class 4-4-0", + 25 => "E18", + 26 => "E428", + 27 => "Brenner E412", + 28 => "E60CP", + 29 => "Eight Wheeler 4-4-0", + 30 => "EP-2 Bipolar", + 31 => "ET22", + 32 => "F3", + 33 => "Fairlie 0-6-6-0", + 34 => "Firefly 2-2-2", + 35 => "FP45", + 36 => "Ge 6/6 Crocodile", + 37 => "GG1", + 38 => "GP7", + 39 => "H10 2-8-2", + 40 => "HST 125", + 41 => "Kriegslok 2-10-0", + 42 => "Mallard 4-6-2", + 43 => "Norris 4-2-0", + 44 => "Northern 4-8-4", + 45 => "Orca NX462", + 46 => "Pacific 4-6-2", + 47 => "Planet 2-2-0", + 48 => "Re 6/6", + 49 => "Red Devil 4-8-4", + 50 => "S3 4-4-0", + 51 => "NA-90D", + 52 => "Shay (2-Truck)", + 53 => "Shinkansen Series 0", + 54 => "Stirling 4-2-2", + 55 => "Trans-Euro", + 56 => "V200", + 57 => "VL80T", + 58 => "GP 35", + 59 => "U1", + 60 => "Zephyr", + _ => return format!("Locomotive {}", index + 1), + } + .to_string() + } + + SmpLoadedNamedLocomotiveAvailabilityTable { + source_kind: "runtime-save-direct-serializer".to_string(), + semantic_family: "scenario-named-locomotive-availability-table".to_string(), + header_offset: None, + entries_offset: Some(0x7c78), + entries_end_offset: Some(0x7c78 + count * 0x41), + observed_entry_count: count, + zero_availability_count: 0, + zero_availability_names: vec![], + entries: (0..count) + .map(|index| SmpRt3105SaveNameTableEntry { + index, + offset: 0x7c78 + index * 0x41, + text: grounded_locomotive_name(index), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }) + .collect(), + } +} + +pub(super) fn save_cargo_catalog( + entries: &[(u32, crate::event::targets::RuntimeCargoClass)], +) -> SmpLoadedCargoCatalog { + SmpLoadedCargoCatalog { + source_kind: "recipe-book-summary-slot-catalog".to_string(), + semantic_family: "scenario-save-derived-cargo-catalog".to_string(), + root_offset: Some(0x0fe7), + observed_entry_count: entries.len(), + entries: entries + .iter() + .enumerate() + .map( + |(index, (slot_id, cargo_class))| SmpLoadedCargoCatalogEntry { + slot_id: *slot_id, + label: format!("Cargo Production Slot {slot_id}"), + cargo_class: *cargo_class, + book_index: index, + max_annual_production_word: 0, + mode_word: 0, + runtime_import_branch_kind: "zero-mode-skipped".to_string(), + annual_amount_word: 0, + supplied_cargo_token_word: 0, + supplied_cargo_token_probable_high16_ascii_stem: None, + demanded_cargo_token_word: 0, + demanded_cargo_token_probable_high16_ascii_stem: None, + }, + ) + .collect(), + } +} + +pub(super) fn save_company_roster() -> SmpLoadedCompanyRoster { + SmpLoadedCompanyRoster { + source_kind: "tracked-save-slice-company-roster".to_string(), + semantic_family: "save-slice-runtime-company-context".to_string(), + observed_entry_count: 2, + selected_company_id: Some(1), + entries: vec![ + SmpLoadedCompanyRosterEntry { + company_id: 1, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 150, + debt: 80, + credit_rating_score: Some(650), + prime_rate: Some(5), + available_track_laying_capacity: Some(6), + track_piece_counts: RuntimeTrackPieceCounts { + total: 20, + single: 5, + double: 8, + transition: 1, + electric: 3, + non_electric: 17, + }, + linked_chairman_profile_id: Some(1), + book_value_per_share: 2620, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1839), + merger_cooldown_year: Some(1838), + preferred_locomotive_engine_type_raw_u8: Some(2), + market_state: Some(crate::state::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 2, + live_bond_slots: Vec::new(), + largest_live_bond_principal: Some(500_000), + highest_coupon_live_bond_principal: Some(350_000), + mutable_support_scalar_raw_u32: 0x3f99999a, + young_company_support_scalar_raw_u32: 0x42700000, + support_progress_word: 12, + recent_per_share_cache_absolute_counter: 0, + recent_per_share_cached_value_bits: 0, + recent_per_share_subscore_raw_u32: 0x420c0000, + cached_share_price_raw_u32: 0x42200000, + chairman_salary_baseline: 24, + chairman_salary_current: 30, + chairman_bonus_year: 1832, + chairman_bonus_amount: 900, + founding_year: 1831, + last_bankruptcy_year: 0, + last_dividend_year: 1837, + current_issue_calendar_word: 5, + current_issue_calendar_word_2: 6, + prior_issue_calendar_word: 4, + prior_issue_calendar_word_2: 5, + city_connection_latch: true, + linked_transit_latch: false, + linked_transit_route_anchor_entry_id: Some(77), + linked_transit_route_anchor_fallback_counts: vec![3, 5, 8], + stat_band_root_0cfb_candidates: Vec::new(), + stat_band_root_0d7f_candidates: Vec::new(), + stat_band_root_1c47_candidates: Vec::new(), + year_stat_family_qword_bits: Vec::new(), + special_stat_family_232a_qword_bits: Vec::new(), + issue_opinion_terms_raw_i32: Vec::new(), + direct_control_transfer_float_fields_raw_u32: BTreeMap::new(), + direct_control_transfer_int_fields_raw_u32: BTreeMap::new(), + }), + }, + SmpLoadedCompanyRosterEntry { + company_id: 2, + active: true, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 90, + debt: 40, + credit_rating_score: Some(480), + prime_rate: Some(6), + available_track_laying_capacity: Some(2), + track_piece_counts: RuntimeTrackPieceCounts { + total: 8, + single: 2, + double: 2, + transition: 0, + electric: 1, + non_electric: 7, + }, + linked_chairman_profile_id: Some(2), + book_value_per_share: 1400, + investor_confidence: 22, + management_attitude: 31, + takeover_cooldown_year: None, + merger_cooldown_year: None, + preferred_locomotive_engine_type_raw_u8: Some(0), + market_state: Some(crate::state::RuntimeCompanyMarketState { + outstanding_shares: 18_000, + bond_count: 1, + live_bond_slots: Vec::new(), + largest_live_bond_principal: Some(300_000), + highest_coupon_live_bond_principal: Some(300_000), + mutable_support_scalar_raw_u32: 0x3f4ccccd, + young_company_support_scalar_raw_u32: 0x42580000, + support_progress_word: 9, + recent_per_share_cache_absolute_counter: 0, + recent_per_share_cached_value_bits: 0, + recent_per_share_subscore_raw_u32: 0x41f00000, + cached_share_price_raw_u32: 0x41f80000, + chairman_salary_baseline: 20, + chairman_salary_current: 22, + chairman_bonus_year: 0, + chairman_bonus_amount: 0, + founding_year: 1833, + last_bankruptcy_year: 0, + last_dividend_year: 0, + current_issue_calendar_word: 3, + current_issue_calendar_word_2: 4, + prior_issue_calendar_word: 2, + prior_issue_calendar_word_2: 3, + city_connection_latch: false, + linked_transit_latch: true, + linked_transit_route_anchor_entry_id: Some(41), + linked_transit_route_anchor_fallback_counts: vec![13, 21, 34], + stat_band_root_0cfb_candidates: Vec::new(), + stat_band_root_0d7f_candidates: Vec::new(), + stat_band_root_1c47_candidates: Vec::new(), + year_stat_family_qword_bits: Vec::new(), + special_stat_family_232a_qword_bits: Vec::new(), + issue_opinion_terms_raw_i32: Vec::new(), + direct_control_transfer_float_fields_raw_u32: BTreeMap::new(), + direct_control_transfer_int_fields_raw_u32: BTreeMap::new(), + }), + }, + ], + } +} + +pub(super) fn save_placed_structure_collection() -> SmpLoadedPlacedStructureCollection { + SmpLoadedPlacedStructureCollection { + source_kind: "save-placed-structure-record-triplets".to_string(), + semantic_family: "scenario-save-placed-structure-triplet-collection".to_string(), + observed_entry_count: 2, + entries: vec![ + SmpLoadedPlacedStructureEntry { + record_index: 0, + primary_name: "FarmCorn".to_string(), + secondary_name: "FarmSet".to_string(), + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_payload_dword: 0, + profile_payload_dword_hex: "0x00000000".to_string(), + profile_status_kind: "farm_growth_stage_bucket".to_string(), + farm_growth_stage_index: Some(4), + profile_companion_byte_u8: Some(0), + profile_companion_byte_hex: Some("0x00".to_string()), + }, + SmpLoadedPlacedStructureEntry { + record_index: 1, + primary_name: "StationA".to_string(), + secondary_name: "StationSetA".to_string(), + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_payload_dword: 0x00005dc1, + profile_payload_dword_hex: "0x00005dc1".to_string(), + profile_status_kind: "opaque_nondefault".to_string(), + farm_growth_stage_index: None, + profile_companion_byte_u8: Some(7), + profile_companion_byte_hex: Some("0x07".to_string()), + }, + ], + } +} + +pub(super) fn save_placed_structure_dynamic_side_buffer_summary() +-> SmpLoadedPlacedStructureDynamicSideBufferSummary { + SmpLoadedPlacedStructureDynamicSideBufferSummary { + source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(), + semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-summary".to_string(), + observed_entry_count: 118, + owner_shared_dword_hex: "0xff0000ff".to_string(), + unique_embedded_name_pair_count: 9, + decoded_embedded_name_row_count: 118, + first_prefix_leading_dword_hex: "0xff0000ff".to_string(), + first_prefix_trailing_word_hex: "0x0001".to_string(), + first_prefix_separator_byte_hex: "0xff".to_string(), + triplet_alignment_overlap_count: 2, + triplet_alignment_side_buffer_only_name_pair_count: 7, + compact_prefix_pattern_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary { + prefix_leading_dword: 0xff0000ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 1, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + count: 3, + first_name_tag_relative_offset: 0x24, + prefix_leading_dword_matches_embedded_profile_tag: true, + section_like_primary_name_count: 0, + cap_like_primary_name_count: 0, + other_primary_name_count: 3, + first_primary_name: Some("FarmCorn".to_string()), + first_secondary_name: Some("FarmSet".to_string()), + }, + ], + name_pair_summaries: vec![SmpSavePlacedStructureDynamicSideBufferNamePairSummary { + primary_name: "FarmCorn".to_string(), + secondary_name: "FarmSet".to_string(), + count: 3, + first_name_tag_relative_offset: 0x24, + unique_compact_prefix_pattern_count: 1, + dominant_prefix_leading_dword: 0xff0000ff, + dominant_prefix_leading_dword_hex: "0xff0000ff".to_string(), + dominant_prefix_trailing_word: 1, + dominant_prefix_trailing_word_hex: "0x0001".to_string(), + dominant_prefix_separator_byte: 0xff, + dominant_prefix_separator_byte_hex: "0xff".to_string(), + dominant_prefix_count: 3, + }], + } +} + +pub(super) fn save_region_collection() -> SmpLoadedRegionCollection { + SmpLoadedRegionCollection { + source_kind: "save-region-record-triplets".to_string(), + semantic_family: "scenario-save-region-triplet-collection".to_string(), + observed_entry_count: 2, + entries: vec![ + SmpLoadedRegionEntry { + record_index: 0, + name: "Marker09".to_string(), + pre_name_prefix_len: 0, + policy_leading_f32_0: 368.0, + policy_leading_f32_1: 0.0, + policy_leading_f32_2: 92.0, + policy_reserved_dwords: vec![0, 0, 0], + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_collection: Some(SmpLoadedRegionProfileCollection { + direct_collection_flag: 1, + entry_stride: 0x22, + live_id_bound: 18, + live_record_count: 17, + trailing_padding_len: 2, + entries: vec![ + SmpLoadedRegionProfileEntry { + entry_index: 0, + name: "House".to_string(), + trailing_weight_f32: 0.2, + }, + SmpLoadedRegionProfileEntry { + entry_index: 1, + name: "Farm Corn".to_string(), + trailing_weight_f32: 0.2, + }, + ], + }), + }, + SmpLoadedRegionEntry { + record_index: 1, + name: "Marker10".to_string(), + pre_name_prefix_len: 8, + policy_leading_f32_0: 552.0, + policy_leading_f32_1: 0.0, + policy_leading_f32_2: 276.0, + policy_reserved_dwords: vec![0, 4, 0], + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_collection: Some(SmpLoadedRegionProfileCollection { + direct_collection_flag: 1, + entry_stride: 0x22, + live_id_bound: 26, + live_record_count: 24, + trailing_padding_len: 0, + entries: vec![SmpLoadedRegionProfileEntry { + entry_index: 0, + name: "Farm Corn".to_string(), + trailing_weight_f32: 0.2, + }], + }), + }, + ], + } +} + +pub(super) fn save_region_fixed_row_run_summary() -> SmpLoadedRegionFixedRowRunSummary { + SmpLoadedRegionFixedRowRunSummary { + source_kind: "save-region-fixed-row-run-candidates".to_string(), + semantic_family: "scenario-save-region-fixed-row-run-summary".to_string(), + target_row_count: 2, + target_row_stride: 0xbc, + target_row_stride_hex: "0xbc".to_string(), + candidates: vec![SmpSaveRegionFixedRowRunCandidate { + count_offset: 0x5300, + count_offset_hex: "0x5300".to_string(), + row_count: 2, + row_stride: 0xbc, + row_stride_hex: "0xbc".to_string(), + rows_offset: 0x5310, + rows_offset_hex: "0x5310".to_string(), + rows_end_offset: 0x5488, + rows_end_offset_hex: "0x5488".to_string(), + distance_to_region_metadata_tag: 0x110, + distance_to_region_metadata_tag_hex: "0x110".to_string(), + dword_lane_summaries: vec![], + shape_signature: "dword0:f32,dword1:zero".to_string(), + shape_family_signature: "family-a".to_string(), + trailing_byte_zero_count: 2, + trailing_byte_nonzero_count: 0, + trailing_byte_distinct_value_count: 1, + trailing_byte_sample_values_hex: vec!["0x00".to_string()], + best_probable_density_lane_relative_offset_hex: Some("0x24".to_string()), + }], + } +} + +pub(super) fn save_chairman_profile_table() -> SmpLoadedChairmanProfileTable { + SmpLoadedChairmanProfileTable { + source_kind: "tracked-save-slice-chairman-profile-table".to_string(), + semantic_family: "save-slice-runtime-chairman-context".to_string(), + observed_entry_count: 2, + selected_chairman_profile_id: Some(1), + entries: vec![ + SmpLoadedChairmanProfileEntry { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 500, + linked_company_id: Some(1), + company_holdings: BTreeMap::from([(1, 1000)]), + holdings_value_total: 700, + net_worth_total: 1200, + purchasing_power_total: 1500, + personality_byte_0x291: Some(12), + issue_opinion_terms_raw_i32: Vec::new(), + }, + SmpLoadedChairmanProfileEntry { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 250, + linked_company_id: Some(2), + company_holdings: BTreeMap::from([(2, 900)]), + holdings_value_total: 600, + net_worth_total: 900, + purchasing_power_total: 1100, + personality_byte_0x291: Some(20), + issue_opinion_terms_raw_i32: Vec::new(), + }, + ], + } +} + +pub(super) fn real_cargo_production_row( + descriptor_id: u32, + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + let slot = descriptor_id.saturating_sub(229); + let descriptor_label = format!("Cargo Production Slot {slot}"); + let recovered_cargo_class = match slot { + 1..=4 => Some("factory".to_string()), + 5..=8 => Some("farm_mine".to_string()), + 9..=11 => Some("other".to_string()), + _ => None, + }; + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id, + descriptor_label: Some(descriptor_label.clone()), + target_mask_bits: Some(0x08), + parameter_family: Some("cargo_production_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set {descriptor_label} to {value}")), + recovered_cargo_slot: Some(slot), + recovered_cargo_class, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_all_cargo_price_row(value: i32) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 105, + descriptor_label: Some("All Cargo Prices".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("cargo_price_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set All Cargo Prices to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![ + "descriptor recovered from checked-in EventEffects semantic catalog".to_string(), + ], + } +} + +pub(super) fn real_named_cargo_price_row( + descriptor_id: u32, + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + let cargo_label = grounded_named_cargo_price_label(descriptor_id).map(ToString::to_string); + let descriptor_label = cargo_label + .as_deref() + .map(|label| format!("{label} Price")) + .unwrap_or_else(|| { + format!( + "Named Cargo Price Slot {}", + descriptor_id.saturating_sub(105) + ) + }); + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id, + descriptor_label: Some(descriptor_label.clone()), + target_mask_bits: Some(0x08), + parameter_family: Some("cargo_price_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set {descriptor_label} to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: cargo_label, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![ + "descriptor recovered from checked-in EventEffects semantic catalog".to_string(), + ], + } +} + +pub(super) fn real_aggregate_cargo_production_row( + descriptor_id: u32, + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + let (label, recovered_cargo_class) = match descriptor_id { + 177 => ("All Cargo Production", None), + 178 => ("All Factory Production", Some("factory".to_string())), + 179 => ("All Farm/Mine Production", Some("farm_mine".to_string())), + _ => ("Unknown Cargo Production", None), + }; + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id, + descriptor_label: Some(label.to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("cargo_production_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set {label} to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![ + "descriptor recovered from checked-in EventEffects semantic catalog".to_string(), + ], + } +} + +pub(super) fn real_named_cargo_production_row( + descriptor_id: u32, + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + let cargo_label = match descriptor_id { + 180 => Some("Alcohol".to_string()), + _ => None, + }; + let descriptor_label = cargo_label + .as_ref() + .map(|label| format!("{label} Production")) + .unwrap_or_else(|| "Unknown Cargo Production".to_string()); + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id, + descriptor_label: Some(descriptor_label.clone()), + target_mask_bits: Some(0x08), + parameter_family: Some("cargo_production_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set {descriptor_label} to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: cargo_label, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![ + "descriptor recovered from checked-in EventEffects semantic catalog".to_string(), + ], + } +} + +pub(super) fn real_territory_access_cost_row( + value: i32, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 453, + descriptor_label: Some("Territory Access Cost".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("territory_access_cost_scalar".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!("Set Territory Access Cost to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_world_flag_row( + descriptor_id: u32, + label: &str, + enabled: bool, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id, + descriptor_label: Some(label.to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("world_flag_toggle".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 0, + raw_scalar_value: if enabled { 1 } else { 0 }, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "bool_toggle".to_string(), + semantic_family: Some("bool_toggle".to_string()), + semantic_preview: Some(format!( + "Set {label} to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_confiscate_all_row( + enabled: bool, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 9, + descriptor_label: Some("Confiscate All".to_string()), + target_mask_bits: Some(0x01), + parameter_family: Some("company_confiscation_variant".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 1, + raw_scalar_value: if enabled { 1 } else { 0 }, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "bool_toggle".to_string(), + semantic_family: Some("bool_toggle".to_string()), + semantic_preview: Some(format!( + "Set Confiscate All to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_retire_train_row( + enabled: bool, + locomotive_name: Option<&str>, + notes: Vec, +) -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 15, + descriptor_label: Some("Retire Train".to_string()), + target_mask_bits: Some(0x0d), + parameter_family: Some("company_or_territory_asset_toggle".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 1, + raw_scalar_value: if enabled { 1 } else { 0 }, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "bool_toggle".to_string(), + semantic_family: Some("bool_toggle".to_string()), + semantic_preview: Some(format!( + "Set Retire Train to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: locomotive_name.map(ToString::to_string), + notes, + } +} + +pub(super) fn unsupported_real_grouped_row() -> SmpLoadedPackedEventGroupedEffectRowSummary { + SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 1, + row_index: 0, + descriptor_id: 9, + descriptor_label: Some("Confiscate All".to_string()), + target_mask_bits: Some(0x01), + parameter_family: Some("company_confiscation_variant".to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode: 1, + raw_scalar_value: 0, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "bool_toggle".to_string(), + semantic_family: Some("bool_toggle".to_string()), + semantic_preview: Some("Set Confiscate All to FALSE".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + recovered_cargo_label: None, + recovered_locomotive_id: None, + locomotive_name: None, + notes: vec![], + } +} + +pub(super) fn real_compact_control() -> SmpLoadedPackedEventCompactControlSummary { + SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 1, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22], + } +} + +pub(super) fn real_compact_control_without_symbolic_company_scope() +-> SmpLoadedPackedEventCompactControlSummary { + SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 1, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![8, 9, 10, 11], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22], + } +} diff --git a/crates/rrt-runtime/src/documents/tests/validation.rs b/crates/rrt-runtime/src/documents/tests/validation.rs new file mode 100644 index 0000000..4f20502 --- /dev/null +++ b/crates/rrt-runtime/src/documents/tests/validation.rs @@ -0,0 +1,194 @@ +use super::*; +use crate::test_support::unique_temp_path; + +#[test] +fn loads_overlay_import_document_with_relative_paths() { + let fixture_dir = unique_temp_path("overlay-import", "dir"); + std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created"); + + let snapshot_path = fixture_dir.join("base.json"); + let save_slice_path = fixture_dir.join("slice.json"); + let overlay_path = fixture_dir.join("overlay.json"); + + let snapshot = RuntimeSnapshotDocument { + format_version: SNAPSHOT_FORMAT_VERSION, + snapshot_id: "base".to_string(), + source: RuntimeSnapshotSource { + source_fixture_id: None, + description: Some("base snapshot".to_string()), + }, + state: RuntimeState { + calendar: CalendarPoint { + year: 1835, + month_slot: 1, + phase_slot: 2, + tick_slot: 4, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(42), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }, + }; + save_runtime_snapshot_document(&snapshot_path, &snapshot).expect("snapshot should save"); + + let save_slice_document = RuntimeSaveSliceDocument { + format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, + save_slice_id: "slice".to_string(), + source: RuntimeSaveSliceDocumentSource::default(), + 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(), + 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: Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 0, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + control_lane_notes: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + records: vec![SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(48), + decode_status: "parity_only".to_string(), + payload_family: "synthetic_harness".to_string(), + trigger_kind: Some(7), + active: Some(true), + marks_collection_dirty: Some(false), + one_shot: Some(false), + compact_control: None, + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: vec![], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { + target: crate::event::targets::RuntimeCompanyTarget::Ids { ids: vec![42] }, + delta: 50, + }], + executable_import_ready: false, + notes: vec!["needs company context".to_string()], + }], + }), + notes: vec![], + }, + }; + save_runtime_save_slice_document(&save_slice_path, &save_slice_document) + .expect("save slice should save"); + + let overlay = RuntimeOverlayImportDocument { + format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, + import_id: "overlay-relative".to_string(), + source: RuntimeOverlayImportDocumentSource { + description: Some("relative overlay".to_string()), + notes: vec![], + }, + base_snapshot_path: "base.json".to_string(), + save_slice_path: "slice.json".to_string(), + }; + save_runtime_overlay_import_document(&overlay_path, &overlay) + .expect("overlay document should save"); + + let input = + load_runtime_state_input(&overlay_path).expect("overlay runtime import should load"); + assert_eq!(input.input_id, "overlay-relative"); + assert_eq!(input.state.event_runtime_records.len(), 1); + assert_eq!(input.state.companies[0].company_id, 42); + + let _ = std::fs::remove_file(snapshot_path); + let _ = std::fs::remove_file(save_slice_path); + let _ = std::fs::remove_file(overlay_path); + let _ = std::fs::remove_dir(fixture_dir); +} diff --git a/crates/rrt-runtime/src/economy.rs b/crates/rrt-runtime/src/economy.rs deleted file mode 100644 index e15cdbe..0000000 --- a/crates/rrt-runtime/src/economy.rs +++ /dev/null @@ -1,875 +0,0 @@ -use std::collections::BTreeSet; -use std::fs; -use std::path::Path; - -use serde::{Deserialize, Serialize}; - -use crate::pk4::inspect_pk4_bytes; - -pub const NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT: usize = 71; -pub const NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT: usize = 50; -pub const CARGO_TYPE_MAGIC: u32 = 0x0000_03ea; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoNameToken { - pub raw_name: String, - pub visible_name: String, - #[serde(default)] - pub localized_string_id: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoTypeEntry { - pub file_name: String, - pub file_size: usize, - pub file_size_hex: String, - pub header_magic: u32, - pub header_magic_hex: String, - pub name: CargoNameToken, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoTypeInspectionReport { - pub directory_path: String, - pub entry_count: usize, - pub unique_visible_name_count: usize, - pub notes: Vec, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoSkinDescriptorEntry { - pub pk4_entry_name: String, - pub payload_len: usize, - pub payload_len_hex: String, - pub descriptor_kind: String, - pub name: CargoNameToken, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoSkinInspectionReport { - pub pk4_path: String, - pub entry_count: usize, - pub unique_visible_name_count: usize, - pub notes: Vec, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoEconomySourceReport { - pub cargo_types_dir: String, - pub cargo_skin_pk4_path: String, - pub named_cargo_price_row_count: usize, - pub named_cargo_production_row_count: usize, - pub cargo_type_count: usize, - pub cargo_skin_count: usize, - pub shared_visible_name_count: usize, - pub visible_name_union_count: usize, - pub cargo_type_only_visible_names: Vec, - pub cargo_skin_only_visible_names: Vec, - pub live_registry_count: usize, - pub live_registry_entries: Vec, - pub price_selector_candidate_excess_count: usize, - pub price_selector_candidate_only_visible_names: Vec, - pub production_selector: Option, - pub price_selector: CargoSelectorReport, - pub notes: Vec, - pub cargo_type_entries: Vec, - pub cargo_skin_entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CargoRegistrySourceKind { - CargoTypes, - CargoSkin, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoLiveRegistryEntry { - pub visible_name: String, - pub raw_names: Vec, - pub localized_string_ids: Vec, - pub source_kinds: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoSelectorEntry { - pub selector_index: usize, - #[serde(default)] - pub descriptor_id: Option, - pub visible_name: String, - pub source_kinds: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CargoSelectorReport { - pub selector_kind: String, - pub exact_resolution: bool, - pub selector_row_count: usize, - pub candidate_registry_count: usize, - pub notes: Vec, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct CargoBindingArtifact { - bindings: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct CargoBindingRow { - descriptor_id: u32, - band: String, - cargo_name: String, - binding_index: usize, -} - -pub fn inspect_cargo_types_dir( - path: &Path, -) -> Result> { - let mut entries = Vec::new(); - for entry in fs::read_dir(path)? { - let entry = entry?; - if !entry.file_type()?.is_file() { - continue; - } - let file_name = entry.file_name(); - let file_name = file_name.to_string_lossy().into_owned(); - if Path::new(&file_name) - .extension() - .and_then(|extension| extension.to_str()) - .map(|extension| extension.eq_ignore_ascii_case("cty")) - != Some(true) - { - continue; - } - let bytes = fs::read(entry.path())?; - entries.push(parse_cargo_type_entry(&file_name, &bytes)?); - } - entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name)); - - let mut notes = Vec::new(); - notes.push( - "CargoTypes entries carry a 0x03ea header and an inline NUL-terminated cargo token string." - .to_string(), - ); - notes.push( - "A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix." - .to_string(), - ); - - let unique_visible_name_count = entries - .iter() - .map(|entry| entry.name.visible_name.as_str()) - .collect::>() - .len(); - - Ok(CargoTypeInspectionReport { - directory_path: path.display().to_string(), - entry_count: entries.len(), - unique_visible_name_count, - notes, - entries, - }) -} - -pub fn inspect_cargo_skin_pk4( - path: &Path, -) -> Result> { - let bytes = fs::read(path)?; - let inspection = inspect_pk4_bytes(&bytes)?; - let mut entries = Vec::new(); - for entry in &inspection.entries { - if entry.extension.as_deref() != Some("dsc") { - continue; - } - let payload = bytes - .get(entry.payload_absolute_offset..entry.payload_end_offset) - .ok_or_else(|| format!("pk4 payload range out of bounds for {}", entry.name))?; - let parsed = parse_cargo_skin_descriptor_entry(&entry.name, payload)?; - entries.push(parsed); - } - entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name)); - - let mut notes = Vec::new(); - notes.push( - "cargoSkin descriptors are parsed from .dsc payloads whose first non-empty line names the descriptor kind and whose second non-empty line carries the cargo token." - .to_string(), - ); - notes.push( - "A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix." - .to_string(), - ); - - let unique_visible_name_count = entries - .iter() - .map(|entry| entry.name.visible_name.as_str()) - .collect::>() - .len(); - - Ok(CargoSkinInspectionReport { - pk4_path: path.display().to_string(), - entry_count: entries.len(), - unique_visible_name_count, - notes, - entries, - }) -} - -pub fn inspect_cargo_economy_sources( - cargo_types_dir: &Path, - cargo_skin_pk4_path: &Path, -) -> Result> { - inspect_cargo_economy_sources_with_bindings(cargo_types_dir, cargo_skin_pk4_path, None) -} - -pub fn inspect_cargo_economy_sources_with_bindings( - cargo_types_dir: &Path, - cargo_skin_pk4_path: &Path, - cargo_bindings_path: Option<&Path>, -) -> Result> { - let cargo_types = inspect_cargo_types_dir(cargo_types_dir)?; - let cargo_skins = inspect_cargo_skin_pk4(cargo_skin_pk4_path)?; - let cargo_bindings = load_cargo_bindings(cargo_bindings_path)?; - Ok(build_cargo_economy_source_report( - cargo_types, - cargo_skins, - cargo_bindings.as_deref(), - )) -} - -fn build_cargo_economy_source_report( - cargo_types: CargoTypeInspectionReport, - cargo_skins: CargoSkinInspectionReport, - cargo_bindings: Option<&[CargoBindingRow]>, -) -> CargoEconomySourceReport { - let cargo_type_visible_names = cargo_types - .entries - .iter() - .map(|entry| entry.name.visible_name.clone()) - .collect::>(); - let cargo_skin_visible_names = cargo_skins - .entries - .iter() - .map(|entry| entry.name.visible_name.clone()) - .collect::>(); - - let shared_visible_name_count = cargo_type_visible_names - .intersection(&cargo_skin_visible_names) - .count(); - let visible_name_union_count = cargo_type_visible_names - .union(&cargo_skin_visible_names) - .count(); - let cargo_type_only_visible_names = cargo_type_visible_names - .difference(&cargo_skin_visible_names) - .cloned() - .collect::>(); - let cargo_skin_only_visible_names = cargo_skin_visible_names - .difference(&cargo_type_visible_names) - .cloned() - .collect::>(); - let live_registry_entries = - build_live_registry_entries(&cargo_types.entries, &cargo_skins.entries); - let production_selector = cargo_bindings.and_then(|bindings| { - build_selector_from_bindings( - bindings, - &live_registry_entries, - "cargo_production_named", - "named_cargo_production", - NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT, - "This selector is grounded from the checked-in named cargo production bindings artifact.", - "The current grounded order matches the 50-row named cargo-production descriptor strip.", - ) - }); - let price_selector = cargo_bindings - .and_then(|bindings| { - build_selector_from_bindings( - bindings, - &live_registry_entries, - "cargo_price_named", - "named_cargo_price", - NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, - "This selector is grounded from the checked-in named cargo price bindings artifact.", - "The current grounded order matches the 71-row named cargo-price descriptor strip.", - ) - }) - .unwrap_or_else(|| build_price_selector_candidate_registry(&live_registry_entries)); - let price_selector_candidate_only_visible_names = price_selector - .entries - .iter() - .map(|entry| entry.visible_name.as_str()) - .collect::>(); - let price_selector_candidate_only_visible_names = live_registry_entries - .iter() - .filter(|entry| { - !price_selector_candidate_only_visible_names.contains(entry.visible_name.as_str()) - }) - .map(|entry| entry.visible_name.clone()) - .collect::>(); - let price_selector_candidate_excess_count = live_registry_entries - .len() - .saturating_sub(NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT); - - let mut notes = Vec::new(); - notes.push(format!( - "Named cargo-price descriptors 106..176 span {} rows, while named cargo-production descriptors 180..229 span {} rows.", - NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT - )); - notes.push(format!( - "The inspected CargoTypes corpus exposes {} visible names, the inspected cargoSkin corpus exposes {} visible names, and their union exposes {} visible names.", - cargo_types.unique_visible_name_count, cargo_skins.unique_visible_name_count, visible_name_union_count - )); - if !price_selector.exact_resolution { - notes.push( - "That visible-name union still does not match the 71-row named cargo-price strip, so this offline source reconstruction is groundwork rather than a complete price-selector binding." - .to_string(), - ); - } else { - notes.push( - "The checked-in bindings now close the 71-row named cargo-price strip on top of the merged live cargo registry." - .to_string(), - ); - } - if cargo_types.unique_visible_name_count == NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT { - notes.push( - "The CargoTypes corpus still matches the 50-row named cargo-production strip cardinality that grounds the current production bindings." - .to_string(), - ); - } - - CargoEconomySourceReport { - cargo_types_dir: cargo_types.directory_path, - cargo_skin_pk4_path: cargo_skins.pk4_path, - named_cargo_price_row_count: NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, - named_cargo_production_row_count: NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT, - cargo_type_count: cargo_types.entry_count, - cargo_skin_count: cargo_skins.entry_count, - shared_visible_name_count, - visible_name_union_count, - cargo_type_only_visible_names, - cargo_skin_only_visible_names, - live_registry_count: live_registry_entries.len(), - live_registry_entries, - price_selector_candidate_excess_count, - price_selector_candidate_only_visible_names, - production_selector, - price_selector, - notes, - cargo_type_entries: cargo_types.entries, - cargo_skin_entries: cargo_skins.entries, - } -} - -fn build_live_registry_entries( - cargo_type_entries: &[CargoTypeEntry], - cargo_skin_entries: &[CargoSkinDescriptorEntry], -) -> Vec { - let mut visible_names = cargo_type_entries - .iter() - .map(|entry| entry.name.visible_name.clone()) - .chain( - cargo_skin_entries - .iter() - .map(|entry| entry.name.visible_name.clone()), - ) - .collect::>() - .into_iter() - .collect::>(); - visible_names.sort(); - - visible_names - .into_iter() - .map(|visible_name| { - let mut raw_names = cargo_type_entries - .iter() - .filter(|entry| entry.name.visible_name == visible_name) - .map(|entry| entry.name.raw_name.clone()) - .chain( - cargo_skin_entries - .iter() - .filter(|entry| entry.name.visible_name == visible_name) - .map(|entry| entry.name.raw_name.clone()), - ) - .collect::>() - .into_iter() - .collect::>(); - raw_names.sort(); - - let localized_string_ids = cargo_type_entries - .iter() - .filter(|entry| entry.name.visible_name == visible_name) - .filter_map(|entry| entry.name.localized_string_id) - .chain( - cargo_skin_entries - .iter() - .filter(|entry| entry.name.visible_name == visible_name) - .filter_map(|entry| entry.name.localized_string_id), - ) - .collect::>() - .into_iter() - .collect::>(); - - let mut source_kinds = Vec::new(); - if cargo_type_entries - .iter() - .any(|entry| entry.name.visible_name == visible_name) - { - source_kinds.push(CargoRegistrySourceKind::CargoTypes); - } - if cargo_skin_entries - .iter() - .any(|entry| entry.name.visible_name == visible_name) - { - source_kinds.push(CargoRegistrySourceKind::CargoSkin); - } - - CargoLiveRegistryEntry { - visible_name, - raw_names, - localized_string_ids, - source_kinds, - } - }) - .collect() -} - -fn build_selector_from_bindings( - bindings: &[CargoBindingRow], - live_registry_entries: &[CargoLiveRegistryEntry], - band: &str, - selector_kind: &str, - selector_row_count: usize, - grounding_note: &str, - order_note: &str, -) -> Option { - let mut rows = bindings - .iter() - .filter(|binding| binding.band == band) - .collect::>(); - rows.sort_by_key(|binding| binding.binding_index); - if rows.is_empty() { - return None; - } - - let entries = rows - .into_iter() - .map(|binding| CargoSelectorEntry { - selector_index: binding.binding_index, - descriptor_id: Some(binding.descriptor_id), - visible_name: binding.cargo_name.clone(), - source_kinds: live_registry_entries - .iter() - .find(|entry| entry.visible_name == binding.cargo_name) - .map(|entry| entry.source_kinds.clone()) - .unwrap_or_default(), - }) - .collect::>(); - - Some(CargoSelectorReport { - selector_kind: selector_kind.to_string(), - exact_resolution: entries.len() == selector_row_count, - selector_row_count, - candidate_registry_count: live_registry_entries.len(), - notes: vec![grounding_note.to_string(), order_note.to_string()], - entries, - }) -} - -fn build_price_selector_candidate_registry( - live_registry_entries: &[CargoLiveRegistryEntry], -) -> CargoSelectorReport { - let entries = live_registry_entries - .iter() - .enumerate() - .map(|(index, entry)| CargoSelectorEntry { - selector_index: index + 1, - descriptor_id: None, - visible_name: entry.visible_name.clone(), - source_kinds: entry.source_kinds.clone(), - }) - .collect::>(); - let candidate_registry_count = entries.len(); - let exact_resolution = candidate_registry_count == NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT; - let mut notes = Vec::new(); - notes.push( - "This is the current merged visible-name registry, sorted lexicographically, not a claimed reproduction of the original price selector." - .to_string(), - ); - if exact_resolution { - notes.push( - "The merged visible-name registry cardinality matches the 71-row named cargo-price descriptor strip." - .to_string(), - ); - } else { - notes.push(format!( - "The merged visible-name registry has {} entries, so the exact 71-row price-selector binding remains unresolved by static source recovery alone.", - candidate_registry_count - )); - let excess = - candidate_registry_count.saturating_sub(NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT); - notes.push(format!( - "Current unresolved gap is {} excess candidate names relative to the descriptor strip.", - excess - )); - } - - CargoSelectorReport { - selector_kind: "named_cargo_price_candidate_registry".to_string(), - exact_resolution, - selector_row_count: NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, - candidate_registry_count, - notes, - entries, - } -} - -fn load_cargo_bindings( - path: Option<&Path>, -) -> Result>, Box> { - let Some(path) = path else { - return Ok(None); - }; - if !path.exists() { - return Ok(None); - } - let artifact: CargoBindingArtifact = serde_json::from_str(&fs::read_to_string(path)?)?; - Ok(Some(artifact.bindings)) -} - -fn parse_cargo_type_entry( - file_name: &str, - bytes: &[u8], -) -> Result> { - if bytes.len() < 5 { - return Err(format!("cargo type entry {file_name} is too short").into()); - } - let header_magic = u32::from_le_bytes(bytes[0..4].try_into().expect("length checked")); - let raw_name = parse_nul_terminated_utf8(bytes, 4) - .ok_or_else(|| format!("cargo type entry {file_name} is missing a NUL-terminated name"))?; - - Ok(CargoTypeEntry { - file_name: file_name.to_string(), - file_size: bytes.len(), - file_size_hex: format!("0x{:x}", bytes.len()), - header_magic, - header_magic_hex: format!("0x{header_magic:08x}"), - name: parse_cargo_name_token(&raw_name), - }) -} - -fn parse_cargo_skin_descriptor_entry( - entry_name: &str, - bytes: &[u8], -) -> Result> { - let text = std::str::from_utf8(bytes)?; - let mut lines = text.lines().map(str::trim).filter(|line| !line.is_empty()); - let descriptor_kind = lines - .next() - .ok_or_else(|| format!("cargo skin descriptor {entry_name} is missing the kind line"))?; - let raw_name = lines - .next() - .ok_or_else(|| format!("cargo skin descriptor {entry_name} is missing the name line"))?; - - Ok(CargoSkinDescriptorEntry { - pk4_entry_name: entry_name.to_string(), - payload_len: bytes.len(), - payload_len_hex: format!("0x{:x}", bytes.len()), - descriptor_kind: descriptor_kind.to_string(), - name: parse_cargo_name_token(raw_name), - }) -} - -fn parse_nul_terminated_utf8(bytes: &[u8], offset: usize) -> Option { - let tail = bytes.get(offset..)?; - let end = tail.iter().position(|byte| *byte == 0)?; - String::from_utf8(tail[..end].to_vec()).ok() -} - -fn parse_cargo_name_token(raw_name: &str) -> CargoNameToken { - let mut visible_name = raw_name.to_string(); - let mut localized_string_id = None; - if let Some(rest) = raw_name.strip_prefix('~') { - let digits = rest - .chars() - .take_while(|character| character.is_ascii_digit()) - .collect::(); - if !digits.is_empty() { - localized_string_id = digits.parse::().ok(); - visible_name = rest[digits.len()..].to_string(); - } - } - - CargoNameToken { - raw_name: raw_name.to_string(), - visible_name, - localized_string_id, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_plain_cargo_type_entry() { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&CARGO_TYPE_MAGIC.to_le_bytes()); - bytes.extend_from_slice(b"Alcohol\0"); - let entry = parse_cargo_type_entry("Alcohol.cty", &bytes).expect("entry should parse"); - assert_eq!(entry.header_magic, CARGO_TYPE_MAGIC); - assert_eq!(entry.name.raw_name, "Alcohol"); - assert_eq!(entry.name.visible_name, "Alcohol"); - assert_eq!(entry.name.localized_string_id, None); - } - - #[test] - fn parses_localized_cargo_type_entry() { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&CARGO_TYPE_MAGIC.to_le_bytes()); - bytes.extend_from_slice(b"~4465Ceramics\0"); - let entry = - parse_cargo_type_entry("~4465Ceramics.cty", &bytes).expect("entry should parse"); - assert_eq!(entry.name.raw_name, "~4465Ceramics"); - assert_eq!(entry.name.visible_name, "Ceramics"); - assert_eq!(entry.name.localized_string_id, Some(4465)); - } - - #[test] - fn parses_cargo_skin_descriptor_entry() { - let entry = parse_cargo_skin_descriptor_entry("Alcohol.dsc", b"cargoSkin\r\nAlcohol\r\n") - .expect("descriptor should parse"); - assert_eq!(entry.descriptor_kind, "cargoSkin"); - assert_eq!(entry.name.visible_name, "Alcohol"); - } - - #[test] - fn builds_cargo_source_report_union_counts() { - let cargo_types = CargoTypeInspectionReport { - directory_path: "CargoTypes".to_string(), - entry_count: 2, - unique_visible_name_count: 2, - notes: Vec::new(), - entries: vec![ - CargoTypeEntry { - file_name: "Alcohol.cty".to_string(), - file_size: 16, - file_size_hex: "0x10".to_string(), - header_magic: CARGO_TYPE_MAGIC, - header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), - name: parse_cargo_name_token("Alcohol"), - }, - CargoTypeEntry { - file_name: "Coal.cty".to_string(), - file_size: 16, - file_size_hex: "0x10".to_string(), - header_magic: CARGO_TYPE_MAGIC, - header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), - name: parse_cargo_name_token("Coal"), - }, - ], - }; - let cargo_skins = CargoSkinInspectionReport { - pk4_path: "Cargo106.PK4".to_string(), - entry_count: 2, - unique_visible_name_count: 2, - notes: Vec::new(), - entries: vec![ - CargoSkinDescriptorEntry { - pk4_entry_name: "Alcohol.dsc".to_string(), - payload_len: 20, - payload_len_hex: "0x14".to_string(), - descriptor_kind: "cargoSkin".to_string(), - name: parse_cargo_name_token("Alcohol"), - }, - CargoSkinDescriptorEntry { - pk4_entry_name: "Beer.dsc".to_string(), - payload_len: 16, - payload_len_hex: "0x10".to_string(), - descriptor_kind: "cargoSkin".to_string(), - name: parse_cargo_name_token("Beer"), - }, - ], - }; - - let report = build_cargo_economy_source_report(cargo_types, cargo_skins, None); - assert_eq!(report.shared_visible_name_count, 1); - assert_eq!(report.visible_name_union_count, 3); - assert_eq!(report.live_registry_count, 3); - assert_eq!( - report.cargo_type_only_visible_names, - vec!["Coal".to_string()] - ); - assert_eq!( - report.cargo_skin_only_visible_names, - vec!["Beer".to_string()] - ); - assert!(!report.price_selector.exact_resolution); - assert_eq!(report.price_selector.candidate_registry_count, 3); - assert_eq!(report.price_selector_candidate_excess_count, 0); - assert!( - report - .price_selector_candidate_only_visible_names - .is_empty() - ); - assert!(report.production_selector.is_none()); - } - - #[test] - fn builds_exact_production_selector_from_bindings() { - let cargo_types = CargoTypeInspectionReport { - directory_path: "CargoTypes".to_string(), - entry_count: 2, - unique_visible_name_count: 2, - notes: Vec::new(), - entries: vec![ - CargoTypeEntry { - file_name: "Alcohol.cty".to_string(), - file_size: 16, - file_size_hex: "0x10".to_string(), - header_magic: CARGO_TYPE_MAGIC, - header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), - name: parse_cargo_name_token("Alcohol"), - }, - CargoTypeEntry { - file_name: "Coal.cty".to_string(), - file_size: 16, - file_size_hex: "0x10".to_string(), - header_magic: CARGO_TYPE_MAGIC, - header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), - name: parse_cargo_name_token("Coal"), - }, - ], - }; - let cargo_skins = CargoSkinInspectionReport { - pk4_path: "Cargo106.PK4".to_string(), - entry_count: 1, - unique_visible_name_count: 1, - notes: Vec::new(), - entries: vec![CargoSkinDescriptorEntry { - pk4_entry_name: "Alcohol.dsc".to_string(), - payload_len: 20, - payload_len_hex: "0x14".to_string(), - descriptor_kind: "cargoSkin".to_string(), - name: parse_cargo_name_token("Alcohol"), - }], - }; - let bindings = vec![ - CargoBindingRow { - descriptor_id: 180, - band: "cargo_production_named".to_string(), - cargo_name: "Alcohol".to_string(), - binding_index: 1, - }, - CargoBindingRow { - descriptor_id: 181, - band: "cargo_production_named".to_string(), - cargo_name: "Coal".to_string(), - binding_index: 2, - }, - ]; - - let report = build_cargo_economy_source_report(cargo_types, cargo_skins, Some(&bindings)); - let selector = report - .production_selector - .expect("production selector should exist"); - assert_eq!(selector.entries.len(), 2); - assert_eq!(selector.entries[0].descriptor_id, Some(180)); - assert_eq!(selector.entries[0].visible_name, "Alcohol"); - assert_eq!( - selector.entries[0].source_kinds, - vec![ - CargoRegistrySourceKind::CargoTypes, - CargoRegistrySourceKind::CargoSkin - ] - ); - assert_eq!(selector.entries[1].visible_name, "Coal"); - assert!( - report - .price_selector_candidate_only_visible_names - .is_empty() - ); - assert_eq!(report.price_selector_candidate_excess_count, 0); - } - - #[test] - fn builds_exact_price_selector_from_bindings() { - let cargo_types = CargoTypeInspectionReport { - directory_path: "CargoTypes".to_string(), - entry_count: 2, - unique_visible_name_count: 2, - notes: Vec::new(), - entries: vec![ - CargoTypeEntry { - file_name: "Alcohol.cty".to_string(), - file_size: 16, - file_size_hex: "0x10".to_string(), - header_magic: CARGO_TYPE_MAGIC, - header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), - name: parse_cargo_name_token("Alcohol"), - }, - CargoTypeEntry { - file_name: "~4513Rock.cty".to_string(), - file_size: 16, - file_size_hex: "0x10".to_string(), - header_magic: CARGO_TYPE_MAGIC, - header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), - name: parse_cargo_name_token("~4513Rock"), - }, - ], - }; - let cargo_skins = CargoSkinInspectionReport { - pk4_path: "Cargo106.PK4".to_string(), - entry_count: 2, - unique_visible_name_count: 2, - notes: Vec::new(), - entries: vec![ - CargoSkinDescriptorEntry { - pk4_entry_name: "Alcohol.dsc".to_string(), - payload_len: 20, - payload_len_hex: "0x14".to_string(), - descriptor_kind: "cargoSkin".to_string(), - name: parse_cargo_name_token("Alcohol"), - }, - CargoSkinDescriptorEntry { - pk4_entry_name: "~4459Beer.dsc".to_string(), - payload_len: 20, - payload_len_hex: "0x14".to_string(), - descriptor_kind: "cargoSkin".to_string(), - name: parse_cargo_name_token("~4459Beer"), - }, - ], - }; - let bindings = vec![ - CargoBindingRow { - descriptor_id: 106, - band: "cargo_price_named".to_string(), - cargo_name: "Alcohol".to_string(), - binding_index: 1, - }, - CargoBindingRow { - descriptor_id: 107, - band: "cargo_price_named".to_string(), - cargo_name: "Beer".to_string(), - binding_index: 2, - }, - CargoBindingRow { - descriptor_id: 108, - band: "cargo_price_named".to_string(), - cargo_name: "Rock".to_string(), - binding_index: 3, - }, - ]; - - let report = build_cargo_economy_source_report(cargo_types, cargo_skins, Some(&bindings)); - assert_eq!(report.price_selector.selector_kind, "named_cargo_price"); - assert!(!report.price_selector.exact_resolution); - assert_eq!(report.price_selector.entries.len(), 3); - assert_eq!(report.price_selector.entries[2].visible_name, "Rock"); - assert_eq!(report.price_selector.entries[2].source_kinds.len(), 1); - assert_eq!( - report.price_selector_candidate_only_visible_names, - Vec::::new() - ); - } -} diff --git a/crates/rrt-runtime/src/engine/advance.rs b/crates/rrt-runtime/src/engine/advance.rs new file mode 100644 index 0000000..47bfeba --- /dev/null +++ b/crates/rrt-runtime/src/engine/advance.rs @@ -0,0 +1,93 @@ +use crate::calendar::BoundaryEventKind; +use crate::engine::command::{BoundaryEvent, ServiceEvent}; +use crate::engine::service::service_periodic_boundary; +use crate::state::{CalendarPoint, RuntimeState}; + +pub(super) fn advance_to_target_calendar_point( + state: &mut RuntimeState, + target: crate::CalendarPoint, + boundary_events: &mut Vec, + service_events: &mut Vec, +) -> Result { + target.validate()?; + if target < state.calendar { + return Err(format!( + "advance_to target {:?} is earlier than current calendar {:?}", + target, state.calendar + )); + } + + let mut steps = 0_u64; + while state.calendar < target { + step_once(state, boundary_events, service_events)?; + steps += 1; + } + Ok(steps) +} + +pub(super) fn step_count( + state: &mut RuntimeState, + steps: u32, + boundary_events: &mut Vec, + service_events: &mut Vec, +) -> Result { + for _ in 0..steps { + step_once(state, boundary_events, service_events)?; + } + Ok(steps.into()) +} + +pub(super) fn step_once( + state: &mut RuntimeState, + boundary_events: &mut Vec, + service_events: &mut Vec, +) -> Result<(), String> { + let prior_calendar = state.calendar; + let boundary = state.calendar.step_forward(); + if boundary != BoundaryEventKind::Tick { + boundary_events.push(BoundaryEvent { + kind: boundary_kind_label(boundary).to_string(), + calendar: state.calendar, + }); + } + if boundary == BoundaryEventKind::YearRollover { + service_sync_world_restore_time_from_calendar(state, prior_calendar); + service_periodic_boundary(state, service_events)?; + } + service_sync_world_restore_time_from_calendar(state, state.calendar); + Ok(()) +} + +pub(super) fn boundary_kind_label(boundary: BoundaryEventKind) -> &'static str { + match boundary { + BoundaryEventKind::Tick => "tick", + BoundaryEventKind::PhaseRollover => "phase_rollover", + BoundaryEventKind::MonthRollover => "month_rollover", + BoundaryEventKind::YearRollover => "year_rollover", + } +} + +pub(super) fn service_sync_world_restore_time_from_calendar( + state: &mut RuntimeState, + calendar: CalendarPoint, +) { + if let Ok(year_word) = u16::try_from(calendar.year) { + state.world_restore.packed_year_word_raw_u16 = Some(year_word); + } + if let Ok(partial_year_progress) = u8::try_from(calendar.month_slot.saturating_add(1)) { + state.world_restore.partial_year_progress_raw_u8 = Some(partial_year_progress); + } + if let Some(tuple) = + crate::derived::runtime_derive_packed_calendar_tuple_from_calendar_point(calendar) + { + let (word_0, word_1) = crate::derived::runtime_encode_packed_calendar_tuple(tuple); + state.world_restore.current_calendar_tuple_word_raw_u32 = Some(word_0); + state.world_restore.current_calendar_tuple_word_2_raw_u32 = Some(word_1); + if let Some(absolute_counter) = + crate::derived::runtime_pack_packed_calendar_tuple_to_absolute_counter(tuple) + { + state.world_restore.absolute_counter_raw_u32 = Some(absolute_counter); + state.world_restore.absolute_counter_mirror_raw_u32 = Some(absolute_counter); + } + } +} diff --git a/crates/rrt-runtime/src/engine/command.rs b/crates/rrt-runtime/src/engine/command.rs new file mode 100644 index 0000000..0461a46 --- /dev/null +++ b/crates/rrt-runtime/src/engine/command.rs @@ -0,0 +1,112 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::engine::advance::{advance_to_target_calendar_point, step_count}; +use crate::engine::service::{service_periodic_boundary, service_trigger_kind}; +use crate::event::news::{RuntimeAnnualFinanceNewsEvent, RuntimeNearCityAcquisitionNewsEvent}; +use crate::state::RuntimeState; +use crate::summary::RuntimeSummary; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum StepCommand { + AdvanceTo { calendar: crate::CalendarPoint }, + StepCount { steps: u32 }, + ServiceTriggerKind { trigger_kind: u8 }, + ServicePeriodicBoundary, +} + +impl StepCommand { + pub fn validate(&self) -> Result<(), String> { + match self { + Self::AdvanceTo { calendar } => calendar.validate(), + Self::StepCount { steps } => { + if *steps == 0 { + return Err("step_count command requires steps > 0".to_string()); + } + Ok(()) + } + Self::ServiceTriggerKind { .. } | Self::ServicePeriodicBoundary => Ok(()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BoundaryEvent { + pub kind: String, + pub calendar: crate::CalendarPoint, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServiceEvent { + pub kind: String, + pub trigger_kind: Option, + pub serviced_record_ids: Vec, + pub applied_effect_count: u32, + pub mutated_company_ids: Vec, + pub mutated_player_ids: Vec, + pub appended_record_ids: Vec, + pub activated_record_ids: Vec, + pub deactivated_record_ids: Vec, + pub removed_record_ids: Vec, + #[serde(default)] + pub finance_news_family_candidates: BTreeMap, + #[serde(default)] + pub annual_finance_news_events: Vec, + #[serde(default)] + pub near_city_acquisition_news_family_candidates: BTreeMap, + #[serde(default)] + pub near_city_acquisition_news_events: Vec, + pub dirty_rerun: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct StepResult { + pub initial_summary: RuntimeSummary, + pub final_summary: RuntimeSummary, + pub steps_executed: u64, + pub boundary_events: Vec, + pub service_events: Vec, +} +pub fn execute_step_command( + state: &mut RuntimeState, + command: &StepCommand, +) -> Result { + state.validate()?; + command.validate()?; + + let initial_summary = RuntimeSummary::from_state(state); + let mut boundary_events = Vec::new(); + let mut service_events = Vec::new(); + let steps_executed = match command { + StepCommand::AdvanceTo { calendar } => advance_to_target_calendar_point( + state, + *calendar, + &mut boundary_events, + &mut service_events, + )?, + StepCommand::StepCount { steps } => { + step_count(state, *steps, &mut boundary_events, &mut service_events)? + } + StepCommand::ServiceTriggerKind { trigger_kind } => { + service_trigger_kind(state, *trigger_kind, &mut service_events)?; + 0 + } + StepCommand::ServicePeriodicBoundary => { + service_periodic_boundary(state, &mut service_events)?; + 0 + } + }; + state.refresh_derived_world_state(); + state.refresh_derived_market_state(); + let final_summary = RuntimeSummary::from_state(state); + + Ok(StepResult { + initial_summary, + final_summary, + steps_executed, + boundary_events, + service_events, + }) +} diff --git a/crates/rrt-runtime/src/engine/conditions/catalogs.rs b/crates/rrt-runtime/src/engine/conditions/catalogs.rs new file mode 100644 index 0000000..1fc0e6e --- /dev/null +++ b/crates/rrt-runtime/src/engine/conditions/catalogs.rs @@ -0,0 +1,88 @@ +use crate::engine::metrics::{cargo_production_total_for_class, compare_condition_value}; +use crate::event::conditions::RuntimeConditionComparator; +use crate::event::targets::RuntimeCargoClass; +use crate::state::RuntimeState; + +pub(super) fn candidate_availability_threshold_matches( + state: &RuntimeState, + name: &str, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .candidate_availability + .get(name) + .copied() + .map(i64::from) + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn named_locomotive_availability_threshold_matches( + state: &RuntimeState, + name: &str, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .named_locomotive_availability + .get(name) + .copied() + .map(i64::from) + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn named_locomotive_cost_threshold_matches( + state: &RuntimeState, + name: &str, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .named_locomotive_cost + .get(name) + .copied() + .map(i64::from) + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn cargo_production_slot_threshold_matches( + state: &RuntimeState, + slot: u32, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .cargo_production_overrides + .get(&slot) + .copied() + .map(i64::from) + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn cargo_production_total_threshold_matches( + state: &RuntimeState, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .cargo_production_overrides + .values() + .copied() + .map(i64::from) + .sum::(); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn cargo_production_class_threshold_matches( + state: &RuntimeState, + cargo_class: RuntimeCargoClass, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = cargo_production_total_for_class(state, cargo_class); + compare_condition_value(actual, comparator, value) +} diff --git a/crates/rrt-runtime/src/engine/conditions/context.rs b/crates/rrt-runtime/src/engine/conditions/context.rs new file mode 100644 index 0000000..b152751 --- /dev/null +++ b/crates/rrt-runtime/src/engine/conditions/context.rs @@ -0,0 +1,50 @@ +use std::collections::BTreeSet; + +#[derive(Debug, Default)] +pub(in crate::engine) struct ResolvedConditionContext { + pub(in crate::engine) matching_company_ids: BTreeSet, + pub(in crate::engine) matching_player_ids: BTreeSet, + pub(in crate::engine) matching_chairman_profile_ids: BTreeSet, +} + +pub(super) fn intersect_company_matches( + company_matches: &mut Option>, + next: BTreeSet, +) { + match company_matches { + Some(existing) => { + existing.retain(|company_id| next.contains(company_id)); + } + None => { + *company_matches = Some(next); + } + } +} + +pub(super) fn intersect_player_matches( + player_matches: &mut Option>, + next: BTreeSet, +) { + match player_matches { + Some(existing) => { + existing.retain(|player_id| next.contains(player_id)); + } + None => { + *player_matches = Some(next); + } + } +} + +pub(super) fn intersect_chairman_matches( + chairman_matches: &mut Option>, + next: BTreeSet, +) { + match chairman_matches { + Some(existing) => { + existing.retain(|profile_id| next.contains(profile_id)); + } + None => { + *chairman_matches = Some(next); + } + } +} diff --git a/crates/rrt-runtime/src/engine/conditions/entities.rs b/crates/rrt-runtime/src/engine/conditions/entities.rs new file mode 100644 index 0000000..240242c --- /dev/null +++ b/crates/rrt-runtime/src/engine/conditions/entities.rs @@ -0,0 +1,140 @@ +use std::collections::BTreeSet; + +use crate::engine::conditions::ResolvedConditionContext; +use crate::engine::metrics::{ + chairman_metric_value, company_metric_value, company_territory_metric_value, + compare_condition_value, +}; +use crate::engine::targets::{ + resolve_chairman_target_ids, resolve_company_target_ids, resolve_player_target_ids, + resolve_territory_target_ids, +}; +use crate::event::conditions::RuntimeConditionComparator; +use crate::event::metrics::{RuntimeChairmanMetric, RuntimeCompanyMetric, RuntimeTrackMetric}; +use crate::event::targets::{ + RuntimeChairmanTarget, RuntimeCompanyTarget, RuntimePlayerTarget, RuntimeTerritoryTarget, +}; +use crate::state::RuntimeState; + +pub(super) fn matching_company_numeric_threshold( + state: &RuntimeState, + target: &RuntimeCompanyTarget, + metric: RuntimeCompanyMetric, + comparator: RuntimeConditionComparator, + value: i64, +) -> Result, String> { + Ok( + resolve_company_target_ids(state, target, &ResolvedConditionContext::default())? + .into_iter() + .filter(|company_id| { + state + .companies + .iter() + .find(|company| company.company_id == *company_id) + .is_some_and(|company| { + compare_condition_value( + company_metric_value(state, company, metric), + comparator, + value, + ) + }) + }) + .collect(), + ) +} + +pub(super) fn matching_company_variable_threshold( + state: &RuntimeState, + target: &RuntimeCompanyTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, +) -> Result, String> { + Ok( + resolve_company_target_ids(state, target, &ResolvedConditionContext::default())? + .into_iter() + .filter(|company_id| { + let actual = state + .company_runtime_variables + .get(company_id) + .and_then(|vars| vars.get(&index)) + .copied() + .unwrap_or(0); + compare_condition_value(actual, comparator, value) + }) + .collect(), + ) +} + +pub(super) fn matching_player_variable_threshold( + state: &RuntimeState, + target: &RuntimePlayerTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, +) -> Result, String> { + Ok( + resolve_player_target_ids(state, target, &ResolvedConditionContext::default())? + .into_iter() + .filter(|player_id| { + let actual = state + .player_runtime_variables + .get(player_id) + .and_then(|vars| vars.get(&index)) + .copied() + .unwrap_or(0); + compare_condition_value(actual, comparator, value) + }) + .collect(), + ) +} + +pub(super) fn matching_chairman_numeric_threshold( + state: &RuntimeState, + target: &RuntimeChairmanTarget, + metric: RuntimeChairmanMetric, + comparator: RuntimeConditionComparator, + value: i64, +) -> Result, String> { + Ok( + resolve_chairman_target_ids(state, target, &ResolvedConditionContext::default())? + .into_iter() + .filter(|profile_id| { + state + .chairman_profiles + .iter() + .find(|profile| profile.profile_id == *profile_id) + .is_some_and(|profile| { + compare_condition_value( + chairman_metric_value(profile, metric), + comparator, + value, + ) + }) + }) + .collect(), + ) +} + +pub(super) fn matching_company_territory_numeric_threshold( + state: &RuntimeState, + target: &RuntimeCompanyTarget, + territory: &RuntimeTerritoryTarget, + metric: RuntimeTrackMetric, + comparator: RuntimeConditionComparator, + value: i64, +) -> Result, String> { + let territory_ids = resolve_territory_target_ids(state, territory)?; + Ok( + resolve_company_target_ids(state, target, &ResolvedConditionContext::default())? + .into_iter() + .filter(|company_id| { + compare_condition_value( + company_territory_metric_value(state, *company_id, &territory_ids, metric), + comparator, + value, + ) + }) + .collect(), + ) +} diff --git a/crates/rrt-runtime/src/engine/conditions/entrypoints.rs b/crates/rrt-runtime/src/engine/conditions/entrypoints.rs new file mode 100644 index 0000000..ecd3122 --- /dev/null +++ b/crates/rrt-runtime/src/engine/conditions/entrypoints.rs @@ -0,0 +1,317 @@ +use std::collections::BTreeSet; + +use crate::engine::conditions::ResolvedConditionContext; +use crate::event::conditions::RuntimeCondition; +use crate::event::targets::RuntimeCargoClass; +use crate::state::RuntimeState; + +use super::{catalogs, context, entities, territories, world_state}; + +pub(in crate::engine) fn evaluate_record_conditions( + state: &RuntimeState, + conditions: &[RuntimeCondition], +) -> Result, String> { + if conditions.is_empty() { + return Ok(Some(ResolvedConditionContext::default())); + } + + let mut company_matches: Option> = None; + let mut player_matches: Option> = None; + let mut chairman_matches: Option> = None; + + for condition in conditions { + match condition { + RuntimeCondition::WorldVariableThreshold { + index, + comparator, + value, + } => { + if !world_state::world_variable_threshold_matches( + state, + *index, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::CompanyNumericThreshold { + target, + metric, + comparator, + value, + } => { + let matching = entities::matching_company_numeric_threshold( + state, + target, + *metric, + *comparator, + *value, + )?; + if matching.is_empty() { + return Ok(None); + } + context::intersect_company_matches(&mut company_matches, matching); + if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { + return Ok(None); + } + } + RuntimeCondition::CompanyVariableThreshold { + target, + index, + comparator, + value, + } => { + let matching = entities::matching_company_variable_threshold( + state, + target, + *index, + *comparator, + *value, + )?; + if matching.is_empty() { + return Ok(None); + } + context::intersect_company_matches(&mut company_matches, matching); + if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { + return Ok(None); + } + } + RuntimeCondition::TerritoryNumericThreshold { + target, + metric, + comparator, + value, + } => { + if !territories::territory_numeric_threshold_matches( + state, + target, + *metric, + *comparator, + *value, + )? { + return Ok(None); + } + } + RuntimeCondition::TerritoryVariableThreshold { + target, + index, + comparator, + value, + } => { + if !territories::territory_variable_threshold_matches( + state, + target, + *index, + *comparator, + *value, + )? { + return Ok(None); + } + } + RuntimeCondition::PlayerVariableThreshold { + target, + index, + comparator, + value, + } => { + let matching = entities::matching_player_variable_threshold( + state, + target, + *index, + *comparator, + *value, + )?; + if matching.is_empty() { + return Ok(None); + } + context::intersect_player_matches(&mut player_matches, matching); + if player_matches.as_ref().is_some_and(BTreeSet::is_empty) { + return Ok(None); + } + } + RuntimeCondition::ChairmanNumericThreshold { + target, + metric, + comparator, + value, + } => { + let matching = entities::matching_chairman_numeric_threshold( + state, + target, + *metric, + *comparator, + *value, + )?; + if matching.is_empty() { + return Ok(None); + } + context::intersect_chairman_matches(&mut chairman_matches, matching); + if chairman_matches.as_ref().is_some_and(BTreeSet::is_empty) { + return Ok(None); + } + } + RuntimeCondition::CompanyTerritoryNumericThreshold { + target, + territory, + metric, + comparator, + value, + } => { + let matching = entities::matching_company_territory_numeric_threshold( + state, + target, + territory, + *metric, + *comparator, + *value, + )?; + if matching.is_empty() { + return Ok(None); + } + context::intersect_company_matches(&mut company_matches, matching); + if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { + return Ok(None); + } + } + RuntimeCondition::SpecialConditionThreshold { + label, + comparator, + value, + } => { + if !world_state::special_condition_threshold_matches( + state, + label, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::CandidateAvailabilityThreshold { + name, + comparator, + value, + } => { + if !catalogs::candidate_availability_threshold_matches( + state, + name, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name, + comparator, + value, + } => { + if !catalogs::named_locomotive_availability_threshold_matches( + state, + name, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::NamedLocomotiveCostThreshold { + name, + comparator, + value, + } => { + if !catalogs::named_locomotive_cost_threshold_matches( + state, + name, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::CargoProductionSlotThreshold { + slot, + comparator, + value, + .. + } => { + if !catalogs::cargo_production_slot_threshold_matches( + state, + *slot, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => { + if !catalogs::cargo_production_total_threshold_matches(state, *comparator, *value) { + return Ok(None); + } + } + RuntimeCondition::FactoryProductionTotalThreshold { comparator, value } => { + if !catalogs::cargo_production_class_threshold_matches( + state, + RuntimeCargoClass::Factory, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value } => { + if !catalogs::cargo_production_class_threshold_matches( + state, + RuntimeCargoClass::FarmMine, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value } => { + if !catalogs::cargo_production_class_threshold_matches( + state, + RuntimeCargoClass::Other, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => { + if !world_state::limited_track_building_amount_threshold_matches( + state, + *comparator, + *value, + ) { + return Ok(None); + } + } + RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => { + if !world_state::territory_access_cost_threshold_matches(state, *comparator, *value) + { + return Ok(None); + } + } + RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => { + if !world_state::economic_status_code_threshold_matches(state, *comparator, *value) + { + return Ok(None); + } + } + RuntimeCondition::WorldFlagEquals { key, value } => { + if !world_state::world_flag_equals(state, key, *value) { + return Ok(None); + } + } + } + } + + Ok(Some(ResolvedConditionContext { + matching_company_ids: company_matches.unwrap_or_default(), + matching_player_ids: player_matches.unwrap_or_default(), + matching_chairman_profile_ids: chairman_matches.unwrap_or_default(), + })) +} diff --git a/crates/rrt-runtime/src/engine/conditions/mod.rs b/crates/rrt-runtime/src/engine/conditions/mod.rs new file mode 100644 index 0000000..971ff08 --- /dev/null +++ b/crates/rrt-runtime/src/engine/conditions/mod.rs @@ -0,0 +1,9 @@ +mod catalogs; +mod context; +mod entities; +mod entrypoints; +mod territories; +mod world_state; + +pub(super) use self::context::ResolvedConditionContext; +pub(super) use self::entrypoints::evaluate_record_conditions; diff --git a/crates/rrt-runtime/src/engine/conditions/territories.rs b/crates/rrt-runtime/src/engine/conditions/territories.rs new file mode 100644 index 0000000..c21f0fe --- /dev/null +++ b/crates/rrt-runtime/src/engine/conditions/territories.rs @@ -0,0 +1,40 @@ +use crate::engine::metrics::{compare_condition_value, territory_metric_value}; +use crate::engine::targets::resolve_territory_target_ids; +use crate::event::conditions::RuntimeConditionComparator; +use crate::event::metrics::RuntimeTerritoryMetric; +use crate::event::targets::RuntimeTerritoryTarget; +use crate::state::RuntimeState; + +pub(super) fn territory_numeric_threshold_matches( + state: &RuntimeState, + target: &RuntimeTerritoryTarget, + metric: RuntimeTerritoryMetric, + comparator: RuntimeConditionComparator, + value: i64, +) -> Result { + let territory_ids = resolve_territory_target_ids(state, target)?; + let actual = territory_metric_value(state, &territory_ids, metric); + Ok(compare_condition_value(actual, comparator, value)) +} + +pub(super) fn territory_variable_threshold_matches( + state: &RuntimeState, + target: &RuntimeTerritoryTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, +) -> Result { + let territory_ids = resolve_territory_target_ids(state, target)?; + let actual = territory_ids + .iter() + .map(|territory_id| { + state + .territory_runtime_variables + .get(territory_id) + .and_then(|vars| vars.get(&index)) + .copied() + .unwrap_or(0) + }) + .sum::(); + Ok(compare_condition_value(actual, comparator, value)) +} diff --git a/crates/rrt-runtime/src/engine/conditions/world_state.rs b/crates/rrt-runtime/src/engine/conditions/world_state.rs new file mode 100644 index 0000000..0a4d5f1 --- /dev/null +++ b/crates/rrt-runtime/src/engine/conditions/world_state.rs @@ -0,0 +1,75 @@ +use crate::engine::metrics::compare_condition_value; +use crate::event::conditions::RuntimeConditionComparator; +use crate::state::RuntimeState; + +pub(super) fn world_variable_threshold_matches( + state: &RuntimeState, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .world_runtime_variables + .get(&index) + .copied() + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn special_condition_threshold_matches( + state: &RuntimeState, + label: &str, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .special_conditions + .get(label) + .copied() + .map(i64::from) + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn limited_track_building_amount_threshold_matches( + state: &RuntimeState, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .world_restore + .limited_track_building_amount + .map(i64::from) + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn territory_access_cost_threshold_matches( + state: &RuntimeState, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .world_restore + .territory_access_cost + .map(i64::from) + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn economic_status_code_threshold_matches( + state: &RuntimeState, + comparator: RuntimeConditionComparator, + value: i64, +) -> bool { + let actual = state + .world_restore + .economic_status_code + .map(i64::from) + .unwrap_or(0); + compare_condition_value(actual, comparator, value) +} + +pub(super) fn world_flag_equals(state: &RuntimeState, key: &str, value: bool) -> bool { + state.world_flags.get(key).copied().unwrap_or(false) == value +} diff --git a/crates/rrt-runtime/src/engine/effects/catalogs.rs b/crates/rrt-runtime/src/engine/effects/catalogs.rs new file mode 100644 index 0000000..d10064a --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/catalogs.rs @@ -0,0 +1,74 @@ +use crate::event::targets::{RuntimeCargoPriceTarget, RuntimeCargoProductionTarget}; +use crate::state::RuntimeState; + +pub(super) fn apply_set_candidate_availability(state: &mut RuntimeState, name: &str, value: u32) { + state.candidate_availability.insert(name.to_string(), value); +} + +pub(super) fn apply_set_named_locomotive_availability( + state: &mut RuntimeState, + name: &str, + value: bool, +) { + state + .named_locomotive_availability + .insert(name.to_string(), u32::from(value)); +} + +pub(super) fn apply_set_named_locomotive_availability_value( + state: &mut RuntimeState, + name: &str, + value: u32, +) { + state + .named_locomotive_availability + .insert(name.to_string(), value); +} + +pub(super) fn apply_set_named_locomotive_cost(state: &mut RuntimeState, name: &str, value: u32) { + state.named_locomotive_cost.insert(name.to_string(), value); +} + +pub(super) fn apply_set_cargo_price_override( + state: &mut RuntimeState, + target: &RuntimeCargoPriceTarget, + value: u32, +) { + match target { + RuntimeCargoPriceTarget::All => { + state.all_cargo_price_override = Some(value); + } + RuntimeCargoPriceTarget::Named { name } => { + state + .named_cargo_price_overrides + .insert(name.clone(), value); + } + } +} + +pub(super) fn apply_set_cargo_production_override( + state: &mut RuntimeState, + target: &RuntimeCargoProductionTarget, + value: u32, +) { + match target { + RuntimeCargoProductionTarget::All => { + state.all_cargo_production_override = Some(value); + } + RuntimeCargoProductionTarget::Factory => { + state.factory_cargo_production_override = Some(value); + } + RuntimeCargoProductionTarget::FarmMine => { + state.farm_mine_cargo_production_override = Some(value); + } + RuntimeCargoProductionTarget::Named { name } => { + state + .named_cargo_production_overrides + .insert(name.clone(), value); + } + } +} + +pub(super) fn apply_set_cargo_production_slot(state: &mut RuntimeState, slot: u32, value: u32) { + state.cargo_production_overrides.insert(slot, value); +} diff --git a/crates/rrt-runtime/src/engine/effects/company.rs b/crates/rrt-runtime/src/engine/effects/company.rs new file mode 100644 index 0000000..20752d8 --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/company.rs @@ -0,0 +1,302 @@ +use std::collections::BTreeSet; + +use crate::engine::conditions::ResolvedConditionContext; +use crate::engine::metrics::{ + apply_u64_delta, retire_matching_trains, set_company_territory_access_pairs, +}; +use crate::engine::service::{ + service_clear_company_live_bonds, service_post_company_stat_delta, + service_set_company_cached_share_price, service_set_company_credit_rating_target, + service_set_company_direct_float_field, service_set_company_issue_opinion_total, + service_set_company_prime_rate_target, service_zero_company_current_cash, +}; +use crate::engine::targets::{resolve_company_target_ids, resolve_territory_target_ids}; +use crate::event::metrics::{RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RuntimeCompanyMetric}; +use crate::event::targets::{RuntimeCompanyTarget, RuntimeTerritoryTarget}; +use crate::state::RuntimeState; + +pub(super) fn apply_set_company_cash( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + value: i64, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + let prior_cash = state + .companies + .iter() + .find(|company| company.company_id == company_id) + .map(|company| company.current_cash) + .ok_or_else(|| format!("missing company_id {company_id} while applying cash effect"))?; + if !service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + value.saturating_sub(prior_cash) as f64, + false, + ) { + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| { + format!("missing company_id {company_id} while applying cash effect") + })?; + company.current_cash = value; + } + mutated_company_ids.insert(company_id); + } + Ok(()) +} + +pub(super) fn apply_set_company_governance_scalar( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + metric: RuntimeCompanyMetric, + value: i64, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + let applied_through_owner_state = match metric { + RuntimeCompanyMetric::CreditRating => { + service_set_company_credit_rating_target(state, company_id, value) + } + RuntimeCompanyMetric::PrimeRate => { + service_set_company_prime_rate_target(state, company_id, value) + } + RuntimeCompanyMetric::BookValuePerShare => { + service_set_company_direct_float_field(state, company_id, 0x32f, value as f64) + } + RuntimeCompanyMetric::InvestorConfidence => { + service_set_company_cached_share_price(state, company_id, value as f64) + } + RuntimeCompanyMetric::ManagementAttitude => service_set_company_issue_opinion_total( + state, + company_id, + crate::event::metrics::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE, + value, + ), + _ => { + return Err(format!( + "unsupported governance metric {:?} in company governance effect", + metric + )); + } + }; + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| { + format!("missing company_id {company_id} while applying governance effect") + })?; + match metric { + RuntimeCompanyMetric::CreditRating => { + if !applied_through_owner_state { + company.credit_rating_score = Some(value); + } + } + RuntimeCompanyMetric::PrimeRate => { + if !applied_through_owner_state { + company.prime_rate = Some(value); + } + } + RuntimeCompanyMetric::BookValuePerShare => { + if !applied_through_owner_state { + company.book_value_per_share = value; + } + } + RuntimeCompanyMetric::InvestorConfidence => { + if !applied_through_owner_state { + company.investor_confidence = value; + } + } + RuntimeCompanyMetric::ManagementAttitude => { + if !applied_through_owner_state { + company.management_attitude = value; + } + } + _ => unreachable!(), + }; + mutated_company_ids.insert(company_id); + } + Ok(()) +} + +pub(super) fn apply_set_company_territory_access( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + territory: &RuntimeTerritoryTarget, + value: bool, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + let territory_ids = resolve_territory_target_ids(state, territory)?; + set_company_territory_access_pairs( + &mut state.company_territory_access, + &company_ids, + &territory_ids, + value, + ); + mutated_company_ids.extend(company_ids); + Ok(()) +} + +pub(super) fn apply_confiscate_company_assets( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids.iter().copied() { + let _ = service_zero_company_current_cash(state, company_id); + let _ = service_clear_company_live_bonds(state, company_id); + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| { + format!("missing company_id {company_id} while applying confiscate effect") + })?; + company.current_cash = 0; + company.debt = 0; + company.active = false; + mutated_company_ids.insert(company_id); + if state.selected_company_id == Some(company_id) { + state.selected_company_id = None; + } + } + retire_matching_trains(&mut state.trains, Some(&company_ids), None, None); + Ok(()) +} + +pub(super) fn apply_deactivate_company( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| { + format!("missing company_id {company_id} while applying deactivate effect") + })?; + company.active = false; + mutated_company_ids.insert(company_id); + if state.selected_company_id == Some(company_id) { + state.selected_company_id = None; + } + } + Ok(()) +} + +pub(super) fn apply_set_company_track_laying_capacity( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + value: Option, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| { + format!("missing company_id {company_id} while applying track capacity effect") + })?; + company.available_track_laying_capacity = value; + mutated_company_ids.insert(company_id); + } + Ok(()) +} + +pub(super) fn apply_adjust_company_cash( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + delta: i64, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + let prior_cash = state + .companies + .iter() + .find(|company| company.company_id == company_id) + .map(|company| company.current_cash) + .ok_or_else(|| format!("missing company_id {company_id} while applying cash effect"))?; + let next_cash = prior_cash + .checked_add(delta) + .ok_or_else(|| format!("company_id {company_id} cash adjustment overflow"))?; + if !service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + delta as f64, + false, + ) { + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| { + format!("missing company_id {company_id} while applying cash effect") + })?; + company.current_cash = next_cash; + } + mutated_company_ids.insert(company_id); + } + Ok(()) +} + +pub(super) fn apply_adjust_company_debt( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + delta: i64, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| format!("missing company_id {company_id} while applying debt effect"))?; + company.debt = apply_u64_delta(company.debt, delta, company_id)?; + mutated_company_ids.insert(company_id); + } + Ok(()) +} + +pub(super) fn apply_set_company_variable( + state: &mut RuntimeState, + target: &RuntimeCompanyTarget, + index: u32, + value: i64, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + state + .company_runtime_variables + .entry(company_id) + .or_default() + .insert(index, value); + mutated_company_ids.insert(company_id); + } + Ok(()) +} diff --git a/crates/rrt-runtime/src/engine/effects/entrypoints.rs b/crates/rrt-runtime/src/engine/effects/entrypoints.rs new file mode 100644 index 0000000..af71068 --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/entrypoints.rs @@ -0,0 +1,253 @@ +use std::collections::BTreeSet; + +use crate::engine::conditions::ResolvedConditionContext; +use crate::engine::effects::{AppliedEffectsSummary, EventGraphMutation}; +use crate::event::effects::RuntimeEffect; +use crate::state::RuntimeState; + +use super::{catalogs, company, event_graph, players, territories, trains, world}; + +pub(in crate::engine) fn apply_runtime_effects( + state: &mut RuntimeState, + effects: &[RuntimeEffect], + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, + mutated_player_ids: &mut BTreeSet, + staged_event_graph_mutations: &mut Vec, +) -> Result { + let mut summary = AppliedEffectsSummary::default(); + + for effect in effects { + match effect { + RuntimeEffect::SetWorldFlag { key, value } => { + world::apply_set_world_flag(state, key, *value); + } + RuntimeEffect::SetWorldScalarOverride { key, value } => { + world::apply_set_world_scalar_override(state, key, *value); + } + RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { + world::apply_set_limited_track_building_amount(state, *value); + } + RuntimeEffect::SetEconomicStatusCode { value } => { + world::apply_set_economic_status_code(state, *value); + } + RuntimeEffect::SetWorldVariable { index, value } => { + world::apply_set_world_variable(state, *index, *value); + } + RuntimeEffect::SetSpecialCondition { label, value } => { + world::apply_set_special_condition(state, label, *value); + } + RuntimeEffect::SetCompanyCash { target, value } => { + company::apply_set_company_cash( + state, + target, + *value, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::SetCompanyGovernanceScalar { + target, + metric, + value, + } => { + company::apply_set_company_governance_scalar( + state, + target, + *metric, + *value, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::SetCompanyTerritoryAccess { + target, + territory, + value, + } => { + company::apply_set_company_territory_access( + state, + target, + territory, + *value, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::ConfiscateCompanyAssets { target } => { + company::apply_confiscate_company_assets( + state, + target, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::DeactivateCompany { target } => { + company::apply_deactivate_company( + state, + target, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { + company::apply_set_company_track_laying_capacity( + state, + target, + *value, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::AdjustCompanyCash { target, delta } => { + company::apply_adjust_company_cash( + state, + target, + *delta, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::AdjustCompanyDebt { target, delta } => { + company::apply_adjust_company_debt( + state, + target, + *delta, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::SetCompanyVariable { + target, + index, + value, + } => { + company::apply_set_company_variable( + state, + target, + *index, + *value, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::SetPlayerCash { target, value } => { + players::apply_set_player_cash( + state, + target, + *value, + condition_context, + mutated_player_ids, + )?; + } + RuntimeEffect::SetChairmanCash { target, value } => { + players::apply_set_chairman_cash(state, target, *value, condition_context)?; + } + RuntimeEffect::DeactivatePlayer { target } => { + players::apply_deactivate_player( + state, + target, + condition_context, + mutated_player_ids, + )?; + } + RuntimeEffect::DeactivateChairman { target } => { + players::apply_deactivate_chairman( + state, + target, + condition_context, + mutated_company_ids, + )?; + } + RuntimeEffect::SetPlayerVariable { + target, + index, + value, + } => { + players::apply_set_player_variable( + state, + target, + *index, + *value, + condition_context, + )?; + } + RuntimeEffect::SetTerritoryVariable { + target, + index, + value, + } => { + territories::apply_set_territory_variable(state, target, *index, *value)?; + } + RuntimeEffect::SetTerritoryAccessCost { value } => { + territories::apply_set_territory_access_cost(state, *value); + } + RuntimeEffect::SetCandidateAvailability { name, value } => { + catalogs::apply_set_candidate_availability(state, name, *value); + } + RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => { + catalogs::apply_set_named_locomotive_availability(state, name, *value); + } + RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => { + catalogs::apply_set_named_locomotive_availability_value(state, name, *value); + } + RuntimeEffect::SetNamedLocomotiveCost { name, value } => { + catalogs::apply_set_named_locomotive_cost(state, name, *value); + } + RuntimeEffect::SetCargoPriceOverride { target, value } => { + catalogs::apply_set_cargo_price_override(state, target, *value); + } + RuntimeEffect::SetCargoProductionOverride { target, value } => { + catalogs::apply_set_cargo_production_override(state, target, *value); + } + RuntimeEffect::SetCargoProductionSlot { slot, value } => { + catalogs::apply_set_cargo_production_slot(state, *slot, *value); + } + RuntimeEffect::RetireTrains { + company_target, + territory_target, + locomotive_name, + } => { + trains::apply_retire_trains( + state, + company_target.as_ref(), + territory_target.as_ref(), + locomotive_name.as_deref(), + condition_context, + )?; + } + RuntimeEffect::AppendEventRecord { record } => { + event_graph::apply_append_event_record( + &mut summary, + staged_event_graph_mutations, + record, + ); + } + RuntimeEffect::ActivateEventRecord { record_id } => { + event_graph::apply_activate_event_record( + &mut summary, + staged_event_graph_mutations, + *record_id, + ); + } + RuntimeEffect::DeactivateEventRecord { record_id } => { + event_graph::apply_deactivate_event_record( + &mut summary, + staged_event_graph_mutations, + *record_id, + ); + } + RuntimeEffect::RemoveEventRecord { record_id } => { + event_graph::apply_remove_event_record( + &mut summary, + staged_event_graph_mutations, + *record_id, + ); + } + } + + summary.applied_effect_count += 1; + } + + Ok(summary) +} diff --git a/crates/rrt-runtime/src/engine/effects/event_graph.rs b/crates/rrt-runtime/src/engine/effects/event_graph.rs new file mode 100644 index 0000000..11c3173 --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/event_graph.rs @@ -0,0 +1,38 @@ +use crate::engine::effects::{AppliedEffectsSummary, EventGraphMutation}; +use crate::event::records::RuntimeEventRecordTemplate; + +pub(super) fn apply_append_event_record( + summary: &mut AppliedEffectsSummary, + staged_event_graph_mutations: &mut Vec, + record: &std::boxed::Box, +) { + staged_event_graph_mutations.push(EventGraphMutation::Append((**record).clone())); + summary.appended_record_ids.push(record.record_id); +} + +pub(super) fn apply_activate_event_record( + summary: &mut AppliedEffectsSummary, + staged_event_graph_mutations: &mut Vec, + record_id: u32, +) { + staged_event_graph_mutations.push(EventGraphMutation::Activate { record_id }); + summary.activated_record_ids.push(record_id); +} + +pub(super) fn apply_deactivate_event_record( + summary: &mut AppliedEffectsSummary, + staged_event_graph_mutations: &mut Vec, + record_id: u32, +) { + staged_event_graph_mutations.push(EventGraphMutation::Deactivate { record_id }); + summary.deactivated_record_ids.push(record_id); +} + +pub(super) fn apply_remove_event_record( + summary: &mut AppliedEffectsSummary, + staged_event_graph_mutations: &mut Vec, + record_id: u32, +) { + staged_event_graph_mutations.push(EventGraphMutation::Remove { record_id }); + summary.removed_record_ids.push(record_id); +} diff --git a/crates/rrt-runtime/src/engine/effects/mod.rs b/crates/rrt-runtime/src/engine/effects/mod.rs new file mode 100644 index 0000000..3f3bdc0 --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/mod.rs @@ -0,0 +1,12 @@ +mod catalogs; +mod company; +mod entrypoints; +mod event_graph; +mod model; +mod players; +mod territories; +mod trains; +mod world; + +pub(super) use self::entrypoints::apply_runtime_effects; +pub(super) use self::model::{AppliedEffectsSummary, EventGraphMutation}; diff --git a/crates/rrt-runtime/src/engine/effects/model.rs b/crates/rrt-runtime/src/engine/effects/model.rs new file mode 100644 index 0000000..726d5ce --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/model.rs @@ -0,0 +1,18 @@ +use crate::event::records::RuntimeEventRecordTemplate; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(in crate::engine) enum EventGraphMutation { + Append(RuntimeEventRecordTemplate), + Activate { record_id: u32 }, + Deactivate { record_id: u32 }, + Remove { record_id: u32 }, +} + +#[derive(Debug, Default)] +pub(in crate::engine) struct AppliedEffectsSummary { + pub(in crate::engine) applied_effect_count: u32, + pub(in crate::engine) appended_record_ids: Vec, + pub(in crate::engine) activated_record_ids: Vec, + pub(in crate::engine) deactivated_record_ids: Vec, + pub(in crate::engine) removed_record_ids: Vec, +} diff --git a/crates/rrt-runtime/src/engine/effects/players.rs b/crates/rrt-runtime/src/engine/effects/players.rs new file mode 100644 index 0000000..af30f27 --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/players.rs @@ -0,0 +1,141 @@ +use std::collections::BTreeSet; + +use crate::engine::conditions::ResolvedConditionContext; +use crate::engine::targets::{resolve_chairman_target_ids, resolve_player_target_ids}; +use crate::event::targets::{RuntimeChairmanTarget, RuntimePlayerTarget}; +use crate::state::RuntimeState; + +pub(super) fn apply_set_player_cash( + state: &mut RuntimeState, + target: &RuntimePlayerTarget, + value: i64, + condition_context: &ResolvedConditionContext, + mutated_player_ids: &mut BTreeSet, +) -> Result<(), String> { + let player_ids = resolve_player_target_ids(state, target, condition_context)?; + for player_id in player_ids { + let player = state + .players + .iter_mut() + .find(|player| player.player_id == player_id) + .ok_or_else(|| format!("missing player_id {player_id} while applying cash effect"))?; + player.current_cash = value; + mutated_player_ids.insert(player_id); + } + Ok(()) +} + +pub(super) fn apply_set_chairman_cash( + state: &mut RuntimeState, + target: &RuntimeChairmanTarget, + value: i64, + condition_context: &ResolvedConditionContext, +) -> Result<(), String> { + let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?; + for profile_id in profile_ids { + let chairman = state + .chairman_profiles + .iter_mut() + .find(|profile| profile.profile_id == profile_id) + .ok_or_else(|| { + format!("missing chairman profile_id {profile_id} while applying cash effect") + })?; + let preserved_threshold_adjusted_holdings_component = chairman + .purchasing_power_total + .saturating_sub(chairman.current_cash) + .max(0); + chairman.current_cash = value; + chairman.purchasing_power_total = chairman + .current_cash + .saturating_add(preserved_threshold_adjusted_holdings_component); + } + Ok(()) +} + +pub(super) fn apply_deactivate_player( + state: &mut RuntimeState, + target: &RuntimePlayerTarget, + condition_context: &ResolvedConditionContext, + mutated_player_ids: &mut BTreeSet, +) -> Result<(), String> { + let player_ids = resolve_player_target_ids(state, target, condition_context)?; + for player_id in player_ids { + let player = state + .players + .iter_mut() + .find(|player| player.player_id == player_id) + .ok_or_else(|| { + format!("missing player_id {player_id} while applying deactivate effect") + })?; + player.active = false; + mutated_player_ids.insert(player_id); + if state.selected_player_id == Some(player_id) { + state.selected_player_id = None; + } + } + Ok(()) +} + +pub(super) fn apply_deactivate_chairman( + state: &mut RuntimeState, + target: &RuntimeChairmanTarget, + condition_context: &ResolvedConditionContext, + mutated_company_ids: &mut BTreeSet, +) -> Result<(), String> { + let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?; + for profile_id in profile_ids.iter().copied() { + let linked_company_id = state + .chairman_profiles + .iter() + .find(|profile| profile.profile_id == profile_id) + .and_then(|profile| profile.linked_company_id); + let chairman = state + .chairman_profiles + .iter_mut() + .find(|profile| profile.profile_id == profile_id) + .ok_or_else(|| { + format!("missing chairman profile_id {profile_id} while applying deactivate effect") + })?; + chairman.active = false; + chairman.linked_company_id = None; + if state.selected_chairman_profile_id == Some(profile_id) { + state.selected_chairman_profile_id = None; + } + if let Some(linked_company_id) = linked_company_id { + if let Some(company) = state + .companies + .iter_mut() + .find(|company| company.company_id == linked_company_id) + { + company.linked_chairman_profile_id = None; + mutated_company_ids.insert(linked_company_id); + } + for other in &mut state.chairman_profiles { + if other.profile_id != profile_id + && other.linked_company_id == Some(linked_company_id) + { + other.linked_company_id = None; + } + } + } + } + Ok(()) +} + +pub(super) fn apply_set_player_variable( + state: &mut RuntimeState, + target: &RuntimePlayerTarget, + index: u32, + value: i64, + condition_context: &ResolvedConditionContext, +) -> Result<(), String> { + let player_ids = resolve_player_target_ids(state, target, condition_context)?; + for player_id in player_ids { + state + .player_runtime_variables + .entry(player_id) + .or_default() + .insert(index, value); + } + Ok(()) +} diff --git a/crates/rrt-runtime/src/engine/effects/territories.rs b/crates/rrt-runtime/src/engine/effects/territories.rs new file mode 100644 index 0000000..22c17ce --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/territories.rs @@ -0,0 +1,24 @@ +use crate::engine::targets::resolve_territory_target_ids; +use crate::event::targets::RuntimeTerritoryTarget; +use crate::state::RuntimeState; + +pub(super) fn apply_set_territory_variable( + state: &mut RuntimeState, + target: &RuntimeTerritoryTarget, + index: u32, + value: i64, +) -> Result<(), String> { + let territory_ids = resolve_territory_target_ids(state, target)?; + for territory_id in territory_ids { + state + .territory_runtime_variables + .entry(territory_id) + .or_default() + .insert(index, value); + } + Ok(()) +} + +pub(super) fn apply_set_territory_access_cost(state: &mut RuntimeState, value: u32) { + state.world_restore.territory_access_cost = Some(value); +} diff --git a/crates/rrt-runtime/src/engine/effects/trains.rs b/crates/rrt-runtime/src/engine/effects/trains.rs new file mode 100644 index 0000000..d157674 --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/trains.rs @@ -0,0 +1,27 @@ +use crate::engine::conditions::ResolvedConditionContext; +use crate::engine::metrics::retire_matching_trains; +use crate::engine::targets::{resolve_company_target_ids, resolve_territory_target_ids}; +use crate::event::targets::{RuntimeCompanyTarget, RuntimeTerritoryTarget}; +use crate::state::RuntimeState; + +pub(super) fn apply_retire_trains( + state: &mut RuntimeState, + company_target: Option<&RuntimeCompanyTarget>, + territory_target: Option<&RuntimeTerritoryTarget>, + locomotive_name: Option<&str>, + condition_context: &ResolvedConditionContext, +) -> Result<(), String> { + let company_ids = company_target + .map(|target| resolve_company_target_ids(state, target, condition_context)) + .transpose()?; + let territory_ids = territory_target + .map(|target| resolve_territory_target_ids(state, target)) + .transpose()?; + retire_matching_trains( + &mut state.trains, + company_ids.as_ref(), + territory_ids.as_ref(), + locomotive_name, + ); + Ok(()) +} diff --git a/crates/rrt-runtime/src/engine/effects/world.rs b/crates/rrt-runtime/src/engine/effects/world.rs new file mode 100644 index 0000000..4b21dc4 --- /dev/null +++ b/crates/rrt-runtime/src/engine/effects/world.rs @@ -0,0 +1,45 @@ +use crate::state::RuntimeState; + +pub(super) fn apply_set_world_flag(state: &mut RuntimeState, key: &str, value: bool) { + state.world_flags.insert(key.to_string(), value); + let raw = u8::from(value); + match key { + "world.all_steam_locos_available" => { + state.world_restore.all_steam_locomotives_available_raw_u8 = Some(raw); + state.world_restore.all_steam_locomotives_available_enabled = Some(value); + } + "world.all_diesel_locos_available" => { + state.world_restore.all_diesel_locomotives_available_raw_u8 = Some(raw); + state.world_restore.all_diesel_locomotives_available_enabled = Some(value); + } + "world.all_electric_locos_available" => { + state + .world_restore + .all_electric_locomotives_available_raw_u8 = Some(raw); + state + .world_restore + .all_electric_locomotives_available_enabled = Some(value); + } + _ => {} + } +} + +pub(super) fn apply_set_world_scalar_override(state: &mut RuntimeState, key: &str, value: i64) { + state.world_scalar_overrides.insert(key.to_string(), value); +} + +pub(super) fn apply_set_limited_track_building_amount(state: &mut RuntimeState, value: i32) { + state.world_restore.limited_track_building_amount = Some(value); +} + +pub(super) fn apply_set_economic_status_code(state: &mut RuntimeState, value: i32) { + state.world_restore.economic_status_code = Some(value); +} + +pub(super) fn apply_set_world_variable(state: &mut RuntimeState, index: u32, value: i64) { + state.world_runtime_variables.insert(index, value); +} + +pub(super) fn apply_set_special_condition(state: &mut RuntimeState, label: &str, value: u32) { + state.special_conditions.insert(label.to_string(), value); +} diff --git a/crates/rrt-runtime/src/engine/metrics.rs b/crates/rrt-runtime/src/engine/metrics.rs new file mode 100644 index 0000000..afe3f0d --- /dev/null +++ b/crates/rrt-runtime/src/engine/metrics.rs @@ -0,0 +1,219 @@ +use crate::derived::{ + runtime_company_book_value_per_share, runtime_company_credit_rating, + runtime_company_investor_confidence, runtime_company_management_attitude, + runtime_company_prime_rate, +}; +use crate::event::conditions::RuntimeConditionComparator; +use crate::event::metrics::{ + RuntimeChairmanMetric, RuntimeCompanyMetric, RuntimeTerritoryMetric, RuntimeTrackMetric, +}; +use crate::event::targets::RuntimeCargoClass; +use crate::state::{ + RuntimeChairmanProfile, RuntimeCompany, RuntimeCompanyTerritoryAccess, RuntimeState, + RuntimeTrackPieceCounts, RuntimeTrain, +}; + +pub(super) fn company_metric_value( + state: &RuntimeState, + company: &RuntimeCompany, + metric: RuntimeCompanyMetric, +) -> i64 { + match metric { + RuntimeCompanyMetric::CurrentCash => company.current_cash, + RuntimeCompanyMetric::TotalDebt => company.debt as i64, + RuntimeCompanyMetric::CreditRating => { + runtime_company_credit_rating(state, company.company_id).unwrap_or(0) + } + RuntimeCompanyMetric::PrimeRate => { + runtime_company_prime_rate(state, company.company_id).unwrap_or(0) + } + RuntimeCompanyMetric::BookValuePerShare => { + runtime_company_book_value_per_share(state, company.company_id).unwrap_or(0) + } + RuntimeCompanyMetric::InvestorConfidence => { + runtime_company_investor_confidence(state, company.company_id).unwrap_or(0) + } + RuntimeCompanyMetric::ManagementAttitude => { + runtime_company_management_attitude(state, company.company_id).unwrap_or(0) + } + RuntimeCompanyMetric::TrackPiecesTotal => i64::from(company.track_piece_counts.total), + RuntimeCompanyMetric::TrackPiecesSingle => i64::from(company.track_piece_counts.single), + RuntimeCompanyMetric::TrackPiecesDouble => i64::from(company.track_piece_counts.double), + RuntimeCompanyMetric::TrackPiecesTransition => { + i64::from(company.track_piece_counts.transition) + } + RuntimeCompanyMetric::TrackPiecesElectric => i64::from(company.track_piece_counts.electric), + RuntimeCompanyMetric::TrackPiecesNonElectric => { + i64::from(company.track_piece_counts.non_electric) + } + } +} + +pub(super) fn chairman_metric_value( + profile: &RuntimeChairmanProfile, + metric: RuntimeChairmanMetric, +) -> i64 { + match metric { + RuntimeChairmanMetric::CurrentCash => profile.current_cash, + RuntimeChairmanMetric::HoldingsValueTotal => profile.holdings_value_total, + RuntimeChairmanMetric::NetWorthTotal => profile.net_worth_total, + RuntimeChairmanMetric::PurchasingPowerTotal => profile.purchasing_power_total, + } +} + +pub(super) fn territory_metric_value( + state: &RuntimeState, + territory_ids: &[u32], + metric: RuntimeTerritoryMetric, +) -> i64 { + state + .territories + .iter() + .filter(|territory| territory_ids.contains(&territory.territory_id)) + .map(|territory| { + track_piece_metric_value( + territory.track_piece_counts, + territory_metric_to_track_metric(metric), + ) + }) + .sum() +} + +pub(super) fn company_territory_metric_value( + state: &RuntimeState, + company_id: u32, + territory_ids: &[u32], + metric: RuntimeTrackMetric, +) -> i64 { + state + .company_territory_track_piece_counts + .iter() + .filter(|entry| { + entry.company_id == company_id && territory_ids.contains(&entry.territory_id) + }) + .map(|entry| track_piece_metric_value(entry.track_piece_counts, metric)) + .sum() +} + +pub(super) fn track_piece_metric_value( + counts: RuntimeTrackPieceCounts, + metric: RuntimeTrackMetric, +) -> i64 { + match metric { + RuntimeTrackMetric::Total => i64::from(counts.total), + RuntimeTrackMetric::Single => i64::from(counts.single), + RuntimeTrackMetric::Double => i64::from(counts.double), + RuntimeTrackMetric::Transition => i64::from(counts.transition), + RuntimeTrackMetric::Electric => i64::from(counts.electric), + RuntimeTrackMetric::NonElectric => i64::from(counts.non_electric), + } +} + +pub(super) fn territory_metric_to_track_metric( + metric: RuntimeTerritoryMetric, +) -> RuntimeTrackMetric { + match metric { + RuntimeTerritoryMetric::TrackPiecesTotal => RuntimeTrackMetric::Total, + RuntimeTerritoryMetric::TrackPiecesSingle => RuntimeTrackMetric::Single, + RuntimeTerritoryMetric::TrackPiecesDouble => RuntimeTrackMetric::Double, + RuntimeTerritoryMetric::TrackPiecesTransition => RuntimeTrackMetric::Transition, + RuntimeTerritoryMetric::TrackPiecesElectric => RuntimeTrackMetric::Electric, + RuntimeTerritoryMetric::TrackPiecesNonElectric => RuntimeTrackMetric::NonElectric, + } +} + +pub(super) fn compare_condition_value( + actual: i64, + comparator: RuntimeConditionComparator, + expected: i64, +) -> bool { + match comparator { + RuntimeConditionComparator::Ge => actual >= expected, + RuntimeConditionComparator::Le => actual <= expected, + RuntimeConditionComparator::Gt => actual > expected, + RuntimeConditionComparator::Lt => actual < expected, + RuntimeConditionComparator::Eq => actual == expected, + RuntimeConditionComparator::Ne => actual != expected, + } +} + +pub(super) fn cargo_production_total_for_class( + state: &RuntimeState, + cargo_class: RuntimeCargoClass, +) -> i64 { + state + .cargo_catalog + .iter() + .filter(|entry| entry.cargo_class == cargo_class) + .filter_map(|entry| state.cargo_production_overrides.get(&entry.slot_id)) + .copied() + .map(i64::from) + .sum() +} + +pub(super) fn apply_u64_delta(current: u64, delta: i64, company_id: u32) -> Result { + if delta >= 0 { + current + .checked_add(delta as u64) + .ok_or_else(|| format!("company_id {company_id} debt adjustment overflow")) + } else { + current + .checked_sub(delta.unsigned_abs()) + .ok_or_else(|| format!("company_id {company_id} debt adjustment underflow")) + } +} + +pub(super) fn retire_matching_trains( + trains: &mut [RuntimeTrain], + company_ids: Option<&Vec>, + territory_ids: Option<&Vec>, + locomotive_name: Option<&str>, +) { + for train in trains.iter_mut() { + if !train.active || train.retired { + continue; + } + if company_ids.is_some_and(|company_ids| !company_ids.contains(&train.owner_company_id)) { + continue; + } + if territory_ids.is_some_and(|territory_ids| { + !train + .territory_id + .is_some_and(|territory_id| territory_ids.contains(&territory_id)) + }) { + continue; + } + if locomotive_name.is_some_and(|name| train.locomotive_name.as_deref() != Some(name)) { + continue; + } + train.active = false; + train.retired = true; + } +} + +pub(super) fn set_company_territory_access_pairs( + access_entries: &mut Vec, + company_ids: &[u32], + territory_ids: &[u32], + value: bool, +) { + if value { + for company_id in company_ids { + for territory_id in territory_ids { + if !access_entries.iter().any(|entry| { + entry.company_id == *company_id && entry.territory_id == *territory_id + }) { + access_entries.push(RuntimeCompanyTerritoryAccess { + company_id: *company_id, + territory_id: *territory_id, + }); + } + } + } + } else { + access_entries.retain(|entry| { + !(company_ids.contains(&entry.company_id) + && territory_ids.contains(&entry.territory_id)) + }); + } +} diff --git a/crates/rrt-runtime/src/engine/mod.rs b/crates/rrt-runtime/src/engine/mod.rs new file mode 100644 index 0000000..33b32ca --- /dev/null +++ b/crates/rrt-runtime/src/engine/mod.rs @@ -0,0 +1,15 @@ +mod advance; +mod command; +mod conditions; +mod effects; +mod metrics; +mod mutations; +mod service; +mod targets; + +pub use self::command::{ + BoundaryEvent, ServiceEvent, StepCommand, StepResult, execute_step_command, +}; + +#[cfg(test)] +mod tests; diff --git a/crates/rrt-runtime/src/engine/mutations.rs b/crates/rrt-runtime/src/engine/mutations.rs new file mode 100644 index 0000000..613618a --- /dev/null +++ b/crates/rrt-runtime/src/engine/mutations.rs @@ -0,0 +1,57 @@ +use crate::engine::effects::EventGraphMutation; +use crate::state::RuntimeState; + +pub(super) fn commit_staged_event_graph_mutations( + state: &mut RuntimeState, + staged_event_graph_mutations: &[EventGraphMutation], +) -> Result<(), String> { + for mutation in staged_event_graph_mutations { + match mutation { + EventGraphMutation::Append(record) => { + if state + .event_runtime_records + .iter() + .any(|existing| existing.record_id == record.record_id) + { + return Err(format!( + "cannot append duplicate event record_id {}", + record.record_id + )); + } + state + .event_runtime_records + .push(record.clone().into_runtime_record()); + } + EventGraphMutation::Activate { record_id } => { + let record = state + .event_runtime_records + .iter_mut() + .find(|record| record.record_id == *record_id) + .ok_or_else(|| { + format!("cannot activate missing event record_id {record_id}") + })?; + record.active = true; + } + EventGraphMutation::Deactivate { record_id } => { + let record = state + .event_runtime_records + .iter_mut() + .find(|record| record.record_id == *record_id) + .ok_or_else(|| { + format!("cannot deactivate missing event record_id {record_id}") + })?; + record.active = false; + } + EventGraphMutation::Remove { record_id } => { + let index = state + .event_runtime_records + .iter() + .position(|record| record.record_id == *record_id) + .ok_or_else(|| format!("cannot remove missing event record_id {record_id}"))?; + state.event_runtime_records.remove(index); + } + } + } + + state.validate() +} diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/bankruptcy.rs b/crates/rrt-runtime/src/engine/service/annual_finance/bankruptcy.rs new file mode 100644 index 0000000..8e9a8cd --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/bankruptcy.rs @@ -0,0 +1,6 @@ +use crate::engine::service::service_apply_company_bankruptcy; +use crate::state::RuntimeState; + +pub(super) fn apply_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool { + service_apply_company_bankruptcy(state, company_id) +} diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/bond_issue.rs b/crates/rrt-runtime/src/engine/service/annual_finance/bond_issue.rs new file mode 100644 index 0000000..40366d4 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/bond_issue.rs @@ -0,0 +1,132 @@ +use crate::derived::runtime_company_bond_interest_rate_quote; +use crate::engine::service::annual_finance::constants::COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE; +use crate::engine::service::{ + service_post_company_stat_delta, service_repay_matured_company_live_bonds_and_compact, +}; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::{RuntimeCompanyBondSlot, RuntimeState}; + +pub(super) fn apply_bond_issue(state: &mut RuntimeState, company_id: u32) -> bool { + let Some(bond_state) = + crate::derived::runtime_company_annual_bond_policy_state(state, company_id) + else { + return false; + }; + if !bond_state.eligible_for_bond_issue_branch { + return false; + } + + let mut mutated = false; + let retired_principal_total = + service_repay_matured_company_live_bonds_and_compact(state, company_id).unwrap_or(0); + mutated |= retired_principal_total > 0; + state.service_state.annual_bond_last_retired_principal_total = state + .service_state + .annual_bond_last_retired_principal_total + .saturating_add(retired_principal_total); + + let issue_bond_count = bond_state.proposed_issue_bond_count.unwrap_or(0); + let Some(principal) = bond_state.issue_principal_step else { + return mutated; + }; + let Some(years_to_maturity) = bond_state.proposed_issue_years_to_maturity else { + return mutated; + }; + let Some(maturity_year) = state + .world_restore + .packed_year_word_raw_u16 + .map(u32::from) + .and_then(|year| year.checked_add(years_to_maturity)) + else { + return mutated; + }; + + for _ in 0..issue_bond_count { + let Some(quote_rate) = runtime_company_bond_interest_rate_quote( + state, + company_id, + principal, + years_to_maturity, + ) else { + break; + }; + + mutated |= service_post_company_stat_delta( + state, + company_id, + 0x0c, + (principal as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, + true, + ); + mutated |= + service_post_company_stat_delta(state, company_id, 0x12, -(principal as f64), false); + mutated |= service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + principal as f64, + false, + ); + + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + break; + }; + let slot_index = market_state.bond_count as u32; + if market_state.bond_count == u8::MAX { + break; + } + market_state.live_bond_slots.push(RuntimeCompanyBondSlot { + slot_index, + principal, + maturity_year, + coupon_rate_raw_u32: (quote_rate as f32).to_bits(), + }); + market_state.bond_count = market_state.bond_count.saturating_add(1); + market_state.largest_live_bond_principal = Some( + market_state + .largest_live_bond_principal + .unwrap_or(0) + .max(principal), + ); + market_state.highest_coupon_live_bond_principal = market_state + .live_bond_slots + .iter() + .filter_map(|slot| { + let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; + coupon.is_finite().then_some((coupon, slot.principal)) + }) + .max_by(|left, right| { + left.0 + .partial_cmp(&right.0) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|(_, principal)| principal); + state.service_state.annual_bond_last_issued_principal_total = state + .service_state + .annual_bond_last_issued_principal_total + .saturating_add(u64::from(principal)); + } + if let Some(company) = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + { + company.debt = state + .service_state + .company_market_state + .get(&company_id) + .map(|market_state| { + market_state + .live_bond_slots + .iter() + .map(|slot| u64::from(slot.principal)) + .sum::() + }) + .unwrap_or(company.debt); + } + mutated +} diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/constants.rs b/crates/rrt-runtime/src/engine/service/annual_finance/constants.rs new file mode 100644 index 0000000..5a622cb --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/constants.rs @@ -0,0 +1,3 @@ +pub(super) const COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT: u32 = 0x33f; +pub(super) const COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE: f64 = -0.02; +pub(super) const COMPANY_REPURCHASE_PRESSURE_SCALE: f64 = 0.7; diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/dividend.rs b/crates/rrt-runtime/src/engine/service/annual_finance/dividend.rs new file mode 100644 index 0000000..453fc71 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/dividend.rs @@ -0,0 +1,30 @@ +use crate::derived::runtime_company_annual_dividend_policy_state; +use crate::engine::service::annual_finance::constants::COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT; +use crate::state::RuntimeState; + +pub(super) fn apply_dividend_adjustment(state: &mut RuntimeState, company_id: u32) -> bool { + let Some(dividend_state) = runtime_company_annual_dividend_policy_state(state, company_id) + else { + return false; + }; + let Some(proposed_tenths) = dividend_state.proposed_dividend_per_share_tenths else { + return false; + }; + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + let raw_bits = ((proposed_tenths as f32) / 10.0).to_bits(); + let prior_bits = market_state + .direct_control_transfer_float_fields_raw_u32 + .insert(COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT, raw_bits); + if prior_bits != Some(raw_bits) { + state.service_state.annual_dividend_adjustment_commit_count += 1; + true + } else { + false + } +} diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/entrypoints.rs b/crates/rrt-runtime/src/engine/service/annual_finance/entrypoints.rs new file mode 100644 index 0000000..306fe6c --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/entrypoints.rs @@ -0,0 +1,124 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use crate::derived::runtime_company_annual_finance_policy_state; +use crate::engine::command::ServiceEvent; +use crate::state::{RuntimeCompanyAnnualFinancePolicyAction, RuntimeState}; + +use super::{bankruptcy, bond_issue, dividend, news, stock_issue, stock_repurchase}; + +pub(crate) fn service_company_annual_finance_policy( + state: &mut RuntimeState, + service_events: &mut Vec, +) -> Result<(), String> { + let active_company_ids = state + .companies + .iter() + .filter(|company| company.active) + .map(|company| company.company_id) + .collect::>(); + let active_company_id_set = active_company_ids.iter().copied().collect::>(); + state + .service_state + .annual_finance_last_actions + .retain(|company_id, _| active_company_id_set.contains(company_id)); + state + .service_state + .annual_finance_last_news_family_candidates + .retain(|company_id, _| active_company_id_set.contains(company_id)); + state.service_state.annual_finance_last_news_events.clear(); + state.service_state.annual_finance_service_calls += 1; + state.service_state.annual_bond_last_retired_principal_total = 0; + state.service_state.annual_bond_last_issued_principal_total = 0; + state.service_state.annual_stock_repurchase_last_share_count = 0; + state.service_state.annual_stock_issue_last_share_count = 0; + + let mut mutated_company_ids = BTreeSet::new(); + let mut applied_effect_count = 0u32; + let mut finance_news_family_candidates = BTreeMap::new(); + let mut annual_finance_news_events = Vec::new(); + + for company_id in active_company_ids { + let Some(policy_state) = runtime_company_annual_finance_policy_state(state, company_id) + else { + continue; + }; + let action = policy_state.action; + state + .service_state + .annual_finance_last_actions + .insert(company_id, action); + state + .service_state + .annual_finance_last_news_family_candidates + .remove(&company_id); + *state + .service_state + .annual_finance_action_counts + .entry(action) + .or_insert(0) += 1; + + let mutated = match action { + RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment => { + dividend::apply_dividend_adjustment(state, company_id) + } + RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy + | RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => { + bankruptcy::apply_bankruptcy(state, company_id) + } + RuntimeCompanyAnnualFinancePolicyAction::StockIssue => { + stock_issue::apply_stock_issue(state, company_id) + } + RuntimeCompanyAnnualFinancePolicyAction::BondIssue => { + bond_issue::apply_bond_issue(state, company_id) + } + RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => { + stock_repurchase::apply_stock_repurchase(state, company_id) + } + _ => false, + }; + + if !mutated { + continue; + } + + if matches!( + action, + RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy + | RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback + | RuntimeCompanyAnnualFinancePolicyAction::StockIssue + | RuntimeCompanyAnnualFinancePolicyAction::BondIssue + | RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase + ) { + news::record_annual_finance_news( + state, + company_id, + action, + &mut finance_news_family_candidates, + &mut annual_finance_news_events, + ); + } + + applied_effect_count += 1; + mutated_company_ids.insert(company_id); + } + + service_events.push(ServiceEvent { + kind: "annual_finance_policy".to_string(), + trigger_kind: None, + serviced_record_ids: Vec::new(), + applied_effect_count, + mutated_company_ids: mutated_company_ids.into_iter().collect(), + mutated_player_ids: Vec::new(), + appended_record_ids: Vec::new(), + activated_record_ids: Vec::new(), + deactivated_record_ids: Vec::new(), + removed_record_ids: Vec::new(), + finance_news_family_candidates, + annual_finance_news_events, + near_city_acquisition_news_family_candidates: BTreeMap::new(), + near_city_acquisition_news_events: Vec::new(), + dirty_rerun: false, + }); + + Ok(()) +} diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/mod.rs b/crates/rrt-runtime/src/engine/service/annual_finance/mod.rs new file mode 100644 index 0000000..a2bee77 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/mod.rs @@ -0,0 +1,10 @@ +mod bankruptcy; +mod bond_issue; +mod constants; +mod dividend; +mod entrypoints; +mod news; +mod stock_issue; +mod stock_repurchase; + +pub(crate) use self::entrypoints::service_company_annual_finance_policy; diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/news.rs b/crates/rrt-runtime/src/engine/service/annual_finance/news.rs new file mode 100644 index 0000000..158c67c --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/news.rs @@ -0,0 +1,44 @@ +use std::collections::BTreeMap; + +use crate::derived::runtime_annual_finance_news_family_candidate_label; +use crate::event::news::RuntimeAnnualFinanceNewsEvent; +use crate::state::{RuntimeCompanyAnnualFinancePolicyAction, RuntimeState}; + +pub(super) fn record_annual_finance_news( + state: &mut RuntimeState, + company_id: u32, + action: RuntimeCompanyAnnualFinancePolicyAction, + finance_news_family_candidates: &mut BTreeMap, + annual_finance_news_events: &mut Vec, +) { + let Some(label) = runtime_annual_finance_news_family_candidate_label( + action, + state.service_state.annual_bond_last_retired_principal_total, + state.service_state.annual_bond_last_issued_principal_total, + state.service_state.annual_stock_repurchase_last_share_count, + state.service_state.annual_stock_issue_last_share_count, + ) else { + return; + }; + + state + .service_state + .annual_finance_last_news_family_candidates + .insert(company_id, label.to_string()); + finance_news_family_candidates.insert(company_id, label.to_string()); + let news_event = RuntimeAnnualFinanceNewsEvent { + company_id, + selector_label: label.to_string(), + action_label: crate::derived::runtime_company_annual_finance_policy_action_label(action) + .to_string(), + retired_principal_total: state.service_state.annual_bond_last_retired_principal_total, + issued_principal_total: state.service_state.annual_bond_last_issued_principal_total, + repurchased_share_count: state.service_state.annual_stock_repurchase_last_share_count, + issued_share_count: state.service_state.annual_stock_issue_last_share_count, + }; + state + .service_state + .annual_finance_last_news_events + .push(news_event.clone()); + annual_finance_news_events.push(news_event); +} diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/stock_issue.rs b/crates/rrt-runtime/src/engine/service/annual_finance/stock_issue.rs new file mode 100644 index 0000000..96735d4 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/stock_issue.rs @@ -0,0 +1,71 @@ +use crate::derived::runtime_company_annual_stock_issue_state; +use crate::engine::service::annual_finance::constants::COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE; +use crate::engine::service::service_post_company_stat_delta; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::RuntimeState; + +pub(super) fn apply_stock_issue(state: &mut RuntimeState, company_id: u32) -> bool { + let Some(issue_state) = runtime_company_annual_stock_issue_state(state, company_id) else { + return false; + }; + let Some(batch_size) = issue_state.trimmed_issue_batch_size else { + return false; + }; + let Some(proceeds_per_tranche) = issue_state.pressured_proceeds else { + return false; + }; + let Some(current_tuple_word_0) = state.world_restore.current_calendar_tuple_word_raw_u32 else { + return false; + }; + let Some(current_tuple_word_1) = state.world_restore.current_calendar_tuple_word_2_raw_u32 + else { + return false; + }; + let Some(total_share_delta) = batch_size.checked_mul(2) else { + return false; + }; + let Some(next_outstanding_shares) = state + .service_state + .company_market_state + .get(&company_id) + .map(|market_state| market_state.outstanding_shares) + .and_then(|value| value.checked_add(total_share_delta)) + else { + return false; + }; + + let mut mutated = false; + for _ in 0..2 { + mutated |= service_post_company_stat_delta( + state, + company_id, + 0x0c, + (batch_size as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, + true, + ); + mutated |= service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + proceeds_per_tranche as f64, + false, + ); + state.service_state.annual_stock_issue_last_share_count = state + .service_state + .annual_stock_issue_last_share_count + .saturating_add(u64::from(batch_size)); + } + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + market_state.outstanding_shares = next_outstanding_shares; + market_state.prior_issue_calendar_word = market_state.current_issue_calendar_word; + market_state.prior_issue_calendar_word_2 = market_state.current_issue_calendar_word_2; + market_state.current_issue_calendar_word = current_tuple_word_0; + market_state.current_issue_calendar_word_2 = current_tuple_word_1; + mutated +} diff --git a/crates/rrt-runtime/src/engine/service/annual_finance/stock_repurchase.rs b/crates/rrt-runtime/src/engine/service/annual_finance/stock_repurchase.rs new file mode 100644 index 0000000..efe4b69 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/annual_finance/stock_repurchase.rs @@ -0,0 +1,86 @@ +use crate::derived::{ + runtime_company_annual_stock_repurchase_state, + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64, + runtime_round_f64_to_i64, +}; +use crate::engine::service::annual_finance::constants::{ + COMPANY_REPURCHASE_PRESSURE_SCALE, COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, +}; +use crate::engine::service::service_post_company_stat_delta; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::RuntimeState; + +pub(super) fn apply_stock_repurchase(state: &mut RuntimeState, company_id: u32) -> bool { + let mut mutated = false; + for _ in 0..128 { + let Some(repurchase_state) = + runtime_company_annual_stock_repurchase_state(state, company_id) + else { + break; + }; + if !repurchase_state.eligible_for_single_batch_repurchase { + break; + } + let Some(batch_size) = repurchase_state.repurchase_batch_size else { + break; + }; + let Some(pressure_shares) = + runtime_round_f64_to_i64(batch_size as f64 * COMPANY_REPURCHASE_PRESSURE_SCALE) + else { + break; + }; + let Some(share_price_scalar) = + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( + state, + company_id, + pressure_shares, + ) + else { + break; + }; + let Some(repurchase_total) = + runtime_round_f64_to_i64(share_price_scalar * batch_size as f64) + else { + break; + }; + if repurchase_total <= 0 { + break; + } + let Some(next_outstanding_shares) = state + .service_state + .company_market_state + .get(&company_id) + .map(|market_state| market_state.outstanding_shares) + .and_then(|value| value.checked_sub(batch_size)) + else { + break; + }; + mutated |= service_post_company_stat_delta( + state, + company_id, + 0x0c, + (repurchase_total as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, + true, + ); + mutated |= service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -(repurchase_total as f64), + false, + ); + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + break; + }; + market_state.outstanding_shares = next_outstanding_shares; + state.service_state.annual_stock_repurchase_last_share_count = state + .service_state + .annual_stock_repurchase_last_share_count + .saturating_add(u64::from(batch_size)); + } + mutated +} diff --git a/crates/rrt-runtime/src/engine/service/bonds.rs b/crates/rrt-runtime/src/engine/service/bonds.rs new file mode 100644 index 0000000..c9fc576 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/bonds.rs @@ -0,0 +1,105 @@ +use crate::engine::service::service_post_company_stat_delta; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::RuntimeState; + +pub(crate) fn service_repay_matured_company_live_bonds_and_compact( + state: &mut RuntimeState, + company_id: u32, +) -> Option { + let Some(current_year_word) = state + .world_restore + .packed_year_word_raw_u16 + .map(u32::from) + .or_else(|| Some(state.calendar.year)) + else { + return None; + }; + let retired_principal_total = state + .service_state + .company_market_state + .get(&company_id) + .map(|market_state| { + market_state + .live_bond_slots + .iter() + .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) + .map(|slot| u64::from(slot.principal)) + .sum::() + }) + .unwrap_or(0); + if retired_principal_total == 0 { + return None; + } + + let retired_principal_total_f64 = retired_principal_total as f64; + let mut company_mutated = false; + company_mutated |= service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -retired_principal_total_f64, + false, + ); + company_mutated |= service_post_company_stat_delta( + state, + company_id, + 0x12, + retired_principal_total_f64, + false, + ); + + if let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + { + market_state + .live_bond_slots + .retain(|slot| slot.maturity_year == 0 || slot.maturity_year > current_year_word); + for (slot_index, slot) in market_state.live_bond_slots.iter_mut().enumerate() { + slot.slot_index = slot_index as u32; + } + market_state.bond_count = market_state.live_bond_slots.len().min(u8::MAX as usize) as u8; + market_state.largest_live_bond_principal = market_state + .live_bond_slots + .iter() + .map(|slot| slot.principal) + .max(); + market_state.highest_coupon_live_bond_principal = market_state + .live_bond_slots + .iter() + .filter_map(|slot| { + let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; + coupon.is_finite().then_some((coupon, slot.principal)) + }) + .max_by(|left, right| { + left.0 + .partial_cmp(&right.0) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|(_, principal)| principal); + company_mutated = true; + } + + if let Some(company) = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + { + company.debt = state + .service_state + .company_market_state + .get(&company_id) + .map(|market_state| { + market_state + .live_bond_slots + .iter() + .map(|slot| u64::from(slot.principal)) + .sum::() + }) + .unwrap_or(0); + company_mutated = true; + } + + company_mutated.then_some(retired_principal_total) +} diff --git a/crates/rrt-runtime/src/engine/service/company_fields.rs b/crates/rrt-runtime/src/engine/service/company_fields.rs new file mode 100644 index 0000000..b19409e --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/company_fields.rs @@ -0,0 +1,242 @@ +use crate::derived::runtime_round_f64_to_i64; +use crate::event::metrics::{ + RUNTIME_COMPANY_STAT_SLOT_COUNT, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN, +}; +use crate::state::{RuntimeCompanyMarketState, RuntimeState}; + +pub(crate) fn service_decode_saved_f64_bits(raw_bits: u64) -> Option { + let value = f64::from_bits(raw_bits); + value.is_finite().then_some(value) +} + +pub(crate) fn service_ensure_company_stat_post_capacity( + market_state: &mut RuntimeCompanyMarketState, + slot_id: u32, +) -> Option { + let index = slot_id + .checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? + .try_into() + .ok()?; + let required_year_len = + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + if market_state.year_stat_family_qword_bits.len() < required_year_len { + market_state + .year_stat_family_qword_bits + .resize(required_year_len, 0.0f64.to_bits()); + } + let required_special_len = RUNTIME_COMPANY_STAT_SLOT_COUNT as usize; + if market_state.special_stat_family_232a_qword_bits.len() < required_special_len { + market_state + .special_stat_family_232a_qword_bits + .resize(required_special_len, 0.0f64.to_bits()); + } + Some(index) +} + +pub(crate) fn service_post_company_stat_delta( + state: &mut RuntimeState, + company_id: u32, + slot_id: u32, + delta: f64, + mirror_cash_totals: bool, +) -> bool { + if !delta.is_finite() { + return false; + } + + let Some(refreshed_current_cash) = ({ + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + let Some(index) = service_ensure_company_stat_post_capacity(market_state, slot_id) else { + return false; + }; + let prior_year_value = market_state + .year_stat_family_qword_bits + .get(index) + .copied() + .and_then(service_decode_saved_f64_bits) + .unwrap_or(0.0); + market_state.year_stat_family_qword_bits[index] = (prior_year_value + delta).to_bits(); + + let special_index = slot_id as usize; + let prior_special_value = market_state + .special_stat_family_232a_qword_bits + .get(special_index) + .copied() + .and_then(service_decode_saved_f64_bits) + .unwrap_or(0.0); + market_state.special_stat_family_232a_qword_bits[special_index] = + (prior_special_value + delta).to_bits(); + + if mirror_cash_totals { + let cash_index = RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize; + let prior_cash_shadow_value = market_state + .special_stat_family_232a_qword_bits + .get(cash_index) + .copied() + .and_then(service_decode_saved_f64_bits) + .unwrap_or(0.0); + market_state.special_stat_family_232a_qword_bits[cash_index] = + (prior_cash_shadow_value + delta).to_bits(); + } + + market_state + .year_stat_family_qword_bits + .get( + (RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize, + ) + .copied() + .and_then(service_decode_saved_f64_bits) + .and_then(runtime_round_f64_to_i64) + }) else { + return false; + }; + + if let Some(company) = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + { + company.current_cash = refreshed_current_cash; + true + } else { + false + } +} + +pub(crate) fn service_set_company_direct_float_field( + state: &mut RuntimeState, + company_id: u32, + field_offset: u32, + value: f64, +) -> bool { + if !value.is_finite() { + return false; + } + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + market_state + .direct_control_transfer_float_fields_raw_u32 + .insert(field_offset, (value as f32).to_bits()); + true +} + +pub(crate) fn service_set_company_cached_share_price( + state: &mut RuntimeState, + company_id: u32, + value: f64, +) -> bool { + if !value.is_finite() { + return false; + } + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + market_state.cached_share_price_raw_u32 = (value as f32).to_bits(); + true +} + +pub(crate) fn service_set_company_issue_opinion_total( + state: &mut RuntimeState, + company_id: u32, + issue_id: u32, + target_total: i64, +) -> bool { + let current_total = crate::derived::runtime_world_issue_opinion_term_sum_raw( + state, + issue_id, + state + .companies + .iter() + .find(|company| company.company_id == company_id) + .and_then(|company| company.linked_chairman_profile_id), + Some(company_id), + None, + ) + .unwrap_or(0); + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + let issue_index = issue_id as usize; + if market_state.issue_opinion_terms_raw_i32.len() <= issue_index { + market_state + .issue_opinion_terms_raw_i32 + .resize(issue_index + 1, 0); + } + let prior_company_term = i64::from(market_state.issue_opinion_terms_raw_i32[issue_index]); + let next_company_term = prior_company_term.saturating_add(target_total - current_total); + let Ok(next_company_term_i32) = i32::try_from(next_company_term) else { + return false; + }; + market_state.issue_opinion_terms_raw_i32[issue_index] = next_company_term_i32; + true +} + +pub(crate) fn service_set_company_prime_rate_target( + state: &mut RuntimeState, + company_id: u32, + value: i64, +) -> bool { + let Some(baseline) = crate::derived::runtime_world_prime_rate_baseline(state) else { + return false; + }; + let target_raw_sum = ((value as f64 - baseline) * 100.0).round(); + if !target_raw_sum.is_finite() { + return false; + } + service_set_company_issue_opinion_total( + state, + company_id, + crate::event::metrics::RUNTIME_WORLD_ISSUE_PRIME_RATE, + target_raw_sum as i64, + ) +} + +pub(crate) fn service_set_company_credit_rating_target( + state: &mut RuntimeState, + company_id: u32, + value: i64, +) -> bool { + let Some(current_rating) = crate::derived::runtime_company_credit_rating(state, company_id) + else { + return false; + }; + let current_issue_total = crate::derived::runtime_world_issue_opinion_term_sum_raw( + state, + crate::event::metrics::RUNTIME_WORLD_ISSUE_CREDIT_MARKET, + state + .companies + .iter() + .find(|company| company.company_id == company_id) + .and_then(|company| company.linked_chairman_profile_id), + Some(company_id), + None, + ) + .unwrap_or(0); + service_set_company_issue_opinion_total( + state, + company_id, + crate::event::metrics::RUNTIME_WORLD_ISSUE_CREDIT_MARKET, + current_issue_total.saturating_add(value.saturating_sub(current_rating)), + ) +} diff --git a/crates/rrt-runtime/src/engine/service/company_lifecycle.rs b/crates/rrt-runtime/src/engine/service/company_lifecycle.rs new file mode 100644 index 0000000..088637e --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/company_lifecycle.rs @@ -0,0 +1,113 @@ +use crate::engine::service::service_post_company_stat_delta; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::RuntimeState; + +pub(crate) fn service_zero_company_current_cash(state: &mut RuntimeState, company_id: u32) -> bool { + let Some(current_cash) = crate::derived::runtime_company_stat_value( + state, + company_id, + crate::event::metrics::RuntimeCompanyStatSelector { + family_id: crate::event::metrics::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ) else { + return false; + }; + if current_cash == 0 { + return true; + } + service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -(current_cash as f64), + false, + ) +} + +pub(crate) fn service_clear_company_live_bonds(state: &mut RuntimeState, company_id: u32) -> bool { + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + market_state.live_bond_slots.clear(); + market_state.bond_count = 0; + market_state.largest_live_bond_principal = None; + market_state.highest_coupon_live_bond_principal = None; + true +} + +pub(crate) fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool { + let Some(bankruptcy_year) = state + .world_restore + .packed_year_word_raw_u16 + .map(u32::from) + .or_else(|| Some(state.calendar.year)) + else { + return false; + }; + + let mut company_mutated = false; + if let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + { + market_state.last_bankruptcy_year = bankruptcy_year; + for slot in &mut market_state.live_bond_slots { + slot.principal /= 2; + } + market_state + .live_bond_slots + .retain(|slot| slot.principal > 0); + market_state.bond_count = market_state.live_bond_slots.len().min(u8::MAX as usize) as u8; + market_state.largest_live_bond_principal = market_state + .live_bond_slots + .iter() + .map(|slot| slot.principal) + .max(); + market_state.highest_coupon_live_bond_principal = market_state + .live_bond_slots + .iter() + .filter_map(|slot| { + let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; + coupon.is_finite().then_some((coupon, slot.principal)) + }) + .max_by(|left, right| { + left.0 + .partial_cmp(&right.0) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|(_, principal)| principal); + company_mutated = true; + } + + let remaining_debt = state + .service_state + .company_market_state + .get(&company_id) + .map(|market_state| { + market_state + .live_bond_slots + .iter() + .map(|slot| u64::from(slot.principal)) + .sum::() + }); + if let Some(company) = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + { + if let Some(remaining_debt) = remaining_debt { + company.debt = remaining_debt.min(u64::from(u32::MAX)) as u64; + } + company_mutated = true; + } + + company_mutated |= service_zero_company_current_cash(state, company_id); + + company_mutated +} diff --git a/crates/rrt-runtime/src/engine/service/mod.rs b/crates/rrt-runtime/src/engine/service/mod.rs new file mode 100644 index 0000000..007b5a3 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/mod.rs @@ -0,0 +1,22 @@ +mod annual_finance; +mod bonds; +mod company_fields; +mod company_lifecycle; +mod near_city_acquisition; +mod periodic; +mod trigger_dispatch; + +pub(crate) use self::annual_finance::service_company_annual_finance_policy; +pub(crate) use self::bonds::service_repay_matured_company_live_bonds_and_compact; +pub(crate) use self::company_fields::{ + service_post_company_stat_delta, service_set_company_cached_share_price, + service_set_company_credit_rating_target, service_set_company_direct_float_field, + service_set_company_issue_opinion_total, service_set_company_prime_rate_target, +}; +pub(crate) use self::company_lifecycle::{ + service_apply_company_bankruptcy, service_clear_company_live_bonds, + service_zero_company_current_cash, +}; +pub(crate) use self::near_city_acquisition::service_company_near_city_acquisition; +pub(crate) use self::periodic::service_periodic_boundary; +pub(crate) use self::trigger_dispatch::service_trigger_kind; diff --git a/crates/rrt-runtime/src/engine/service/near_city_acquisition.rs b/crates/rrt-runtime/src/engine/service/near_city_acquisition.rs new file mode 100644 index 0000000..69d9593 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/near_city_acquisition.rs @@ -0,0 +1,170 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use crate::engine::command::ServiceEvent; +use crate::event::news::RuntimeNearCityAcquisitionNewsEvent; +use crate::state::{ + RuntimeNearCityAcquisitionSite, RuntimeNearCityAcquisitionValueProvenance, RuntimeState, +}; + +pub(crate) fn service_company_near_city_acquisition( + state: &mut RuntimeState, + service_events: &mut Vec, +) -> Result<(), String> { + state.service_state.near_city_acquisition_service_calls += 1; + state + .service_state + .near_city_acquisition_last_news_family_candidates + .clear(); + state + .service_state + .near_city_acquisition_last_news_events + .clear(); + + let active_company_ids = state + .companies + .iter() + .filter(|company| company.active) + .map(|company| company.company_id) + .collect::>(); + let mut acquired_company_ids = BTreeSet::new(); + let mut mutated_company_ids = BTreeSet::new(); + let mut news_family_candidates = BTreeMap::new(); + let mut news_events = Vec::new(); + let mut applied_effect_count = 0_u32; + + for site_index in 0..state.service_state.near_city_acquisition_sites.len() { + let site_snapshot = state.service_state.near_city_acquisition_sites[site_index].clone(); + if site_snapshot.owner_company_id.is_some() + || !is_acquirable_subtype(&site_snapshot.candidate_subtype_label) + { + continue; + } + + let Some(company_id) = select_near_city_acquisition_company( + state, + &site_snapshot, + &active_company_ids, + &acquired_company_ids, + ) else { + continue; + }; + let site = &mut state.service_state.near_city_acquisition_sites[site_index]; + + site.owner_company_id = Some(company_id); + site.owner_company_id_provenance = + RuntimeNearCityAcquisitionValueProvenance::BestEffortGuess; + acquired_company_ids.insert(company_id); + mutated_company_ids.insert(company_id); + applied_effect_count += 1; + + let selector_label = near_city_selector_label(site); + state + .service_state + .near_city_acquisition_last_news_family_candidates + .insert(site.site_id, selector_label.clone()); + news_family_candidates.insert(site.site_id, selector_label.clone()); + + let news_event = RuntimeNearCityAcquisitionNewsEvent { + company_id, + site_id: site.site_id, + site_primary_name: site.primary_name.clone(), + site_secondary_name: site.secondary_name.clone(), + selector_label, + outcome_label: "acquired_near_city_industry".to_string(), + subtype_label: site.candidate_subtype_label.clone(), + owner_company_id_provenance: site.owner_company_id_provenance, + cached_tri_lane_provenance: site.cached_tri_lane_provenance, + }; + state + .service_state + .near_city_acquisition_last_news_events + .push(news_event.clone()); + news_events.push(news_event); + } + + service_events.push(ServiceEvent { + kind: "near_city_acquisition".to_string(), + trigger_kind: None, + serviced_record_ids: Vec::new(), + applied_effect_count, + mutated_company_ids: mutated_company_ids.into_iter().collect(), + mutated_player_ids: Vec::new(), + appended_record_ids: Vec::new(), + activated_record_ids: Vec::new(), + deactivated_record_ids: Vec::new(), + removed_record_ids: Vec::new(), + finance_news_family_candidates: BTreeMap::new(), + annual_finance_news_events: Vec::new(), + near_city_acquisition_news_family_candidates: news_family_candidates, + near_city_acquisition_news_events: news_events, + dirty_rerun: false, + }); + + Ok(()) +} + +fn select_near_city_acquisition_company( + state: &RuntimeState, + site: &RuntimeNearCityAcquisitionSite, + active_company_ids: &BTreeSet, + acquired_company_ids: &BTreeSet, +) -> Option { + let mut candidates = state + .companies + .iter() + .filter(|company| active_company_ids.contains(&company.company_id)) + .filter(|company| !acquired_company_ids.contains(&company.company_id)) + .map(|company| { + let market_state = state + .service_state + .company_market_state + .get(&company.company_id); + let side_latch_state = state + .service_state + .company_periodic_side_latch_state + .get(&company.company_id); + let mut score = 0_i32; + if state.selected_company_id == Some(company.company_id) { + score += 3; + } + if site.preferred_company_id == Some(company.company_id) { + score += 4; + } + if side_latch_state.is_some_and(|latch| latch.linked_transit_latch) { + score += 4; + } + if side_latch_state.is_some_and(|latch| latch.city_connection_latch) { + score += 2; + } + if market_state + .is_some_and(|market| market.linked_transit_route_anchor_entry_id.is_some()) + { + score += 1; + } + (score, company.company_id) + }) + .collect::>(); + candidates.sort_by(|left, right| right.cmp(left)); + + candidates + .into_iter() + .find(|(score, _)| *score >= 4) + .map(|(_, company_id)| company_id) +} + +fn is_acquirable_subtype(subtype_label: &str) -> bool { + matches!( + subtype_label, + "farm" | "mine" | "industry" | "industry_like" + ) +} + +fn near_city_selector_label(site: &RuntimeNearCityAcquisitionSite) -> String { + match site.candidate_subtype_label.as_str() { + "farm" => "near_city_farm_acquisition", + "mine" => "near_city_mine_acquisition", + "industry" | "industry_like" => "near_city_industry_acquisition", + _ => "near_city_site_acquisition", + } + .to_string() +} diff --git a/crates/rrt-runtime/src/engine/service/periodic.rs b/crates/rrt-runtime/src/engine/service/periodic.rs new file mode 100644 index 0000000..2e8a96f --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/periodic.rs @@ -0,0 +1,72 @@ +use std::collections::BTreeSet; + +use crate::engine::command::ServiceEvent; +use crate::engine::service::{ + service_company_annual_finance_policy, service_company_near_city_acquisition, + service_trigger_kind, +}; +use crate::state::{RuntimeCompanyPeriodicSideLatchState, RuntimeState}; + +const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 2, 3, 4, 5, 6]; + +pub(crate) fn service_periodic_boundary( + state: &mut RuntimeState, + service_events: &mut Vec, +) -> Result<(), String> { + state.service_state.periodic_boundary_calls += 1; + service_refresh_company_periodic_side_latch_state(state); + + for trigger_kind in PERIODIC_TRIGGER_KIND_ORDER { + service_trigger_kind(state, trigger_kind, service_events)?; + } + service_company_near_city_acquisition(state, service_events)?; + service_company_annual_finance_policy(state, service_events)?; + + Ok(()) +} + +pub(crate) fn service_refresh_company_periodic_side_latch_state(state: &mut RuntimeState) { + let active_company_ids = state + .companies + .iter() + .filter(|company| company.active) + .map(|company| company.company_id) + .collect::>(); + state + .service_state + .company_periodic_side_latch_state + .retain(|company_id, _| active_company_ids.contains(company_id)); + + for company_id in active_company_ids { + match ( + state + .service_state + .company_periodic_side_latch_state + .get_mut(&company_id), + state.service_state.company_market_state.get(&company_id), + ) { + (Some(latch_state), Some(market_state)) => { + latch_state.preferred_locomotive_engine_type_raw_u8 = None; + latch_state.city_connection_latch = market_state.city_connection_latch; + latch_state.linked_transit_latch = market_state.linked_transit_latch; + } + (Some(latch_state), None) => { + latch_state.preferred_locomotive_engine_type_raw_u8 = None; + } + (None, Some(market_state)) => { + state + .service_state + .company_periodic_side_latch_state + .insert( + company_id, + RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: None, + city_connection_latch: market_state.city_connection_latch, + linked_transit_latch: market_state.linked_transit_latch, + }, + ); + } + (None, None) => {} + } + } +} diff --git a/crates/rrt-runtime/src/engine/service/trigger_dispatch.rs b/crates/rrt-runtime/src/engine/service/trigger_dispatch.rs new file mode 100644 index 0000000..83b5788 --- /dev/null +++ b/crates/rrt-runtime/src/engine/service/trigger_dispatch.rs @@ -0,0 +1,120 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use crate::engine::command::ServiceEvent; +use crate::engine::conditions::evaluate_record_conditions; +use crate::engine::effects::apply_runtime_effects; +use crate::engine::mutations::commit_staged_event_graph_mutations; +use crate::state::RuntimeState; + +pub(crate) fn service_trigger_kind( + state: &mut RuntimeState, + trigger_kind: u8, + service_events: &mut Vec, +) -> Result<(), String> { + let eligible_indices = state + .event_runtime_records + .iter() + .enumerate() + .filter(|(_, record)| { + record.active + && record.trigger_kind == trigger_kind + && !(record.one_shot && record.has_fired) + }) + .map(|(index, _)| index) + .collect::>(); + + let mut serviced_record_ids = Vec::new(); + let mut applied_effect_count = 0_u32; + let mut mutated_company_ids = BTreeSet::new(); + let mut mutated_player_ids = BTreeSet::new(); + let mut appended_record_ids = Vec::new(); + let mut activated_record_ids = Vec::new(); + let mut deactivated_record_ids = Vec::new(); + let mut removed_record_ids = Vec::new(); + let mut staged_event_graph_mutations = Vec::new(); + let mut dirty_rerun = false; + + *state + .service_state + .trigger_dispatch_counts + .entry(trigger_kind) + .or_insert(0) += 1; + + for index in eligible_indices { + let ( + record_id, + record_conditions, + record_effects, + record_marks_collection_dirty, + record_one_shot, + ) = { + let record = &state.event_runtime_records[index]; + ( + record.record_id, + record.conditions.clone(), + record.effects.clone(), + record.marks_collection_dirty, + record.one_shot, + ) + }; + + let Some(condition_context) = evaluate_record_conditions(state, &record_conditions)? else { + continue; + }; + + let effect_summary = apply_runtime_effects( + state, + &record_effects, + &condition_context, + &mut mutated_company_ids, + &mut mutated_player_ids, + &mut staged_event_graph_mutations, + )?; + applied_effect_count += effect_summary.applied_effect_count; + appended_record_ids.extend(effect_summary.appended_record_ids); + activated_record_ids.extend(effect_summary.activated_record_ids); + deactivated_record_ids.extend(effect_summary.deactivated_record_ids); + removed_record_ids.extend(effect_summary.removed_record_ids); + + { + let record = &mut state.event_runtime_records[index]; + record.service_count += 1; + if record_one_shot { + record.has_fired = true; + } + } + + serviced_record_ids.push(record_id); + state.service_state.total_event_record_services += 1; + if trigger_kind != 0x0a && record_marks_collection_dirty { + dirty_rerun = true; + } + } + + commit_staged_event_graph_mutations(state, &staged_event_graph_mutations)?; + + service_events.push(ServiceEvent { + kind: "trigger_dispatch".to_string(), + trigger_kind: Some(trigger_kind), + serviced_record_ids, + applied_effect_count, + mutated_company_ids: mutated_company_ids.into_iter().collect(), + mutated_player_ids: mutated_player_ids.into_iter().collect(), + appended_record_ids, + activated_record_ids, + deactivated_record_ids, + removed_record_ids, + finance_news_family_candidates: BTreeMap::new(), + annual_finance_news_events: Vec::new(), + near_city_acquisition_news_family_candidates: BTreeMap::new(), + near_city_acquisition_news_events: Vec::new(), + dirty_rerun, + }); + + if dirty_rerun { + state.service_state.dirty_rerun_count += 1; + service_trigger_kind(state, 0x0a, service_events)?; + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/engine/targets/chairmen.rs b/crates/rrt-runtime/src/engine/targets/chairmen.rs new file mode 100644 index 0000000..9503be5 --- /dev/null +++ b/crates/rrt-runtime/src/engine/targets/chairmen.rs @@ -0,0 +1,102 @@ +use std::collections::BTreeSet; + +use crate::engine::conditions::ResolvedConditionContext; +use crate::event::targets::RuntimeChairmanTarget; +use crate::state::{RuntimeChairmanProfile, RuntimeCompanyControllerKind, RuntimeState}; + +use super::shared; + +pub(in crate::engine) fn resolve_chairman_target_ids( + state: &RuntimeState, + target: &RuntimeChairmanTarget, + condition_context: &ResolvedConditionContext, +) -> Result, String> { + match target { + RuntimeChairmanTarget::AllActive => Ok(state + .chairman_profiles + .iter() + .filter(|profile| profile.active) + .map(|profile| profile.profile_id) + .collect()), + RuntimeChairmanTarget::HumanChairmen => Ok(state + .chairman_profiles + .iter() + .filter(|profile| { + chairman_profile_matches_company_controller_kind( + state, + profile, + RuntimeCompanyControllerKind::Human, + ) + }) + .map(|profile| profile.profile_id) + .collect()), + RuntimeChairmanTarget::AiChairmen => Ok(state + .chairman_profiles + .iter() + .filter(|profile| { + chairman_profile_matches_company_controller_kind( + state, + profile, + RuntimeCompanyControllerKind::Ai, + ) + }) + .map(|profile| profile.profile_id) + .collect()), + RuntimeChairmanTarget::Ids { ids } => { + let known_ids = state + .chairman_profiles + .iter() + .map(|profile| profile.profile_id) + .collect::>(); + shared::validate_known_ids(ids, &known_ids, |profile_id| { + format!("target references unknown chairman profile_id {profile_id}") + })?; + Ok(ids.clone()) + } + RuntimeChairmanTarget::SelectedChairman => { + let selected_profile_id = state.selected_chairman_profile_id.ok_or_else(|| { + "target requires selected_chairman_profile_id context".to_string() + })?; + if state + .chairman_profiles + .iter() + .any(|profile| profile.profile_id == selected_profile_id && profile.active) + { + Ok(vec![selected_profile_id]) + } else { + Err( + "target requires selected_chairman_profile_id to reference an active chairman profile" + .to_string(), + ) + } + } + RuntimeChairmanTarget::ConditionTrueChairman => { + if condition_context.matching_chairman_profile_ids.is_empty() { + Err("target requires chairman condition-evaluation context".to_string()) + } else { + Ok(condition_context + .matching_chairman_profile_ids + .iter() + .copied() + .collect()) + } + } + } +} + +pub(in crate::engine) fn chairman_profile_matches_company_controller_kind( + state: &RuntimeState, + profile: &RuntimeChairmanProfile, + controller_kind: RuntimeCompanyControllerKind, +) -> bool { + profile.active + && profile + .linked_company_id + .and_then(|company_id| { + state + .companies + .iter() + .find(|company| company.company_id == company_id) + }) + .is_some_and(|company| company.controller_kind == controller_kind) +} diff --git a/crates/rrt-runtime/src/engine/targets/company.rs b/crates/rrt-runtime/src/engine/targets/company.rs new file mode 100644 index 0000000..507ab7c --- /dev/null +++ b/crates/rrt-runtime/src/engine/targets/company.rs @@ -0,0 +1,89 @@ +use std::collections::BTreeSet; + +use crate::engine::conditions::ResolvedConditionContext; +use crate::event::targets::RuntimeCompanyTarget; +use crate::state::{RuntimeCompanyControllerKind, RuntimeState}; + +use super::shared; + +pub(in crate::engine) fn resolve_company_target_ids( + state: &RuntimeState, + target: &RuntimeCompanyTarget, + condition_context: &ResolvedConditionContext, +) -> Result, String> { + match target { + RuntimeCompanyTarget::AllActive => Ok(state + .companies + .iter() + .filter(|company| company.active) + .map(|company| company.company_id) + .collect()), + RuntimeCompanyTarget::Ids { ids } => { + let known_ids = state + .companies + .iter() + .map(|company| company.company_id) + .collect::>(); + shared::validate_known_ids(ids, &known_ids, |company_id| { + format!("target references unknown company_id {company_id}") + })?; + Ok(ids.clone()) + } + RuntimeCompanyTarget::HumanCompanies => { + shared::ensure_no_unknown_company_controller_kinds( + &state.companies, + "target requires company role context but at least one company has unknown controller_kind", + )?; + Ok(state + .companies + .iter() + .filter(|company| { + company.active && company.controller_kind == RuntimeCompanyControllerKind::Human + }) + .map(|company| company.company_id) + .collect()) + } + RuntimeCompanyTarget::AiCompanies => { + shared::ensure_no_unknown_company_controller_kinds( + &state.companies, + "target requires company role context but at least one company has unknown controller_kind", + )?; + Ok(state + .companies + .iter() + .filter(|company| { + company.active && company.controller_kind == RuntimeCompanyControllerKind::Ai + }) + .map(|company| company.company_id) + .collect()) + } + RuntimeCompanyTarget::SelectedCompany => { + let selected_company_id = state + .selected_company_id + .ok_or_else(|| "target requires selected_company_id context".to_string())?; + if state + .companies + .iter() + .any(|company| company.company_id == selected_company_id && company.active) + { + Ok(vec![selected_company_id]) + } else { + Err( + "target requires selected_company_id to reference an active company" + .to_string(), + ) + } + } + RuntimeCompanyTarget::ConditionTrueCompany => { + if condition_context.matching_company_ids.is_empty() { + Err("target requires condition-evaluation context".to_string()) + } else { + Ok(condition_context + .matching_company_ids + .iter() + .copied() + .collect()) + } + } + } +} diff --git a/crates/rrt-runtime/src/engine/targets/mod.rs b/crates/rrt-runtime/src/engine/targets/mod.rs new file mode 100644 index 0000000..0d85cbb --- /dev/null +++ b/crates/rrt-runtime/src/engine/targets/mod.rs @@ -0,0 +1,10 @@ +mod chairmen; +mod company; +mod players; +mod shared; +mod territories; + +pub(super) use self::chairmen::resolve_chairman_target_ids; +pub(super) use self::company::resolve_company_target_ids; +pub(super) use self::players::resolve_player_target_ids; +pub(super) use self::territories::resolve_territory_target_ids; diff --git a/crates/rrt-runtime/src/engine/targets/players.rs b/crates/rrt-runtime/src/engine/targets/players.rs new file mode 100644 index 0000000..43ce3f6 --- /dev/null +++ b/crates/rrt-runtime/src/engine/targets/players.rs @@ -0,0 +1,86 @@ +use std::collections::BTreeSet; + +use crate::engine::conditions::ResolvedConditionContext; +use crate::event::targets::RuntimePlayerTarget; +use crate::state::{RuntimeCompanyControllerKind, RuntimeState}; + +use super::shared; + +pub(in crate::engine) fn resolve_player_target_ids( + state: &RuntimeState, + target: &RuntimePlayerTarget, + condition_context: &ResolvedConditionContext, +) -> Result, String> { + match target { + RuntimePlayerTarget::AllActive => Ok(state + .players + .iter() + .filter(|player| player.active) + .map(|player| player.player_id) + .collect()), + RuntimePlayerTarget::Ids { ids } => { + let known_ids = state + .players + .iter() + .map(|player| player.player_id) + .collect::>(); + shared::validate_known_ids(ids, &known_ids, |player_id| { + format!("target references unknown player_id {player_id}") + })?; + Ok(ids.clone()) + } + RuntimePlayerTarget::HumanPlayers => { + shared::ensure_no_unknown_player_controller_kinds( + &state.players, + "target requires player role context but at least one player has unknown controller_kind", + )?; + Ok(state + .players + .iter() + .filter(|player| { + player.active && player.controller_kind == RuntimeCompanyControllerKind::Human + }) + .map(|player| player.player_id) + .collect()) + } + RuntimePlayerTarget::AiPlayers => { + shared::ensure_no_unknown_player_controller_kinds( + &state.players, + "target requires player role context but at least one player has unknown controller_kind", + )?; + Ok(state + .players + .iter() + .filter(|player| { + player.active && player.controller_kind == RuntimeCompanyControllerKind::Ai + }) + .map(|player| player.player_id) + .collect()) + } + RuntimePlayerTarget::SelectedPlayer => { + let selected_player_id = state + .selected_player_id + .ok_or_else(|| "target requires selected_player_id context".to_string())?; + if state + .players + .iter() + .any(|player| player.player_id == selected_player_id && player.active) + { + Ok(vec![selected_player_id]) + } else { + Err("target requires selected_player_id to reference an active player".to_string()) + } + } + RuntimePlayerTarget::ConditionTruePlayer => { + if condition_context.matching_player_ids.is_empty() { + Err("target requires player condition-evaluation context".to_string()) + } else { + Ok(condition_context + .matching_player_ids + .iter() + .copied() + .collect()) + } + } + } +} diff --git a/crates/rrt-runtime/src/engine/targets/shared.rs b/crates/rrt-runtime/src/engine/targets/shared.rs new file mode 100644 index 0000000..e9c4afe --- /dev/null +++ b/crates/rrt-runtime/src/engine/targets/shared.rs @@ -0,0 +1,44 @@ +use std::collections::BTreeSet; + +use crate::state::RuntimeCompanyControllerKind; + +pub(super) fn validate_known_ids( + ids: &[u32], + known_ids: &BTreeSet, + missing_message: impl Fn(u32) -> String, +) -> Result<(), String> { + for id in ids { + if !known_ids.contains(id) { + return Err(missing_message(*id)); + } + } + Ok(()) +} + +pub(super) fn ensure_no_unknown_company_controller_kinds( + companies: &[crate::state::RuntimeCompany], + error: &str, +) -> Result<(), String> { + if companies + .iter() + .any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown) + { + Err(error.to_string()) + } else { + Ok(()) + } +} + +pub(super) fn ensure_no_unknown_player_controller_kinds( + players: &[crate::state::RuntimePlayer], + error: &str, +) -> Result<(), String> { + if players + .iter() + .any(|player| player.controller_kind == RuntimeCompanyControllerKind::Unknown) + { + Err(error.to_string()) + } else { + Ok(()) + } +} diff --git a/crates/rrt-runtime/src/engine/targets/territories.rs b/crates/rrt-runtime/src/engine/targets/territories.rs new file mode 100644 index 0000000..68a03c9 --- /dev/null +++ b/crates/rrt-runtime/src/engine/targets/territories.rs @@ -0,0 +1,30 @@ +use std::collections::BTreeSet; + +use crate::event::targets::RuntimeTerritoryTarget; +use crate::state::RuntimeState; + +use super::shared; + +pub(in crate::engine) fn resolve_territory_target_ids( + state: &RuntimeState, + target: &RuntimeTerritoryTarget, +) -> Result, String> { + match target { + RuntimeTerritoryTarget::AllTerritories => Ok(state + .territories + .iter() + .map(|territory| territory.territory_id) + .collect()), + RuntimeTerritoryTarget::Ids { ids } => { + let known_ids = state + .territories + .iter() + .map(|territory| territory.territory_id) + .collect::>(); + shared::validate_known_ids(ids, &known_ids, |territory_id| { + format!("territory target references unknown territory_id {territory_id}") + })?; + Ok(ids.clone()) + } + } +} diff --git a/crates/rrt-runtime/src/engine/tests/advance.rs b/crates/rrt-runtime/src/engine/tests/advance.rs new file mode 100644 index 0000000..b644784 --- /dev/null +++ b/crates/rrt-runtime/src/engine/tests/advance.rs @@ -0,0 +1,163 @@ +use super::*; + +#[test] +fn advances_to_target() { + let mut state = state(); + let result = execute_step_command( + &mut state, + &StepCommand::AdvanceTo { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 5, + }, + }, + ) + .expect("advance_to should succeed"); + + assert_eq!(result.steps_executed, 5); + assert_eq!(state.calendar.tick_slot, 5); + assert_eq!(state.world_restore.packed_year_word_raw_u16, Some(1830)); + assert_eq!(state.world_restore.partial_year_progress_raw_u8, Some(1)); + assert_eq!( + state.world_restore.current_calendar_tuple_word_raw_u32, + Some(0x0101_0726) + ); + assert_eq!( + state.world_restore.current_calendar_tuple_word_2_raw_u32, + Some(0x2801_0001) + ); + assert_eq!( + state.world_restore.absolute_counter_raw_u32, + Some(885_427_240) + ); + assert_eq!( + state.world_restore.absolute_counter_mirror_raw_u32, + Some(885_427_240) + ); + assert_eq!( + state.world_restore.selected_year_gap_scalar_raw_u32, + Some((1.0f32 / 3.0).to_bits()) + ); + assert_eq!( + state + .world_restore + .selected_year_gap_scalar_value_f32_text + .as_deref(), + Some("0.333333") + ); +} + +#[test] +fn year_rollover_step_runs_periodic_boundary_services() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: MONTH_SLOTS_PER_YEAR - 1, + phase_slot: PHASE_SLOTS_PER_MONTH - 1, + tick_slot: crate::calendar::TICKS_PER_PHASE - 1, + }, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 77, + trigger_kind: 1, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.periodic_rollover_service_fired".to_string(), + value: true, + }], + }], + ..state() + }; + + let result = execute_step_command(&mut state, &StepCommand::StepCount { steps: 1 }) + .expect("year rollover step should run periodic boundary services"); + + assert_eq!(result.steps_executed, 1); + assert_eq!( + result.boundary_events, + vec![BoundaryEvent { + kind: "year_rollover".to_string(), + calendar: CalendarPoint { + year: 1831, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + }] + ); + assert_eq!(state.service_state.periodic_boundary_calls, 1); + assert_eq!(state.service_state.total_event_record_services, 1); + assert_eq!( + state + .world_flags + .get("world.periodic_rollover_service_fired"), + Some(&true) + ); + assert_eq!(state.world_restore.packed_year_word_raw_u16, Some(1831)); + assert_eq!(state.world_restore.partial_year_progress_raw_u8, Some(1)); + assert_eq!( + state.world_restore.current_calendar_tuple_word_raw_u32, + Some(0x0101_0727) + ); + assert_eq!( + state.world_restore.current_calendar_tuple_word_2_raw_u32, + Some(0x0001_0001) + ); + assert_eq!( + state.world_restore.absolute_counter_raw_u32, + Some(885_911_040) + ); + assert_eq!( + state.world_restore.absolute_counter_mirror_raw_u32, + Some(885_911_040) + ); + assert_eq!( + state.world_restore.selected_year_gap_scalar_raw_u32, + Some((1.0f32 / 3.0).to_bits()) + ); + assert_eq!( + state + .world_restore + .selected_year_gap_scalar_value_f32_text + .as_deref(), + Some("0.333333") + ); + assert!( + result + .service_events + .iter() + .any(|event| event.trigger_kind == Some(1)) + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "annual_finance_policy") + ); +} + +#[test] +fn rejects_backward_target() { + let mut state = state(); + state.calendar.tick_slot = 3; + + let result = execute_step_command( + &mut state, + &StepCommand::AdvanceTo { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 2, + }, + }, + ); + + assert!(result.is_err()); +} diff --git a/crates/rrt-runtime/src/engine/tests/annual_finance.rs b/crates/rrt-runtime/src/engine/tests/annual_finance.rs new file mode 100644 index 0000000..de0ede4 --- /dev/null +++ b/crates/rrt-runtime/src/engine/tests/annual_finance.rs @@ -0,0 +1,1345 @@ +use super::*; + +#[test] +fn periodic_boundary_applies_dividend_adjustment_from_annual_finance_policy() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); + + let mut state = RuntimeState { + calendar: crate::CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + dividend_policy_raw_u8: Some(0), + dividend_adjustment_allowed: Some(true), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(1), + ..crate::state::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 21, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(7), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(21), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 7, + name: "Chairman Seven".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(21), + company_holdings: BTreeMap::from([(21, 9_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + company_market_state: BTreeMap::from([( + 21, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1840, + last_dividend_year: 1844, + year_stat_family_qword_bits, + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x33f, + 0.4f32.to_bits(), + )]), + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply annual finance policy"); + + assert_eq!(state.service_state.annual_finance_service_calls, 1); + assert_eq!( + state.service_state.annual_dividend_adjustment_commit_count, + 1 + ); + assert_eq!( + state.service_state.annual_finance_last_actions.get(&21), + Some(&crate::state::RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment) + ); + assert_eq!( + state.service_state.company_market_state[&21] + .direct_control_transfer_float_fields_raw_u32 + .get(&0x33f), + Some(&1.8f32.to_bits()) + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![21] + && event.finance_news_family_candidates.is_empty()) + ); +} + +#[test] +fn periodic_boundary_applies_stock_issue_from_annual_finance_policy() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 250_000.0f64.to_bits(); + + let current_issue_calendar_word = 0x0101_0725; + let current_issue_calendar_word_2 = 0x0001_0001; + let prior_issue_calendar_word = 0x0101_0701; + let prior_issue_calendar_word_2 = 0x0001_0001; + + let mut state = RuntimeState { + calendar: crate::CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + current_calendar_tuple_word_raw_u32: Some(0x0101_0726), + current_calendar_tuple_word_2_raw_u32: Some(0x0001_0001), + absolute_counter_raw_u32: Some(885_911_040), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + issue_37_value: Some(5.0f32.to_bits()), + issue_38_value: Some(2), + ..crate::state::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 22, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(22), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + company_market_state: BTreeMap::from([( + 22, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 2, + highest_coupon_live_bond_principal: Some(300_000), + founding_year: 1840, + cached_share_price_raw_u32: 35.0f32.to_bits(), + recent_per_share_cache_absolute_counter: 885_911_040, + recent_per_share_cached_value_bits: 34.0f64.to_bits(), + current_issue_calendar_word, + current_issue_calendar_word_2, + prior_issue_calendar_word, + prior_issue_calendar_word_2, + live_bond_slots: vec![ + crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 300_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.11f32.to_bits(), + }, + crate::state::RuntimeCompanyBondSlot { + slot_index: 1, + principal: 200_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.07f32.to_bits(), + }, + ], + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x32f, + 30.0f32.to_bits(), + )]), + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply annual stock issue policy"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&22), + Some(&crate::state::RuntimeCompanyAnnualFinancePolicyAction::StockIssue) + ); + assert_eq!( + state + .service_state + .annual_finance_last_news_family_candidates + .get(&22), + Some(&"4053".to_string()) + ); + assert_eq!( + state.service_state.annual_stock_issue_last_share_count, + 4_000 + ); + assert_eq!( + state.service_state.annual_stock_repurchase_last_share_count, + 0 + ); + assert_eq!(state.companies[0].current_cash, 390_000); + assert_eq!( + state.service_state.company_market_state[&22].outstanding_shares, + 24_000 + ); + assert_eq!( + state.service_state.company_market_state[&22].prior_issue_calendar_word, + current_issue_calendar_word + ); + assert_eq!( + state.service_state.company_market_state[&22].prior_issue_calendar_word_2, + current_issue_calendar_word_2 + ); + assert_eq!( + state.service_state.company_market_state[&22].current_issue_calendar_word, + 0x0101_0726 + ); + assert_eq!( + state.service_state.company_market_state[&22].current_issue_calendar_word_2, + 0x0001_0001 + ); + assert!(result.service_events.iter().any(|event| { + event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![22] + && event.finance_news_family_candidates.get(&22) == Some(&"4053".to_string()) + && event.annual_finance_news_events.iter().any(|news| { + news.company_id == 22 + && news.selector_label == "4053" + && news.action_label == "stock_issue" + && news.issued_share_count == 4_000 + }) + })); +} + +#[test] +fn periodic_boundary_applies_stock_repurchase_from_annual_finance_policy() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 1_600_000.0f64.to_bits(); + + let base_state = RuntimeState { + calendar: crate::CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + absolute_counter_raw_u32: Some(1_000), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(0), + ..crate::state::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 23, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(8), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(23), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 8, + name: "Chairman".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(23), + company_holdings: BTreeMap::from([(23, 9_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(8), + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + company_market_state: BTreeMap::from([( + 23, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1840, + city_connection_latch: true, + recent_per_share_cache_absolute_counter: 1_000, + recent_per_share_cached_value_bits: 50.0f64.to_bits(), + mutable_support_scalar_raw_u32: 0.0f32.to_bits(), + young_company_support_scalar_raw_u32: 0.0f32.to_bits(), + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let pressured_share_price = + crate::derived::runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( + &base_state, + 23, + 700, + ) + .expect("repurchase share price"); + let expected_repurchase_total = + crate::derived::runtime_round_f64_to_i64(pressured_share_price * 1_000.0) + .expect("repurchase total should round"); + + let mut state = base_state; + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply annual stock repurchase policy"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&23), + Some(&crate::state::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase) + ); + assert_eq!( + state + .service_state + .annual_finance_last_news_family_candidates + .get(&23), + Some(&"2887".to_string()) + ); + assert_eq!(state.service_state.annual_stock_issue_last_share_count, 0); + assert_eq!( + state.service_state.annual_stock_repurchase_last_share_count, + 1_000 + ); + assert_eq!( + state.companies[0].current_cash, + 1_600_000 - expected_repurchase_total + ); + assert_eq!( + state.service_state.company_market_state[&23].outstanding_shares, + 9_000 + ); + assert!(result.service_events.iter().any(|event| { + event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![23] + && event.finance_news_family_candidates.get(&23) == Some(&"2887".to_string()) + && event.annual_finance_news_events.iter().any(|news| { + news.company_id == 23 + && news.selector_label == "2887" + && news.action_label == "stock_repurchase" + && news.repurchased_share_count == 1_000 + }) + })); +} + +#[test] +fn periodic_boundary_applies_bond_issue_from_annual_finance_policy() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = (-400_000.0f64).to_bits(); + + let mut state = RuntimeState { + calendar: crate::CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..crate::state::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 24, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + current_cash: 0, + debt: 0, + credit_rating_score: Some(6), + prime_rate: Some(5), + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(24), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + company_market_state: BTreeMap::from([( + 24, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1840, + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply annual bond issue policy"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&24), + Some(&crate::state::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) + ); + assert_eq!( + state + .service_state + .annual_finance_last_news_family_candidates + .get(&24), + Some(&"2886".to_string()) + ); + assert_eq!(state.companies[0].current_cash, 100_000); + assert_eq!(state.service_state.company_market_state[&24].bond_count, 1); + assert_eq!( + state.service_state.annual_bond_last_retired_principal_total, + 0 + ); + assert_eq!( + state.service_state.annual_bond_last_issued_principal_total, + 500_000 + ); + assert_eq!( + state.service_state.company_market_state[&24].largest_live_bond_principal, + Some(500_000) + ); + assert_eq!( + state.service_state.company_market_state[&24].highest_coupon_live_bond_principal, + Some(500_000) + ); + assert_eq!( + state.service_state.company_market_state[&24].live_bond_slots, + vec![crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 500_000, + maturity_year: 1875, + coupon_rate_raw_u32: 0.09f32.to_bits(), + }] + ); + assert!(result.service_events.iter().any(|event| { + event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![24] + && event.finance_news_family_candidates.get(&24) == Some(&"2886".to_string()) + && event.annual_finance_news_events.iter().any(|news| { + news.company_id == 24 + && news.selector_label == "2886" + && news.action_label == "bond_issue" + && news.retired_principal_total == 0 + && news.issued_principal_total == 500_000 + }) + })); +} + +#[test] +fn periodic_boundary_retires_live_bonds_when_annual_bond_lane_needs_no_reissue() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value( + &mut year_stat_family_qword_bits, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + 900_000.0, + ); + write_current_value(&mut year_stat_family_qword_bits, 0x12, -350_000.0); + + let mut state = crate::RuntimeState { + calendar: crate::CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..crate::state::RuntimeWorldRestoreState::default() + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 25, + current_cash: 900_000, + debt: 350_000, + active: true, + credit_rating_score: Some(7), + prime_rate: Some(6), + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + available_track_laying_capacity: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(25), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + company_market_state: BTreeMap::from([( + 25, + crate::state::RuntimeCompanyMarketState { + bond_count: 2, + live_bond_slots: vec![ + crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 200_000, + maturity_year: 1845, + coupon_rate_raw_u32: 0.09f32.to_bits(), + }, + crate::state::RuntimeCompanyBondSlot { + slot_index: 1, + principal: 150_000, + maturity_year: 1845, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }, + ], + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply annual bond repayment lane"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&25), + Some(&crate::state::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) + ); + assert_eq!( + state + .service_state + .annual_finance_last_news_family_candidates + .get(&25), + Some(&"2885".to_string()) + ); + assert_eq!(state.companies[0].current_cash, 550_000); + assert_eq!(state.companies[0].debt, 0); + assert_eq!( + state.service_state.annual_bond_last_retired_principal_total, + 350_000 + ); + assert_eq!( + state.service_state.annual_bond_last_issued_principal_total, + 0 + ); + assert_eq!(state.service_state.company_market_state[&25].bond_count, 0); + assert!( + state.service_state.company_market_state[&25] + .live_bond_slots + .is_empty() + ); + assert_eq!( + state.service_state.company_market_state[&25].largest_live_bond_principal, + None + ); + assert_eq!( + state.service_state.company_market_state[&25].highest_coupon_live_bond_principal, + None + ); + assert!(result.service_events.iter().any(|event| { + event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![25] + && event.finance_news_family_candidates.get(&25) == Some(&"2885".to_string()) + && event.annual_finance_news_events.iter().any(|news| { + news.company_id == 25 + && news.selector_label == "2885" + && news.action_label == "bond_issue" + && news.retired_principal_total == 350_000 + && news.issued_principal_total == 0 + }) + })); +} + +#[test] +fn periodic_boundary_retires_then_reissues_exact_annual_bond_count() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value( + &mut year_stat_family_qword_bits, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -400_000.0, + ); + write_current_value(&mut year_stat_family_qword_bits, 0x12, -350_000.0); + + let mut state = crate::RuntimeState { + calendar: crate::CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..crate::state::RuntimeWorldRestoreState::default() + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 26, + current_cash: -400_000, + debt: 350_000, + active: true, + credit_rating_score: Some(7), + prime_rate: Some(6), + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + available_track_laying_capacity: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(26), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + company_market_state: BTreeMap::from([( + 26, + crate::state::RuntimeCompanyMarketState { + bond_count: 2, + linked_transit_latch: true, + live_bond_slots: vec![ + crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 200_000, + maturity_year: 1845, + coupon_rate_raw_u32: 0.09f32.to_bits(), + }, + crate::state::RuntimeCompanyBondSlot { + slot_index: 1, + principal: 150_000, + maturity_year: 1845, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }, + ], + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply annual bond restructure lane"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&26), + Some(&crate::state::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) + ); + assert_eq!( + state + .service_state + .annual_finance_last_news_family_candidates + .get(&26), + Some(&"2883".to_string()) + ); + assert_eq!(state.companies[0].current_cash, 250_000); + assert_eq!(state.companies[0].debt, 1_000_000); + assert_eq!( + state.service_state.annual_bond_last_retired_principal_total, + 350_000 + ); + assert_eq!( + state.service_state.annual_bond_last_issued_principal_total, + 1_000_000 + ); + assert_eq!(state.service_state.company_market_state[&26].bond_count, 2); + assert_eq!( + state.service_state.company_market_state[&26].largest_live_bond_principal, + Some(500_000) + ); + assert_eq!( + state.service_state.company_market_state[&26].highest_coupon_live_bond_principal, + Some(500_000) + ); + assert_eq!( + state.service_state.company_market_state[&26].live_bond_slots, + vec![ + crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 500_000, + maturity_year: 1875, + coupon_rate_raw_u32: 0.09f32.to_bits(), + }, + crate::state::RuntimeCompanyBondSlot { + slot_index: 1, + principal: 500_000, + maturity_year: 1875, + coupon_rate_raw_u32: 0.09f32.to_bits(), + }, + ] + ); + assert!(result.service_events.iter().any(|event| { + event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![26] + && event.finance_news_family_candidates.get(&26) == Some(&"2883".to_string()) + && event.annual_finance_news_events.iter().any(|news| { + news.company_id == 26 + && news.selector_label == "2883" + && news.action_label == "bond_issue" + && news.retired_principal_total == 350_000 + && news.issued_principal_total == 1_000_000 + }) + })); +} + +#[test] +fn periodic_boundary_applies_creditor_pressure_bankruptcy_from_annual_finance_policy() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value( + &mut year_stat_family_qword_bits, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -200_000.0, + ); + write_current_value(&mut year_stat_family_qword_bits, 0x12, -500_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 95_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -125_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -115_000.0); + + let mut state = RuntimeState { + calendar: crate::CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..crate::state::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 31, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + current_cash: 0, + debt: 200_000, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(31), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: vec![crate::state::RuntimeTrain { + train_id: 88, + owner_company_id: 31, + territory_id: None, + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }], + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + company_market_state: BTreeMap::from([( + 31, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 10_000, + bond_count: 1, + largest_live_bond_principal: Some(500_000), + highest_coupon_live_bond_principal: Some(500_000), + cached_share_price_raw_u32: 25.0f32.to_bits(), + founding_year: 1841, + last_bankruptcy_year: 1832, + year_stat_family_qword_bits, + live_bond_slots: vec![crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 500_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }], + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply creditor-pressure bankruptcy"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&31), + Some(&crate::state::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy) + ); + assert_eq!( + state + .service_state + .annual_finance_last_news_family_candidates + .get(&31), + Some(&"2881".to_string()) + ); + assert_eq!(state.companies[0].current_cash, 0); + assert_eq!(state.companies[0].debt, 250_000); + assert!(state.companies[0].active); + assert_eq!(state.selected_company_id, Some(31)); + assert!(!state.trains[0].retired); + assert_eq!( + state.service_state.company_market_state[&31].last_bankruptcy_year, + 1845 + ); + assert_eq!(state.service_state.company_market_state[&31].bond_count, 1); + assert_eq!( + state.service_state.company_market_state[&31].largest_live_bond_principal, + Some(250_000) + ); + assert_eq!( + state.service_state.company_market_state[&31].highest_coupon_live_bond_principal, + Some(250_000) + ); + assert!( + state.service_state.company_market_state[&31].live_bond_slots + == vec![crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 250_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }] + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![31] + && event.finance_news_family_candidates.get(&31) == Some(&"2881".to_string())) + ); +} + +#[test] +fn periodic_boundary_applies_deep_distress_bankruptcy_fallback_from_annual_finance_policy() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value( + &mut year_stat_family_qword_bits, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -350_000.0, + ); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0); + + let mut state = RuntimeState { + calendar: crate::CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..crate::state::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 32, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + current_cash: 0, + debt: 50_000, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(32), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: vec![crate::state::RuntimeTrain { + train_id: 89, + owner_company_id: 32, + territory_id: None, + locomotive_name: Some("Orca".to_string()), + active: true, + retired: false, + }], + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + company_market_state: BTreeMap::from([( + 32, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 8_000, + bond_count: 1, + largest_live_bond_principal: Some(250_000), + highest_coupon_live_bond_principal: Some(250_000), + founding_year: 1841, + last_bankruptcy_year: 1840, + year_stat_family_qword_bits, + live_bond_slots: vec![crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 250_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.07f32.to_bits(), + }], + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply deep-distress bankruptcy fallback"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&32), + Some( + &crate::state::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback + ) + ); + assert_eq!( + state + .service_state + .annual_finance_last_news_family_candidates + .get(&32), + Some(&"2881".to_string()) + ); + assert_eq!(state.companies[0].current_cash, 0); + assert_eq!(state.companies[0].debt, 125_000); + assert!(state.companies[0].active); + assert_eq!(state.selected_company_id, Some(32)); + assert!(!state.trains[0].retired); + assert_eq!( + state.service_state.company_market_state[&32].last_bankruptcy_year, + 1845 + ); + assert_eq!(state.service_state.company_market_state[&32].bond_count, 1); + assert_eq!( + state.service_state.company_market_state[&32].largest_live_bond_principal, + Some(125_000) + ); + assert_eq!( + state.service_state.company_market_state[&32].highest_coupon_live_bond_principal, + Some(125_000) + ); + assert!( + state.service_state.company_market_state[&32].live_bond_slots + == vec![crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 125_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.07f32.to_bits(), + }] + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![32] + && event.finance_news_family_candidates.get(&32) == Some(&"2881".to_string())) + ); +} diff --git a/crates/rrt-runtime/src/engine/tests/effects.rs b/crates/rrt-runtime/src/engine/tests/effects.rs new file mode 100644 index 0000000..061ada9 --- /dev/null +++ b/crates/rrt-runtime/src/engine/tests/effects.rs @@ -0,0 +1,1439 @@ +use super::*; + +#[test] +fn applies_company_effects_for_specific_targets() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash: 10, + debt: 5, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash: 20, + debt: 8, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 10, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::Ids { ids: vec![2] }, + delta: 4, + }, + RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::Ids { ids: vec![2] }, + delta: -3, + }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("targeted company effects should succeed"); + + assert_eq!(state.companies[0].current_cash, 10); + assert_eq!(state.companies[1].current_cash, 24); + assert_eq!(state.companies[1].debt, 5); + assert_eq!(result.service_events[0].applied_effect_count, 2); + assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); +} + +#[test] +fn set_company_cash_updates_owner_state_backed_current_cash() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 100.0f64.to_bits(); + + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 10, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + value: 250, + }], + }], + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + year_stat_family_qword_bits, + special_stat_family_232a_qword_bits: vec![0u64; 0x20], + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + calendar: crate::CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("cash effect should apply through owner state"); + + assert_eq!(state.companies[0].current_cash, 250); + assert_eq!( + crate::derived::runtime_company_stat_value( + &state, + 1, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + Some(250) + ); +} + +#[test] +fn set_company_governance_scalar_updates_owner_state_backed_metrics() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 11, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::PrimeRate, + value: 6, + }, + RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::BookValuePerShare, + value: 2620, + }, + RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::InvestorConfidence, + value: 37, + }, + RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::ManagementAttitude, + value: 58, + }, + ], + }], + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: { + let mut terms = vec![0; 0x3b]; + terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 0; + terms + }, + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + issue_opinion_terms_raw_i32: vec![0; 0x3b], + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + calendar: crate::CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + ..crate::state::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("governance effect should apply through owner state"); + + assert_eq!(state.companies[0].prime_rate, Some(6)); + assert_eq!(state.companies[0].book_value_per_share, 2620); + assert_eq!(state.companies[0].investor_confidence, 37); + assert_eq!(state.companies[0].management_attitude, 58); + assert_eq!( + crate::derived::runtime_company_prime_rate(&state, 1), + Some(6) + ); + assert_eq!( + crate::derived::runtime_company_book_value_per_share(&state, 1), + Some(2620) + ); + assert_eq!( + crate::derived::runtime_company_investor_confidence(&state, 1), + Some(37) + ); + assert_eq!( + crate::derived::runtime_company_management_attitude(&state, 1), + Some(58) + ); + assert_eq!( + state.service_state.company_market_state[&1] + .direct_control_transfer_float_fields_raw_u32 + .get(&0x32f) + .copied(), + Some(2620.0f32.to_bits()) + ); + assert_eq!( + state.service_state.company_market_state[&1].cached_share_price_raw_u32, + 37.0f32.to_bits() + ); + assert_eq!( + state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32 + [RUNTIME_WORLD_ISSUE_PRIME_RATE as usize], + 100 + ); + assert_eq!( + state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32 + [RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize], + 58 + ); +} + +#[test] +fn set_company_credit_rating_governance_scalar_updates_issue38_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = + 20.0f64.to_bits(); + year_stat_family_qword_bits[(0x01 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = + 100.0f64.to_bits(); + year_stat_family_qword_bits[(0x09 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = + 0.0f64.to_bits(); + + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + issue_38_value: Some(2), + packed_year_word_raw_u16: Some(1835), + ..RuntimeWorldRestoreState::default() + }, + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1830, + last_bankruptcy_year: 1800, + year_stat_family_qword_bits, + issue_opinion_terms_raw_i32: vec![0; 0x3b], + live_bond_slots: vec![crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 100_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.05f32.to_bits(), + }], + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 13, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::CreditRating, + value: 7, + }], + }], + calendar: crate::CalendarPoint { + year: 1835, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + metadata: BTreeMap::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("credit-rating governance effect should apply through owner state"); + + assert_eq!(state.companies[0].credit_rating_score, Some(7)); + assert_eq!( + crate::derived::runtime_company_credit_rating(&state, 1), + Some(7) + ); + assert_eq!( + state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32 + [RUNTIME_WORLD_ISSUE_CREDIT_MARKET as usize], + -3 + ); +} + +#[test] +fn adjust_company_cash_updates_owner_state_backed_current_cash() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 100.0f64.to_bits(); + + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 12, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + delta: 25, + }], + }], + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + year_stat_family_qword_bits, + special_stat_family_232a_qword_bits: vec![0u64; 0x20], + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + calendar: crate::CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("cash adjustment should apply through owner state"); + + assert_eq!(state.companies[0].current_cash, 125); + assert_eq!( + crate::derived::runtime_company_stat_value( + &state, + 1, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + Some(125) + ); +} + +#[test] +fn applies_named_locomotive_availability_effects() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 10, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetNamedLocomotiveAvailability { + name: "Big Boy".to_string(), + value: false, + }, + RuntimeEffect::SetNamedLocomotiveAvailability { + name: "GP7".to_string(), + value: true, + }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("named locomotive availability effects should succeed"); + + assert_eq!(state.named_locomotive_availability.get("Big Boy"), Some(&0)); + assert_eq!(state.named_locomotive_availability.get("GP7"), Some(&1)); + assert_eq!(result.service_events[0].applied_effect_count, 2); +} + +#[test] +fn applies_named_locomotive_cost_effects() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 11, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetNamedLocomotiveCost { + name: "Big Boy".to_string(), + value: 250000, + }, + RuntimeEffect::SetNamedLocomotiveCost { + name: "GP7".to_string(), + value: 175000, + }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("named locomotive cost effects should succeed"); + + assert_eq!(state.named_locomotive_cost.get("Big Boy"), Some(&250000)); + assert_eq!(state.named_locomotive_cost.get("GP7"), Some(&175000)); + assert_eq!(result.service_events[0].applied_effect_count, 2); +} + +#[test] +fn applies_scalar_named_locomotive_availability_effects() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 12, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetNamedLocomotiveAvailabilityValue { + name: "Big Boy".to_string(), + value: 42, + }, + RuntimeEffect::SetNamedLocomotiveAvailabilityValue { + name: "GP7".to_string(), + value: 7, + }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("scalar named locomotive availability effects should succeed"); + + assert_eq!( + state.named_locomotive_availability.get("Big Boy"), + Some(&42) + ); + assert_eq!(state.named_locomotive_availability.get("GP7"), Some(&7)); + assert_eq!(result.service_events[0].applied_effect_count, 2); +} + +#[test] +fn applies_world_scalar_override_effects() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 13, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetWorldScalarOverride { + key: "world.build_stations_cost".to_string(), + value: 350000, + }, + RuntimeEffect::SetCargoPriceOverride { + target: RuntimeCargoPriceTarget::All, + value: 180, + }, + RuntimeEffect::SetCargoPriceOverride { + target: RuntimeCargoPriceTarget::Named { + name: "Coal".to_string(), + }, + value: 95, + }, + RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::Factory, + value: 225, + }, + RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::Named { + name: "Corn".to_string(), + }, + value: 140, + }, + RuntimeEffect::SetCargoProductionSlot { + slot: 1, + value: 125, + }, + RuntimeEffect::SetTerritoryAccessCost { value: 750000 }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("world scalar override effects should succeed"); + + assert_eq!( + state + .world_scalar_overrides + .get("world.build_stations_cost"), + Some(&350000) + ); + assert_eq!(state.all_cargo_price_override, Some(180)); + assert_eq!(state.named_cargo_price_overrides.get("Coal"), Some(&95)); + assert_eq!(state.factory_cargo_production_override, Some(225)); + assert_eq!( + state.named_cargo_production_overrides.get("Corn"), + Some(&140) + ); + assert_eq!(state.cargo_production_overrides.get(&1), Some(&125)); + assert_eq!(state.world_restore.territory_access_cost, Some(750000)); + assert_eq!(result.service_events[0].applied_effect_count, 7); +} + +#[test] +fn applies_locomotive_policy_world_flags_through_owner_state() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 213, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetWorldFlag { + key: "world.all_steam_locos_available".to_string(), + value: true, + }, + RuntimeEffect::SetWorldFlag { + key: "world.all_diesel_locos_available".to_string(), + value: false, + }, + RuntimeEffect::SetWorldFlag { + key: "world.all_electric_locos_available".to_string(), + value: true, + }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("locomotive policy world flags should succeed"); + + assert_eq!( + state.world_flags.get("world.all_steam_locos_available"), + Some(&true) + ); + assert_eq!( + state.world_restore.all_steam_locomotives_available_raw_u8, + Some(1) + ); + assert_eq!( + state.world_restore.all_diesel_locomotives_available_raw_u8, + Some(0) + ); + assert_eq!( + state + .world_restore + .all_electric_locomotives_available_raw_u8, + Some(1) + ); + assert_eq!(result.service_events[0].applied_effect_count, 3); +} + +#[test] +fn applies_runtime_variable_effects() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 20, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + players: vec![RuntimePlayer { + player_id: 9, + current_cash: 0, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("North".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 14, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetWorldVariable { + index: 1, + value: -5, + }, + RuntimeEffect::SetCompanyVariable { + target: RuntimeCompanyTarget::AllActive, + index: 2, + value: 17, + }, + RuntimeEffect::SetPlayerVariable { + target: RuntimePlayerTarget::Ids { ids: vec![9] }, + index: 3, + value: 99, + }, + RuntimeEffect::SetTerritoryVariable { + target: RuntimeTerritoryTarget::Ids { ids: vec![7] }, + index: 4, + value: 1234, + }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("runtime variable effects should succeed"); + + assert_eq!(state.world_runtime_variables.get(&1), Some(&-5)); + assert_eq!( + state + .company_runtime_variables + .get(&1) + .and_then(|vars| vars.get(&2)), + Some(&17) + ); + assert_eq!( + state + .company_runtime_variables + .get(&2) + .and_then(|vars| vars.get(&2)), + Some(&17) + ); + assert_eq!( + state + .player_runtime_variables + .get(&9) + .and_then(|vars| vars.get(&3)), + Some(&99) + ); + assert_eq!( + state + .territory_runtime_variables + .get(&7) + .and_then(|vars| vars.get(&4)), + Some(&1234) + ); + assert_eq!(result.service_events[0].applied_effect_count, 4); +} + +#[test] +fn applies_economic_status_code_effect() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 90, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetEconomicStatusCode { value: 3 }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("economic-status effect should succeed"); + + assert_eq!(state.world_restore.economic_status_code, Some(3)); +} + +#[test] +fn applies_limited_track_building_amount_effect() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 91, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("limited-track-building-amount effect should succeed"); + + assert_eq!(state.world_restore.limited_track_building_amount, Some(18)); +} + +#[test] +fn confiscate_company_assets_zeros_company_and_retires_owned_trains() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 50.0f64.to_bits(); + + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 50, + debt: 7, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 80, + debt: 9, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + selected_company_id: Some(1), + trains: vec![ + RuntimeTrain { + train_id: 10, + owner_company_id: 1, + territory_id: None, + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 11, + owner_company_id: 2, + territory_id: None, + locomotive_name: Some("Orca".to_string()), + active: true, + retired: false, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 91, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::ConfiscateCompanyAssets { + target: RuntimeCompanyTarget::SelectedCompany, + }], + }], + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + year_stat_family_qword_bits, + special_stat_family_232a_qword_bits: vec![0u64; 0x20], + live_bond_slots: vec![crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 20_000, + maturity_year: 1845, + coupon_rate_raw_u32: 0.05f32.to_bits(), + }], + bond_count: 1, + largest_live_bond_principal: Some(20_000), + highest_coupon_live_bond_principal: Some(20_000), + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("confiscation effect should succeed"); + + assert_eq!(state.companies[0].current_cash, 0); + assert_eq!(state.companies[0].debt, 0); + assert!(!state.companies[0].active); + assert_eq!(state.selected_company_id, None); + assert!(state.trains[0].retired); + assert!(!state.trains[1].retired); + assert_eq!( + crate::derived::runtime_company_stat_value( + &state, + 1, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + Some(0) + ); + assert!( + state.service_state.company_market_state[&1] + .live_bond_slots + .is_empty() + ); + assert_eq!(state.service_state.company_market_state[&1].bond_count, 0); + assert_eq!( + state.service_state.company_market_state[&1].largest_live_bond_principal, + None + ); + assert_eq!( + state.service_state.company_market_state[&1].highest_coupon_live_bond_principal, + None + ); +} + +#[test] +fn retire_trains_respects_company_territory_and_locomotive_filters() { + let mut state = RuntimeState { + territories: vec![ + RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + RuntimeTerritory { + territory_id: 8, + name: Some("Great Plains".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + ], + trains: vec![ + RuntimeTrain { + train_id: 10, + owner_company_id: 1, + territory_id: Some(7), + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 11, + owner_company_id: 1, + territory_id: Some(7), + locomotive_name: Some("Orca".to_string()), + active: true, + retired: false, + }, + RuntimeTrain { + train_id: 12, + owner_company_id: 1, + territory_id: Some(8), + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 92, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::RetireTrains { + company_target: Some(RuntimeCompanyTarget::SelectedCompany), + territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }), + locomotive_name: Some("Mikado".to_string()), + }], + }], + selected_company_id: Some(1), + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("retire-trains effect should succeed"); + + assert!(state.trains[0].retired); + assert!(!state.trains[1].retired); + assert!(!state.trains[2].retired); +} + +#[test] +fn set_chairman_cash_supports_all_active_target() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + linked_chairman_profile_id: Some(1), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + RuntimeCompany { + company_id: 2, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + linked_chairman_profile_id: Some(2), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + ], + chairman_profiles: vec![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 10, + linked_company_id: Some(1), + company_holdings: BTreeMap::from([(1, 2)]), + holdings_value_total: 20, + net_worth_total: 30, + purchasing_power_total: 70, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 20, + linked_company_id: Some(2), + company_holdings: BTreeMap::from([(2, 3)]), + holdings_value_total: 60, + net_worth_total: 80, + purchasing_power_total: 110, + }, + RuntimeChairmanProfile { + profile_id: 3, + name: "Chairman Three".to_string(), + active: false, + current_cash: 30, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 30, + purchasing_power_total: 30, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 93, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetChairmanCash { + target: RuntimeChairmanTarget::AllActive, + value: 77, + }], + }], + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([ + ( + 1, + crate::state::RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41200000, + ..crate::state::RuntimeCompanyMarketState::default() + }, + ), + ( + 2, + crate::state::RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41a00000, + ..crate::state::RuntimeCompanyMarketState::default() + }, + ), + ]), + ..RuntimeServiceState::default() + }, + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("all-active chairman cash effect should succeed"); + + assert_eq!(state.chairman_profiles[0].current_cash, 77); + assert_eq!(state.chairman_profiles[1].current_cash, 77); + assert_eq!(state.chairman_profiles[2].current_cash, 30); + assert_eq!(state.chairman_profiles[0].net_worth_total, 97); + assert_eq!(state.chairman_profiles[0].purchasing_power_total, 137); + assert_eq!(state.chairman_profiles[1].net_worth_total, 137); + assert_eq!(state.chairman_profiles[1].purchasing_power_total, 167); +} + +#[test] +fn deactivate_chairman_clears_selected_and_company_links_for_ids_target() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(1), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 80, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(2), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + chairman_profiles: vec![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 10, + linked_company_id: Some(1), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 20, + linked_company_id: Some(2), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + ], + selected_chairman_profile_id: Some(2), + event_runtime_records: vec![RuntimeEventRecord { + record_id: 94, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::DeactivateChairman { + target: RuntimeChairmanTarget::Ids { ids: vec![2] }, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("ids-target chairman deactivation should succeed"); + + assert!(state.chairman_profiles[0].active); + assert!(!state.chairman_profiles[1].active); + assert_eq!(state.chairman_profiles[1].linked_company_id, None); + assert_eq!(state.selected_chairman_profile_id, None); + assert_eq!(state.companies[0].linked_chairman_profile_id, Some(1)); + assert_eq!(state.companies[1].linked_chairman_profile_id, None); +} diff --git a/crates/rrt-runtime/src/engine/tests/event_graph.rs b/crates/rrt-runtime/src/engine/tests/event_graph.rs new file mode 100644 index 0000000..0add2dd --- /dev/null +++ b/crates/rrt-runtime/src/engine/tests/event_graph.rs @@ -0,0 +1,504 @@ +use super::*; + +#[test] +fn evaluates_runtime_variable_conditions_before_effects_run() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(1), + players: vec![RuntimePlayer { + player_id: 1, + current_cash: 50, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_player_id: Some(1), + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("North".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + world_runtime_variables: BTreeMap::from([(1, 111)]), + company_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(2, 222)]))]), + player_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(3, 333)]))]), + territory_runtime_variables: BTreeMap::from([(7, BTreeMap::from([(4, 444)]))]), + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 27, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![ + RuntimeCondition::WorldVariableThreshold { + index: 1, + comparator: RuntimeConditionComparator::Eq, + value: 111, + }, + RuntimeCondition::CompanyVariableThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + index: 2, + comparator: RuntimeConditionComparator::Eq, + value: 222, + }, + RuntimeCondition::PlayerVariableThreshold { + target: RuntimePlayerTarget::SelectedPlayer, + index: 3, + comparator: RuntimeConditionComparator::Eq, + value: 333, + }, + RuntimeCondition::TerritoryVariableThreshold { + target: RuntimeTerritoryTarget::Ids { ids: vec![7] }, + index: 4, + comparator: RuntimeConditionComparator::Eq, + value: 444, + }, + ], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "runtime_variable_condition_passed".to_string(), + value: true, + }], + }, + RuntimeEventRecord { + record_id: 28, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::PlayerVariableThreshold { + target: RuntimePlayerTarget::SelectedPlayer, + index: 4, + comparator: RuntimeConditionComparator::Gt, + value: 0, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "runtime_variable_condition_failed".to_string(), + value: true, + }], + }, + ], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("runtime-variable conditions should evaluate successfully"); + + assert_eq!(result.service_events[0].serviced_record_ids, vec![27]); + assert_eq!( + state.world_flags.get("runtime_variable_condition_passed"), + Some(&true) + ); + assert_eq!( + state.world_flags.get("runtime_variable_condition_failed"), + None + ); +} + +#[test] +fn one_shot_record_only_fires_once() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 20, + trigger_kind: 2, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: true, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetWorldFlag { + key: "one_shot".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 2 }, + ) + .expect("first one-shot service should succeed"); + let second = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 2 }, + ) + .expect("second one-shot service should succeed"); + + assert_eq!(state.event_runtime_records[0].service_count, 1); + assert!(state.event_runtime_records[0].has_fired); + assert_eq!( + second.service_events[0].serviced_record_ids, + Vec::::new() + ); + assert_eq!(second.service_events[0].applied_effect_count, 0); +} + +#[test] +fn rejects_debt_underflow() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash: 10, + debt: 2, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 30, + trigger_kind: 3, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::AllActive, + delta: -3, + }], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 3 }, + ); + + assert!(result.is_err()); +} + +#[test] +fn appended_record_waits_until_later_pass_without_dirty_rerun() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 40, + trigger_kind: 5, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: true, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AppendEventRecord { + record: Box::new(RuntimeEventRecordTemplate { + record_id: 41, + trigger_kind: 5, + active: true, + marks_collection_dirty: false, + one_shot: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetWorldFlag { + key: "follow_on_later_pass".to_string(), + value: true, + }], + }), + }], + }], + ..state() + }; + + let first = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 5 }, + ) + .expect("first pass should succeed"); + + assert_eq!(first.service_events.len(), 1); + assert_eq!(first.service_events[0].serviced_record_ids, vec![40]); + assert_eq!(first.service_events[0].appended_record_ids, vec![41]); + assert_eq!(state.world_flags.get("follow_on_later_pass"), None); + assert_eq!(state.event_runtime_records.len(), 2); + assert_eq!(state.event_runtime_records[1].service_count, 0); + + let second = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 5 }, + ) + .expect("second pass should succeed"); + + assert_eq!(second.service_events[0].serviced_record_ids, vec![41]); + assert_eq!(state.world_flags.get("follow_on_later_pass"), Some(&true)); + assert!(state.event_runtime_records[0].has_fired); + assert_eq!(state.event_runtime_records[1].service_count, 1); +} + +#[test] +fn appended_record_runs_in_dirty_rerun_after_commit() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 50, + trigger_kind: 1, + active: true, + service_count: 0, + marks_collection_dirty: true, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AppendEventRecord { + record: Box::new(RuntimeEventRecordTemplate { + record_id: 51, + trigger_kind: 0x0a, + active: true, + marks_collection_dirty: false, + one_shot: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetWorldFlag { + key: "dirty_rerun_follow_on".to_string(), + value: true, + }], + }), + }], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 1 }, + ) + .expect("dirty rerun with follow-on should succeed"); + + assert_eq!(result.service_events.len(), 2); + assert_eq!(result.service_events[0].serviced_record_ids, vec![50]); + assert_eq!(result.service_events[0].appended_record_ids, vec![51]); + assert_eq!(result.service_events[1].trigger_kind, Some(0x0a)); + assert_eq!(result.service_events[1].serviced_record_ids, vec![51]); + assert_eq!(state.service_state.dirty_rerun_count, 1); + assert_eq!(state.event_runtime_records.len(), 2); + assert_eq!(state.event_runtime_records[1].service_count, 1); + assert_eq!(state.world_flags.get("dirty_rerun_follow_on"), Some(&true)); +} + +#[test] +fn lifecycle_mutations_commit_between_passes() { + let mut state = RuntimeState { + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 60, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: true, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::AppendEventRecord { + record: Box::new(RuntimeEventRecordTemplate { + record_id: 64, + trigger_kind: 7, + active: true, + marks_collection_dirty: false, + one_shot: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetCandidateAvailability { + name: "Appended Industry".to_string(), + value: 1, + }], + }), + }, + RuntimeEffect::DeactivateEventRecord { record_id: 61 }, + RuntimeEffect::ActivateEventRecord { record_id: 62 }, + RuntimeEffect::RemoveEventRecord { record_id: 63 }, + ], + }, + RuntimeEventRecord { + record_id: 61, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetWorldFlag { + key: "deactivated_after_first_pass".to_string(), + value: true, + }], + }, + RuntimeEventRecord { + record_id: 62, + trigger_kind: 7, + active: false, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetSpecialCondition { + label: "Activated On Second Pass".to_string(), + value: 1, + }], + }, + RuntimeEventRecord { + record_id: 63, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetWorldFlag { + key: "removed_after_first_pass".to_string(), + value: true, + }], + }, + ], + ..state() + }; + + let first = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("first lifecycle pass should succeed"); + + assert_eq!( + first.service_events[0].serviced_record_ids, + vec![60, 61, 63] + ); + assert_eq!(first.service_events[0].appended_record_ids, vec![64]); + assert_eq!(first.service_events[0].activated_record_ids, vec![62]); + assert_eq!(first.service_events[0].deactivated_record_ids, vec![61]); + assert_eq!(first.service_events[0].removed_record_ids, vec![63]); + assert_eq!( + state + .event_runtime_records + .iter() + .map(|record| (record.record_id, record.active)) + .collect::>(), + vec![(60, true), (61, false), (62, true), (64, true)] + ); + + let second = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("second lifecycle pass should succeed"); + + assert_eq!(second.service_events[0].serviced_record_ids, vec![62, 64]); + assert_eq!( + state.special_conditions.get("Activated On Second Pass"), + Some(&1) + ); + assert_eq!( + state.candidate_availability.get("Appended Industry"), + Some(&1) + ); + assert_eq!( + state.world_flags.get("deactivated_after_first_pass"), + Some(&true) + ); + assert_eq!( + state.world_flags.get("removed_after_first_pass"), + Some(&true) + ); +} + +#[test] +fn rejects_duplicate_appended_record_id() { + let mut state = RuntimeState { + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 70, + trigger_kind: 4, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AppendEventRecord { + record: Box::new(RuntimeEventRecordTemplate { + record_id: 71, + trigger_kind: 4, + active: true, + marks_collection_dirty: false, + one_shot: false, + conditions: Vec::new(), + effects: Vec::new(), + }), + }], + }, + RuntimeEventRecord { + record_id: 71, + trigger_kind: 4, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: Vec::new(), + }, + ], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 4 }, + ); + + assert!(result.is_err()); +} + +#[test] +fn rejects_missing_lifecycle_mutation_target() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 80, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::ActivateEventRecord { record_id: 999 }], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ); + + assert!(result.is_err()); +} diff --git a/crates/rrt-runtime/src/engine/tests/mod.rs b/crates/rrt-runtime/src/engine/tests/mod.rs new file mode 100644 index 0000000..d8a2fe7 --- /dev/null +++ b/crates/rrt-runtime/src/engine/tests/mod.rs @@ -0,0 +1,21 @@ +use super::*; +use std::collections::BTreeMap; + +use crate::CalendarPoint; +use crate::calendar::{MONTH_SLOTS_PER_YEAR, PHASE_SLOTS_PER_MONTH}; +use crate::event::conditions::*; +use crate::event::effects::*; +use crate::event::metrics::*; +use crate::event::records::*; +use crate::event::targets::*; +use crate::state::*; + +mod advance; +mod annual_finance; +mod effects; +mod event_graph; +mod periodic; +mod support; +mod targets; + +use support::*; diff --git a/crates/rrt-runtime/src/engine/tests/periodic.rs b/crates/rrt-runtime/src/engine/tests/periodic.rs new file mode 100644 index 0000000..f980f9a --- /dev/null +++ b/crates/rrt-runtime/src/engine/tests/periodic.rs @@ -0,0 +1,257 @@ +use super::*; + +#[test] +fn services_periodic_trigger_order_and_dirty_rerun() { + let mut state = RuntimeState { + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 1, + trigger_kind: 1, + active: true, + service_count: 0, + marks_collection_dirty: true, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetWorldFlag { + key: "runtime.effect_fired".to_string(), + value: true, + }], + }, + RuntimeEventRecord { + record_id: 2, + trigger_kind: 4, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::AllActive, + delta: 5, + }], + }, + RuntimeEventRecord { + record_id: 3, + trigger_kind: 0x0a, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetSpecialCondition { + label: "Dirty rerun fired".to_string(), + value: 1, + }], + }, + ], + ..state() + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary service should succeed"); + + assert_eq!(result.steps_executed, 0); + assert_eq!(state.service_state.periodic_boundary_calls, 1); + assert_eq!(state.service_state.total_event_record_services, 3); + assert_eq!(state.service_state.dirty_rerun_count, 1); + assert_eq!(state.event_runtime_records[0].service_count, 1); + assert_eq!(state.event_runtime_records[1].service_count, 1); + assert_eq!(state.event_runtime_records[2].service_count, 1); + assert_eq!(state.world_flags.get("runtime.effect_fired"), Some(&true)); + assert_eq!(state.companies[0].current_cash, 15); + assert_eq!(state.special_conditions.get("Dirty rerun fired"), Some(&1)); + assert_eq!( + state.service_state.trigger_dispatch_counts.get(&1), + Some(&1) + ); + assert_eq!( + state.service_state.trigger_dispatch_counts.get(&4), + Some(&1) + ); + assert_eq!( + state.service_state.trigger_dispatch_counts.get(&0x0a), + Some(&1) + ); + assert_eq!(result.service_events.len(), 9); + assert_eq!(result.service_events[0].applied_effect_count, 1); + assert_eq!( + result + .service_events + .iter() + .find(|event| event.trigger_kind == Some(4)) + .expect("trigger kind 4 event should be present") + .applied_effect_count, + 1 + ); + assert_eq!( + result + .service_events + .iter() + .find(|event| event.trigger_kind == Some(0x0a)) + .expect("trigger kind 0x0a event should be present") + .applied_effect_count, + 1 + ); + assert_eq!( + result + .service_events + .iter() + .find(|event| event.trigger_kind == Some(4)) + .expect("trigger kind 4 event should be present") + .mutated_company_ids, + vec![1] + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "near_city_acquisition") + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "annual_finance_policy") + ); +} + +#[test] +fn periodic_boundary_applies_near_city_acquisition_and_emits_news() { + let mut state = state(); + state.service_state.company_market_state = BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + city_connection_latch: true, + linked_transit_latch: false, + linked_transit_route_anchor_entry_id: Some(77), + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]); + state.service_state.near_city_acquisition_regions = + vec![crate::state::RuntimeNearCityAcquisitionRegion { + region_id: 0, + name: "Marker09".to_string(), + profile_names: vec!["Farm Corn".to_string()], + fixed_row_shape_family_signature: Some("family-a".to_string()), + fixed_row_best_density_lane_relative_offset_hex: Some("0x24".to_string()), + }]; + state.service_state.near_city_acquisition_sites = + vec![crate::state::RuntimeNearCityAcquisitionSite { + site_id: 0, + primary_name: "FarmCorn".to_string(), + secondary_name: "FarmSet".to_string(), + region_id: Some(0), + owner_company_id: None, + preferred_company_id: Some(1), + owner_company_id_provenance: + crate::state::RuntimeNearCityAcquisitionValueProvenance::BestEffortGuess, + self_id: 0, + self_id_provenance: crate::state::RuntimeNearCityAcquisitionValueProvenance::Grounded, + candidate_subtype_label: "farm".to_string(), + candidate_subtype_provenance: + crate::state::RuntimeNearCityAcquisitionValueProvenance::Grounded, + cached_tri_lane_0: 0xff0000ff, + cached_tri_lane_1: 1, + cached_tri_lane_2: 0x24, + cached_tri_lane_provenance: + crate::state::RuntimeNearCityAcquisitionValueProvenance::Grounded, + nontransport_source_label: "best_effort_company_market_latch_projection".to_string(), + tri_lane_source_label: "matched_side_buffer_name_pair_plus_region_fixed_row_candidate" + .to_string(), + }]; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply near-city acquisition"); + + assert_eq!(state.service_state.near_city_acquisition_service_calls, 1); + assert_eq!( + state.service_state.near_city_acquisition_sites[0].owner_company_id, + Some(1) + ); + assert_eq!( + state + .service_state + .near_city_acquisition_last_news_events + .len(), + 1 + ); + assert_eq!( + state.service_state.near_city_acquisition_last_news_events[0].selector_label, + "near_city_farm_acquisition" + ); + assert!(result.service_events.iter().any(|event| { + event.kind == "near_city_acquisition" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![1] + && event.near_city_acquisition_news_family_candidates.get(&0) + == Some(&"near_city_farm_acquisition".to_string()) + && event.near_city_acquisition_news_events.len() == 1 + })); +} + +#[test] +fn periodic_boundary_clears_transient_preferred_locomotive_side_latch() { + let mut state = state(); + state.service_state.company_periodic_side_latch_state = BTreeMap::from([( + 1, + RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: Some(2), + city_connection_latch: true, + linked_transit_latch: false, + }, + )]); + + execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should refresh periodic side latches"); + + assert_eq!( + state + .service_state + .company_periodic_side_latch_state + .get(&1), + Some(&RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: None, + city_connection_latch: true, + linked_transit_latch: false, + }) + ); +} + +#[test] +fn periodic_boundary_reseeds_finance_side_latches_from_market_state() { + let mut state = state(); + state.service_state.company_market_state = BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + city_connection_latch: false, + linked_transit_latch: true, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]); + state.service_state.company_periodic_side_latch_state = BTreeMap::from([( + 1, + RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: Some(2), + city_connection_latch: true, + linked_transit_latch: false, + }, + )]); + + execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should reseed finance latches from market state"); + + assert_eq!( + state + .service_state + .company_periodic_side_latch_state + .get(&1), + Some(&RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: None, + city_connection_latch: false, + linked_transit_latch: true, + }) + ); +} diff --git a/crates/rrt-runtime/src/engine/tests/support.rs b/crates/rrt-runtime/src/engine/tests/support.rs new file mode 100644 index 0000000..dbe2838 --- /dev/null +++ b/crates/rrt-runtime/src/engine/tests/support.rs @@ -0,0 +1,25 @@ +use super::*; + +use crate::test_support::empty_runtime_state; + +pub(super) fn state() -> RuntimeState { + let mut state = empty_runtime_state(); + state.companies.push(RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }); + state +} diff --git a/crates/rrt-runtime/src/engine/tests/targets.rs b/crates/rrt-runtime/src/engine/tests/targets.rs new file mode 100644 index 0000000..1eb6bbb --- /dev/null +++ b/crates/rrt-runtime/src/engine/tests/targets.rs @@ -0,0 +1,1289 @@ +use super::*; + +#[test] +fn resolves_symbolic_company_targets() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 20, + debt: 2, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + selected_company_id: Some(1), + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 11, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::HumanCompanies, + delta: 5, + }], + }, + RuntimeEventRecord { + record_id: 12, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::AiCompanies, + delta: 3, + }], + }, + RuntimeEventRecord { + record_id: 13, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::SelectedCompany, + delta: 7, + }], + }, + ], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("symbolic target effects should succeed"); + + assert_eq!(state.companies[0].current_cash, 22); + assert_eq!(state.companies[0].debt, 0); + assert_eq!(state.companies[1].current_cash, 20); + assert_eq!(state.companies[1].debt, 5); + assert_eq!(result.service_events[0].mutated_company_ids, vec![1, 2]); +} + +#[test] +fn rejects_selected_company_target_without_selection_context() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 14, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::SelectedCompany, + delta: 1, + }], + }], + ..state() + }; + + let error = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect_err("selected company target should require selection context"); + + assert!(error.contains("selected_company_id")); +} + +#[test] +fn rejects_human_or_ai_targets_without_role_context() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 15, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::HumanCompanies, + delta: 1, + }], + }], + ..state() + }; + + let error = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect_err("human target should require controller metadata"); + + assert!(error.contains("controller_kind")); +} + +#[test] +fn all_active_and_role_targets_exclude_inactive_companies() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 1, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 20, + debt: 2, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: false, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 3, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 30, + debt: 3, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 16, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::AllActive, + delta: 5, + }], + }, + RuntimeEventRecord { + record_id: 17, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::HumanCompanies, + delta: 4, + }], + }, + RuntimeEventRecord { + record_id: 18, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::AiCompanies, + delta: 6, + }], + }, + ], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("active-company filtering should succeed"); + + assert_eq!(state.companies[0].current_cash, 15); + assert_eq!(state.companies[1].current_cash, 20); + assert_eq!(state.companies[2].current_cash, 35); + assert_eq!(state.companies[0].debt, 5); + assert_eq!(state.companies[1].debt, 2); + assert_eq!(state.companies[2].debt, 9); +} + +#[test] +fn deactivating_selected_company_clears_selection() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: Some(8), + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }], + selected_company_id: Some(1), + event_runtime_records: vec![RuntimeEventRecord { + record_id: 19, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::DeactivateCompany { + target: RuntimeCompanyTarget::SelectedCompany, + }], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("deactivate company effect should succeed"); + + assert!(!state.companies[0].active); + assert_eq!(state.selected_company_id, None); + assert_eq!(result.service_events[0].mutated_company_ids, vec![1]); +} + +#[test] +fn deactivating_selected_player_clears_selection() { + let mut state = RuntimeState { + players: vec![RuntimePlayer { + player_id: 1, + current_cash: 500, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_player_id: Some(1), + event_runtime_records: vec![RuntimeEventRecord { + record_id: 19, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::DeactivatePlayer { + target: RuntimePlayerTarget::SelectedPlayer, + }], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("deactivate player effect should succeed"); + + assert!(!state.players[0].active); + assert_eq!(state.selected_player_id, None); + assert_eq!(result.service_events[0].mutated_player_ids, vec![1]); +} + +#[test] +fn sets_track_laying_capacity_for_resolved_targets() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 20, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 20, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { + target: RuntimeCompanyTarget::Ids { ids: vec![2] }, + value: Some(14), + }], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("track capacity effect should succeed"); + + assert_eq!(state.companies[0].available_track_laying_capacity, None); + assert_eq!(state.companies[1].available_track_laying_capacity, Some(14)); + assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); +} + +#[test] +fn sets_and_clears_company_territory_access_for_resolved_targets() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 20, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + territories: vec![ + RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + RuntimeTerritory { + territory_id: 8, + name: Some("Great Plains".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + ], + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 21, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: true, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetCompanyTerritoryAccess { + target: RuntimeCompanyTarget::SelectedCompany, + territory: RuntimeTerritoryTarget::Ids { ids: vec![7, 8] }, + value: true, + }], + }, + RuntimeEventRecord { + record_id: 22, + trigger_kind: 8, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: true, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetCompanyTerritoryAccess { + target: RuntimeCompanyTarget::SelectedCompany, + territory: RuntimeTerritoryTarget::Ids { ids: vec![8] }, + value: false, + }], + }, + ], + selected_company_id: Some(1), + ..state() + }; + + let first = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("territory access grant should succeed"); + + assert_eq!( + state.company_territory_access, + vec![ + crate::state::RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 7, + }, + crate::state::RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 8, + }, + ] + ); + assert_eq!(first.service_events[0].mutated_company_ids, vec![1]); + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 8 }, + ) + .expect("territory access clear should succeed"); + + assert_eq!( + state.company_territory_access, + vec![crate::state::RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 7, + }] + ); +} + +#[test] +fn rejects_condition_true_company_target_without_condition_context() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 16, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + delta: 1, + }], + }], + ..state() + }; + + let error = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect_err("condition-relative target should remain blocked"); + + assert!(error.contains("condition-evaluation context")); +} + +#[test] +fn evaluates_world_state_conditions_before_effects_run() { + let mut state = RuntimeState { + world_restore: RuntimeWorldRestoreState { + economic_status_code: Some(3), + ..RuntimeWorldRestoreState::default() + }, + world_flags: BTreeMap::from([( + String::from("world.disable_stock_buying_and_selling"), + true, + )]), + candidate_availability: BTreeMap::from([(String::from("Mogul"), 2)]), + special_conditions: BTreeMap::from([(String::from("Use Wartime Cargos"), 1)]), + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 23, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![ + RuntimeCondition::SpecialConditionThreshold { + label: "Use Wartime Cargos".to_string(), + comparator: RuntimeConditionComparator::Ge, + value: 1, + }, + RuntimeCondition::CandidateAvailabilityThreshold { + name: "Mogul".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 2, + }, + RuntimeCondition::EconomicStatusCodeThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 3, + }, + RuntimeCondition::WorldFlagEquals { + key: "world.disable_stock_buying_and_selling".to_string(), + value: true, + }, + ], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world_condition_passed".to_string(), + value: true, + }], + }, + RuntimeEventRecord { + record_id: 24, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::SpecialConditionThreshold { + label: "Disable Cargo Economy".to_string(), + comparator: RuntimeConditionComparator::Gt, + value: 0, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world_condition_failed".to_string(), + value: true, + }], + }, + ], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("world-state conditions should evaluate successfully"); + + assert_eq!(result.service_events[0].serviced_record_ids, vec![23]); + assert_eq!(state.world_flags.get("world_condition_passed"), Some(&true)); + assert_eq!(state.world_flags.get("world_condition_failed"), None); +} + +#[test] +fn evaluates_world_scalar_conditions_before_effects_run() { + let mut state = RuntimeState { + world_restore: RuntimeWorldRestoreState { + limited_track_building_amount: Some(18), + territory_access_cost: Some(750000), + ..RuntimeWorldRestoreState::default() + }, + cargo_catalog: vec![ + crate::state::RuntimeCargoCatalogEntry { + slot_id: 1, + label: "Cargo Production Slot 1".to_string(), + cargo_class: RuntimeCargoClass::Factory, + supplied_token_stem: None, + demanded_token_stem: None, + }, + crate::state::RuntimeCargoCatalogEntry { + slot_id: 5, + label: "Cargo Production Slot 5".to_string(), + cargo_class: RuntimeCargoClass::FarmMine, + supplied_token_stem: None, + demanded_token_stem: None, + }, + crate::state::RuntimeCargoCatalogEntry { + slot_id: 9, + label: "Cargo Production Slot 9".to_string(), + cargo_class: RuntimeCargoClass::Other, + supplied_token_stem: None, + demanded_token_stem: None, + }, + ], + named_locomotive_availability: BTreeMap::from([(String::from("Big Boy"), 42)]), + named_locomotive_cost: BTreeMap::from([(String::from("GP7"), 175000)]), + cargo_production_overrides: BTreeMap::from([(1, 125), (5, 75), (9, 30)]), + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 25, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![ + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Big Boy".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 42, + }, + RuntimeCondition::NamedLocomotiveCostThreshold { + name: "GP7".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 175000, + }, + RuntimeCondition::CargoProductionSlotThreshold { + slot: 1, + label: "Cargo Production Slot 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, + RuntimeCondition::CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 230, + }, + RuntimeCondition::FactoryProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, + RuntimeCondition::FarmMineProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 75, + }, + RuntimeCondition::OtherCargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 30, + }, + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 18, + }, + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 750000, + }, + ], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world_scalar_condition_passed".to_string(), + value: true, + }], + }, + RuntimeEventRecord { + record_id: 26, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Missing Loco".to_string(), + comparator: RuntimeConditionComparator::Gt, + value: 0, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world_scalar_condition_failed".to_string(), + value: true, + }], + }, + ], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("world-scalar conditions should evaluate successfully"); + + assert_eq!(result.service_events[0].serviced_record_ids, vec![25]); + assert_eq!( + state.world_flags.get("world_scalar_condition_passed"), + Some(&true) + ); + assert_eq!(state.world_flags.get("world_scalar_condition_failed"), None); +} + +#[test] +fn company_governance_metric_conditions_gate_execution() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 2620, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1844), + merger_cooldown_year: Some(1845), + }], + selected_company_id: Some(1), + event_runtime_records: vec![RuntimeEventRecord { + record_id: 95, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.book_value_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("book-value company condition should gate execution"); + + assert_eq!( + state.world_flags.get("world.book_value_gate_passed"), + Some(&true) + ); +} + +#[test] +fn derived_prime_rate_condition_reads_rehosted_issue_owner_state() { + let mut issue_terms = vec![0; 0x3b]; + issue_terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100; + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 2620, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1844), + merger_cooldown_year: Some(1845), + }], + selected_company_id: Some(1), + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + ..RuntimeWorldRestoreState::default() + }, + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: issue_terms, + ..RuntimeServiceState::default() + }, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 195, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: RuntimeCompanyMetric::PrimeRate, + comparator: RuntimeConditionComparator::Eq, + value: 6, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.derived_prime_rate_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("derived prime-rate company condition should gate execution"); + + assert_eq!( + state + .world_flags + .get("world.derived_prime_rate_gate_passed"), + Some(&true) + ); +} + +#[test] +fn derived_investor_confidence_condition_reads_rehosted_share_price_cache() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 2620, + investor_confidence: 0, + management_attitude: 58, + takeover_cooldown_year: Some(1844), + merger_cooldown_year: Some(1845), + }], + selected_company_id: Some(1), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + recent_per_share_subscore_raw_u32: 12.0f32.to_bits(), + cached_share_price_raw_u32: 37.0f32.to_bits(), + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 196, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: RuntimeCompanyMetric::InvestorConfidence, + comparator: RuntimeConditionComparator::Eq, + value: 37, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.derived_investor_confidence_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("derived investor-confidence company condition should gate execution"); + + assert_eq!( + state + .world_flags + .get("world.derived_investor_confidence_gate_passed"), + Some(&true) + ); +} + +#[test] +fn derived_management_attitude_condition_reads_issue3a_owner_state() { + let mut issue_terms = vec![0; 0x3b]; + issue_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 40; + let mut company_terms = vec![0; 0x3b]; + company_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 12; + let mut chairman_terms = vec![0; 0x3b]; + chairman_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 6; + + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(3), + book_value_per_share: 2620, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: Some(1844), + merger_cooldown_year: Some(1845), + }], + selected_company_id: Some(1), + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 3, + name: "Chairman".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: issue_terms, + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + issue_opinion_terms_raw_i32: company_terms, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(3, chairman_terms)]), + ..RuntimeServiceState::default() + }, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 196, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: RuntimeCompanyMetric::ManagementAttitude, + comparator: RuntimeConditionComparator::Eq, + value: 58, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.derived_management_attitude_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("derived management-attitude company condition should gate execution"); + + assert_eq!( + state + .world_flags + .get("world.derived_management_attitude_gate_passed"), + Some(&true) + ); +} + +#[test] +fn book_value_condition_reads_rehosted_direct_company_field_band() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1844), + merger_cooldown_year: Some(1845), + }], + selected_company_id: Some(1), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x32f, + 2620.0f32.to_bits(), + )]), + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 197, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.rehosted_book_value_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("rehosted direct-field book-value condition should gate execution"); + + assert_eq!( + state + .world_flags + .get("world.rehosted_book_value_gate_passed"), + Some(&true) + ); +} + +#[test] +fn derived_credit_rating_condition_reads_rehosted_finance_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = + 20.0f64.to_bits(); + year_stat_family_qword_bits[(0x01 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = + 100.0f64.to_bits(); + year_stat_family_qword_bits[(0x09 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = + 0.0f64.to_bits(); + + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1844), + merger_cooldown_year: Some(1845), + }], + selected_company_id: Some(1), + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + issue_38_value: Some(2), + packed_year_word_raw_u16: Some(1835), + ..RuntimeWorldRestoreState::default() + }, + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1830, + last_bankruptcy_year: 1800, + year_stat_family_qword_bits, + live_bond_slots: vec![crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 100_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.05f32.to_bits(), + }], + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 196, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: RuntimeCompanyMetric::CreditRating, + comparator: RuntimeConditionComparator::Eq, + value: 10, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.derived_credit_rating_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("derived credit-rating company condition should gate execution"); + + assert_eq!( + state + .world_flags + .get("world.derived_credit_rating_gate_passed"), + Some(&true) + ); +} + +#[test] +fn chairman_metric_conditions_support_all_active_target() { + let mut state = RuntimeState { + chairman_profiles: vec![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 20, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 500, + net_worth_total: 700, + purchasing_power_total: 900, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 30, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 600, + net_worth_total: 800, + purchasing_power_total: 1000, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 96, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::ChairmanNumericThreshold { + target: RuntimeChairmanTarget::AllActive, + metric: RuntimeChairmanMetric::PurchasingPowerTotal, + comparator: RuntimeConditionComparator::Ge, + value: 900, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.chairman_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("all-active chairman condition should gate execution"); + + assert_eq!( + state.world_flags.get("world.chairman_gate_passed"), + Some(&true) + ); +} diff --git a/crates/rrt-runtime/src/event/conditions.rs b/crates/rrt-runtime/src/event/conditions.rs new file mode 100644 index 0000000..2338e18 --- /dev/null +++ b/crates/rrt-runtime/src/event/conditions.rs @@ -0,0 +1,130 @@ +use serde::{Deserialize, Serialize}; + +use crate::event::metrics::{ + RuntimeChairmanMetric, RuntimeCompanyMetric, RuntimeTerritoryMetric, RuntimeTrackMetric, +}; +use crate::event::targets::{ + RuntimeChairmanTarget, RuntimeCompanyTarget, RuntimePlayerTarget, RuntimeTerritoryTarget, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeConditionComparator { + Ge, + Le, + Gt, + Lt, + Eq, + Ne, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum RuntimeCondition { + WorldVariableThreshold { + index: u32, + comparator: RuntimeConditionComparator, + value: i64, + }, + CompanyNumericThreshold { + target: RuntimeCompanyTarget, + metric: RuntimeCompanyMetric, + comparator: RuntimeConditionComparator, + value: i64, + }, + CompanyVariableThreshold { + target: RuntimeCompanyTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, + }, + ChairmanNumericThreshold { + target: RuntimeChairmanTarget, + metric: RuntimeChairmanMetric, + comparator: RuntimeConditionComparator, + value: i64, + }, + PlayerVariableThreshold { + target: RuntimePlayerTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, + }, + TerritoryNumericThreshold { + target: RuntimeTerritoryTarget, + metric: RuntimeTerritoryMetric, + comparator: RuntimeConditionComparator, + value: i64, + }, + TerritoryVariableThreshold { + target: RuntimeTerritoryTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, + }, + CompanyTerritoryNumericThreshold { + target: RuntimeCompanyTarget, + territory: RuntimeTerritoryTarget, + metric: RuntimeTrackMetric, + comparator: RuntimeConditionComparator, + value: i64, + }, + SpecialConditionThreshold { + label: String, + comparator: RuntimeConditionComparator, + value: i64, + }, + CandidateAvailabilityThreshold { + name: String, + comparator: RuntimeConditionComparator, + value: i64, + }, + NamedLocomotiveAvailabilityThreshold { + name: String, + comparator: RuntimeConditionComparator, + value: i64, + }, + NamedLocomotiveCostThreshold { + name: String, + comparator: RuntimeConditionComparator, + value: i64, + }, + CargoProductionSlotThreshold { + slot: u32, + label: String, + comparator: RuntimeConditionComparator, + value: i64, + }, + CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + FactoryProductionTotalThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + FarmMineProductionTotalThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + OtherCargoProductionTotalThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + EconomicStatusCodeThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + WorldFlagEquals { + key: String, + value: bool, + }, +} diff --git a/crates/rrt-runtime/src/event/effects.rs b/crates/rrt-runtime/src/event/effects.rs new file mode 100644 index 0000000..629a67b --- /dev/null +++ b/crates/rrt-runtime/src/event/effects.rs @@ -0,0 +1,147 @@ +use serde::{Deserialize, Serialize}; + +use crate::event::metrics::RuntimeCompanyMetric; +use crate::event::records::RuntimeEventRecordTemplate; +use crate::event::targets::{ + RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanTarget, + RuntimeCompanyTarget, RuntimePlayerTarget, RuntimeTerritoryTarget, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum RuntimeEffect { + SetWorldFlag { + key: String, + value: bool, + }, + SetLimitedTrackBuildingAmount { + value: i32, + }, + SetEconomicStatusCode { + value: i32, + }, + SetCompanyCash { + target: RuntimeCompanyTarget, + value: i64, + }, + SetPlayerCash { + target: RuntimePlayerTarget, + value: i64, + }, + SetChairmanCash { + target: RuntimeChairmanTarget, + value: i64, + }, + SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget, + metric: RuntimeCompanyMetric, + value: i64, + }, + DeactivatePlayer { + target: RuntimePlayerTarget, + }, + DeactivateChairman { + target: RuntimeChairmanTarget, + }, + SetCompanyTerritoryAccess { + target: RuntimeCompanyTarget, + territory: RuntimeTerritoryTarget, + value: bool, + }, + ConfiscateCompanyAssets { + target: RuntimeCompanyTarget, + }, + DeactivateCompany { + target: RuntimeCompanyTarget, + }, + SetCompanyTrackLayingCapacity { + target: RuntimeCompanyTarget, + value: Option, + }, + RetireTrains { + #[serde(default)] + company_target: Option, + #[serde(default)] + territory_target: Option, + #[serde(default)] + locomotive_name: Option, + }, + AdjustCompanyCash { + target: RuntimeCompanyTarget, + delta: i64, + }, + AdjustCompanyDebt { + target: RuntimeCompanyTarget, + delta: i64, + }, + SetCandidateAvailability { + name: String, + value: u32, + }, + SetNamedLocomotiveAvailability { + name: String, + value: bool, + }, + SetNamedLocomotiveAvailabilityValue { + name: String, + value: u32, + }, + SetNamedLocomotiveCost { + name: String, + value: u32, + }, + SetCargoPriceOverride { + target: RuntimeCargoPriceTarget, + value: u32, + }, + SetCargoProductionOverride { + target: RuntimeCargoProductionTarget, + value: u32, + }, + SetCargoProductionSlot { + slot: u32, + value: u32, + }, + SetWorldVariable { + index: u32, + value: i64, + }, + SetCompanyVariable { + target: RuntimeCompanyTarget, + index: u32, + value: i64, + }, + SetPlayerVariable { + target: RuntimePlayerTarget, + index: u32, + value: i64, + }, + SetTerritoryVariable { + target: RuntimeTerritoryTarget, + index: u32, + value: i64, + }, + SetWorldScalarOverride { + key: String, + value: i64, + }, + SetTerritoryAccessCost { + value: u32, + }, + SetSpecialCondition { + label: String, + value: u32, + }, + AppendEventRecord { + record: Box, + }, + ActivateEventRecord { + record_id: u32, + }, + DeactivateEventRecord { + record_id: u32, + }, + RemoveEventRecord { + record_id: u32, + }, +} diff --git a/crates/rrt-runtime/src/event/metrics.rs b/crates/rrt-runtime/src/event/metrics.rs new file mode 100644 index 0000000..e74b0f1 --- /dev/null +++ b/crates/rrt-runtime/src/event/metrics.rs @@ -0,0 +1,133 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeCompanyConditionTestScope { + #[default] + Disabled, + AllCompanies, + SelectedCompanyOnly, + AiCompaniesOnly, + HumanCompaniesOnly, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum RuntimePlayerConditionTestScope { + #[default] + Disabled, + AllPlayers, + SelectedPlayerOnly, + AiPlayersOnly, + HumanPlayersOnly, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeCompanyMetric { + CurrentCash, + TotalDebt, + CreditRating, + PrimeRate, + BookValuePerShare, + InvestorConfidence, + ManagementAttitude, + TrackPiecesTotal, + TrackPiecesSingle, + TrackPiecesDouble, + TrackPiecesTransition, + TrackPiecesElectric, + TrackPiecesNonElectric, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeCompanyMarketMetric { + OutstandingShares, + BondCount, + LargestLiveBondPrincipal, + HighestCouponLiveBondPrincipal, + AssignedSharePool, + UnassignedSharePool, + CachedSharePrice, + ChairmanSalaryBaseline, + ChairmanSalaryCurrent, + ChairmanBonusAmount, + CurrentIssueAbsoluteCounter, + PriorIssueAbsoluteCounter, + CurrentIssueAgeAbsoluteCounterDelta, + CurrentIssueCalendarWord, + CurrentIssueCalendarWord2, + PriorIssueCalendarWord, + PriorIssueCalendarWord2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeChairmanMetric { + CurrentCash, + HoldingsValueTotal, + NetWorthTotal, + PurchasingPowerTotal, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyStatSelector { + pub family_id: u32, + pub slot_id: u32, +} + +pub const RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER: u32 = 0x2329; +pub const RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A: u32 = 0x232a; +pub const RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH: u32 = 0x0d; +pub const RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE: u32 = 0x1d; +pub const RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING: u32 = 0x19; +pub const RUNTIME_COMPANY_STAT_SLOT_COUNT: u32 = 0x2b; +pub const RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN: u32 = 11; +pub const RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE: u32 = 0x37; +pub const RUNTIME_WORLD_ISSUE_CREDIT_MARKET: u32 = 0x38; +pub const RUNTIME_WORLD_ISSUE_PRIME_RATE: u32 = 0x39; +pub const RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE: u32 = 0x3a; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeWorldIssueState { + pub issue_id: u32, + pub raw_value_u32: u32, + #[serde(default)] + pub multiplier_raw_u32: Option, + #[serde(default)] + pub multiplier_value_f32_text: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedCalendarTuple { + pub year_word: u16, + pub month_1_based: u8, + pub week_1_based: u8, + pub day_1_based: u8, + pub hour_0_based: u8, + pub quarter_day_1_based: u8, + pub minute_0_based: u8, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeTerritoryMetric { + TrackPiecesTotal, + TrackPiecesSingle, + TrackPiecesDouble, + TrackPiecesTransition, + TrackPiecesElectric, + TrackPiecesNonElectric, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeTrackMetric { + Total, + Single, + Double, + Transition, + Electric, + NonElectric, +} diff --git a/crates/rrt-runtime/src/event/mod.rs b/crates/rrt-runtime/src/event/mod.rs new file mode 100644 index 0000000..54c8d25 --- /dev/null +++ b/crates/rrt-runtime/src/event/mod.rs @@ -0,0 +1,7 @@ +pub mod conditions; +pub mod effects; +pub mod metrics; +pub mod news; +pub mod packed; +pub mod records; +pub mod targets; diff --git a/crates/rrt-runtime/src/event/news.rs b/crates/rrt-runtime/src/event/news.rs new file mode 100644 index 0000000..f544293 --- /dev/null +++ b/crates/rrt-runtime/src/event/news.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +use crate::state::RuntimeNearCityAcquisitionValueProvenance; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct RuntimeAnnualFinanceNewsEvent { + pub company_id: u32, + pub selector_label: String, + pub action_label: String, + pub retired_principal_total: u64, + pub issued_principal_total: u64, + pub repurchased_share_count: u64, + pub issued_share_count: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct RuntimeNearCityAcquisitionNewsEvent { + pub company_id: u32, + pub site_id: u32, + pub site_primary_name: String, + pub site_secondary_name: String, + pub selector_label: String, + pub outcome_label: String, + pub subtype_label: String, + #[serde(default)] + pub owner_company_id_provenance: RuntimeNearCityAcquisitionValueProvenance, + #[serde(default)] + pub cached_tri_lane_provenance: RuntimeNearCityAcquisitionValueProvenance, +} diff --git a/crates/rrt-runtime/src/event/packed.rs b/crates/rrt-runtime/src/event/packed.rs new file mode 100644 index 0000000..e59f690 --- /dev/null +++ b/crates/rrt-runtime/src/event/packed.rs @@ -0,0 +1,186 @@ +use serde::{Deserialize, Serialize}; + +use crate::event::conditions::RuntimeCondition; +use crate::event::effects::RuntimeEffect; +use crate::event::metrics::{RuntimeCompanyConditionTestScope, RuntimePlayerConditionTestScope}; +use crate::event::targets::RuntimeCompanyTarget; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventCollectionSummary { + pub source_kind: String, + pub mechanism_family: String, + pub mechanism_confidence: String, + #[serde(default)] + pub container_profile_family: Option, + pub packed_state_version: u32, + pub packed_state_version_hex: String, + pub live_id_bound: u32, + pub live_record_count: usize, + pub live_entry_ids: Vec, + #[serde(default)] + pub decoded_record_count: usize, + #[serde(default)] + pub imported_runtime_record_count: usize, + #[serde(default)] + pub records: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventRecordSummary { + pub record_index: usize, + pub live_entry_id: u32, + #[serde(default)] + pub payload_offset: Option, + #[serde(default)] + pub payload_len: Option, + pub decode_status: String, + #[serde(default)] + pub payload_family: String, + #[serde(default)] + pub trigger_kind: Option, + #[serde(default)] + pub active: Option, + #[serde(default)] + pub marks_collection_dirty: Option, + #[serde(default)] + pub one_shot: Option, + #[serde(default)] + pub compact_control: Option, + #[serde(default)] + pub text_bands: Vec, + #[serde(default)] + pub standalone_condition_row_count: usize, + #[serde(default)] + pub standalone_condition_rows: Vec, + #[serde(default)] + pub negative_sentinel_scope: Option, + #[serde(default)] + pub grouped_effect_row_counts: Vec, + #[serde(default)] + pub grouped_effect_rows: Vec, + #[serde(default)] + pub grouped_company_targets: Vec>, + #[serde(default)] + pub decoded_conditions: Vec, + #[serde(default)] + pub decoded_actions: Vec, + #[serde(default)] + pub executable_import_ready: bool, + #[serde(default)] + pub import_outcome: Option, + #[serde(default)] + pub notes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventNegativeSentinelScopeSummary { + pub company_test_scope: RuntimeCompanyConditionTestScope, + pub player_test_scope: RuntimePlayerConditionTestScope, + pub territory_scope_selector_is_0x63: bool, + #[serde(default)] + pub source_row_indexes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventCompactControlSummary { + pub mode_byte_0x7ef: u8, + pub primary_selector_0x7f0: u32, + pub grouped_mode_0x7f4: u8, + pub one_shot_header_0x7f5: u32, + pub modifier_flag_0x7f9: u8, + pub modifier_flag_0x7fa: u8, + #[serde(default)] + pub grouped_target_scope_ordinals_0x7fb: Vec, + #[serde(default)] + pub grouped_scope_checkboxes_0x7ff: Vec, + pub summary_toggle_0x800: u8, + #[serde(default)] + pub grouped_territory_selectors_0x80f: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventTextBandSummary { + pub label: String, + pub packed_len: usize, + pub present: bool, + pub preview: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventConditionRowSummary { + pub row_index: usize, + pub raw_condition_id: i32, + pub subtype: u8, + #[serde(default)] + pub flag_bytes: Vec, + #[serde(default)] + pub candidate_name: Option, + #[serde(default)] + pub comparator: Option, + #[serde(default)] + pub metric: Option, + #[serde(default)] + pub semantic_family: Option, + #[serde(default)] + pub semantic_preview: Option, + #[serde(default)] + pub requires_candidate_name_binding: bool, + #[serde(default)] + pub recovered_cargo_slot: Option, + #[serde(default)] + pub recovered_cargo_class: Option, + #[serde(default)] + pub recovered_cargo_label: Option, + #[serde(default)] + pub recovered_cargo_supplied_token_stem: Option, + #[serde(default)] + pub recovered_cargo_demanded_token_stem: Option, + #[serde(default)] + pub notes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventGroupedEffectRowSummary { + pub group_index: usize, + pub row_index: usize, + pub descriptor_id: u32, + #[serde(default)] + pub descriptor_label: Option, + #[serde(default)] + pub target_mask_bits: Option, + #[serde(default)] + pub parameter_family: Option, + #[serde(default)] + pub grouped_target_subject: Option, + #[serde(default)] + pub grouped_target_scope: Option, + pub opcode: u8, + pub raw_scalar_value: i32, + pub value_byte_0x09: u8, + pub value_dword_0x0d: u32, + pub value_byte_0x11: u8, + pub value_byte_0x12: u8, + pub value_word_0x14: u16, + pub value_word_0x16: u16, + pub row_shape: String, + #[serde(default)] + pub semantic_family: Option, + #[serde(default)] + pub semantic_preview: Option, + #[serde(default)] + pub recovered_cargo_slot: Option, + #[serde(default)] + pub recovered_cargo_class: Option, + #[serde(default)] + pub recovered_cargo_label: Option, + #[serde(default)] + pub recovered_cargo_supplied_token_stem: Option, + #[serde(default)] + pub recovered_cargo_demanded_token_stem: Option, + #[serde(default)] + pub recovered_locomotive_id: Option, + #[serde(default)] + pub locomotive_name: Option, + #[serde(default)] + pub notes: Vec, +} diff --git a/crates/rrt-runtime/src/event/records.rs b/crates/rrt-runtime/src/event/records.rs new file mode 100644 index 0000000..5ea7bc3 --- /dev/null +++ b/crates/rrt-runtime/src/event/records.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; + +use crate::event::conditions::RuntimeCondition; +use crate::event::effects::RuntimeEffect; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeEventRecordTemplate { + pub record_id: u32, + pub trigger_kind: u8, + pub active: bool, + #[serde(default)] + pub marks_collection_dirty: bool, + #[serde(default)] + pub one_shot: bool, + #[serde(default)] + pub conditions: Vec, + #[serde(default)] + pub effects: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeEventRecord { + pub record_id: u32, + pub trigger_kind: u8, + pub active: bool, + #[serde(default)] + pub service_count: u32, + #[serde(default)] + pub marks_collection_dirty: bool, + #[serde(default)] + pub one_shot: bool, + #[serde(default)] + pub has_fired: bool, + #[serde(default)] + pub conditions: Vec, + #[serde(default)] + pub effects: Vec, +} + +impl RuntimeEventRecordTemplate { + pub fn into_runtime_record(self) -> RuntimeEventRecord { + RuntimeEventRecord { + record_id: self.record_id, + trigger_kind: self.trigger_kind, + active: self.active, + service_count: 0, + marks_collection_dirty: self.marks_collection_dirty, + one_shot: self.one_shot, + has_fired: false, + conditions: self.conditions, + effects: self.effects, + } + } +} diff --git a/crates/rrt-runtime/src/event/targets.rs b/crates/rrt-runtime/src/event/targets.rs new file mode 100644 index 0000000..0784623 --- /dev/null +++ b/crates/rrt-runtime/src/event/targets.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeCargoClass { + #[default] + Other, + Factory, + FarmMine, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum RuntimeCompanyTarget { + AllActive, + HumanCompanies, + AiCompanies, + SelectedCompany, + ConditionTrueCompany, + Ids { ids: Vec }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum RuntimePlayerTarget { + AllActive, + HumanPlayers, + AiPlayers, + SelectedPlayer, + ConditionTruePlayer, + Ids { ids: Vec }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum RuntimeChairmanTarget { + AllActive, + HumanChairmen, + AiChairmen, + SelectedChairman, + ConditionTrueChairman, + Ids { ids: Vec }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum RuntimeCargoPriceTarget { + All, + Named { name: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum RuntimeCargoProductionTarget { + All, + Factory, + FarmMine, + Named { name: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum RuntimeTerritoryTarget { + AllTerritories, + Ids { ids: Vec }, +} diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs deleted file mode 100644 index 7fab01d..0000000 --- a/crates/rrt-runtime/src/import.rs +++ /dev/null @@ -1,16210 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::path::{Path, PathBuf}; - -use serde::{Deserialize, Serialize}; - -use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; -use crate::{ - CalendarPoint, RuntimeCargoCatalogEntry, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, - RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, - RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketState, - RuntimeCompanyTarget, RuntimeCondition, RuntimeEffect, RuntimeEventRecord, - RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, - RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, - RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, - RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, - RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState, - RuntimeServiceState, RuntimeState, RuntimeTerritoryTarget, - RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldRestoreState, - SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary, - SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary, - SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice, -}; - -pub const STATE_DUMP_FORMAT_VERSION: u32 = 1; -pub const SAVE_SLICE_DOCUMENT_FORMAT_VERSION: u32 = 1; -pub const OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION: u32 = 1; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct RuntimeStateDumpSource { - #[serde(default)] - pub description: Option, - #[serde(default)] - pub source_binary: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeStateDumpDocument { - pub format_version: u32, - pub dump_id: String, - #[serde(default)] - pub source: RuntimeStateDumpSource, - pub state: RuntimeState, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct RuntimeSaveSliceDocumentSource { - #[serde(default)] - pub description: Option, - #[serde(default)] - pub original_save_filename: Option, - #[serde(default)] - pub original_save_sha256: Option, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RuntimeSaveSliceDocument { - pub format_version: u32, - pub save_slice_id: String, - #[serde(default)] - pub source: RuntimeSaveSliceDocumentSource, - pub save_slice: SmpLoadedSaveSlice, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct RuntimeOverlayImportDocumentSource { - #[serde(default)] - pub description: Option, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeOverlayImportDocument { - pub format_version: u32, - pub import_id: String, - #[serde(default)] - pub source: RuntimeOverlayImportDocumentSource, - pub base_snapshot_path: String, - pub save_slice_path: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RuntimeStateImport { - pub import_id: String, - pub description: Option, - pub state: RuntimeState, -} - -#[derive(Debug)] -struct SaveSliceProjection { - world_flags: BTreeMap, - save_profile: RuntimeSaveProfileState, - world_restore: RuntimeWorldRestoreState, - metadata: BTreeMap, - packed_event_collection: Option, - event_runtime_records: Vec, - companies: Vec, - has_company_projection: bool, - has_company_selection_override: bool, - selected_company_id: Option, - company_market_state: BTreeMap, - company_periodic_side_latch_state: BTreeMap, - has_company_market_projection: bool, - world_issue_opinion_base_terms_raw_i32: Vec, - chairman_profiles: Vec, - has_chairman_projection: bool, - has_chairman_selection_override: bool, - selected_chairman_profile_id: Option, - chairman_issue_opinion_terms_raw_i32: BTreeMap>, - chairman_personality_raw_u8: BTreeMap, - candidate_availability: BTreeMap, - named_locomotive_availability: BTreeMap, - locomotive_catalog: Option>, - cargo_catalog: Option>, - named_locomotive_cost: BTreeMap, - all_cargo_price_override: Option, - named_cargo_price_overrides: BTreeMap, - all_cargo_production_override: Option, - factory_cargo_production_override: Option, - farm_mine_cargo_production_override: Option, - named_cargo_production_overrides: BTreeMap, - cargo_production_overrides: BTreeMap, - world_scalar_overrides: BTreeMap, - special_conditions: BTreeMap, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum SaveSliceProjectionMode { - Standalone, - Overlay, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ImportRuntimeContext { - known_company_ids: BTreeSet, - selected_company_id: Option, - has_complete_company_controller_context: bool, - known_player_ids: BTreeSet, - selected_player_id: Option, - has_complete_player_controller_context: bool, - known_chairman_profile_ids: BTreeSet, - selected_chairman_profile_id: Option, - known_territory_ids: BTreeSet, - has_territory_context: bool, - territory_name_to_id: BTreeMap, - has_train_context: bool, - has_train_territory_context: bool, - locomotive_catalog_names_by_id: BTreeMap, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ImportBlocker { - MissingCompanyContext, - MissingSelectionContext, - MissingCompanyRoleContext, - MissingPlayerContext, - MissingPlayerSelectionContext, - MissingPlayerRoleContext, - MissingChairmanContext, - ChairmanTargetScope, - MissingConditionContext, - MissingPlayerConditionContext, - CompanyConditionScopeDisabled, - MissingTerritoryContext, - NamedTerritoryBinding, - UnmappedOrdinaryCondition, - UnmappedWorldCondition, - EvidenceBlockedDescriptor, - MissingTrainContext, - MissingTrainTerritoryContext, - MissingLocomotiveCatalogContext, -} - -impl ImportRuntimeContext { - fn standalone() -> Self { - Self { - known_company_ids: BTreeSet::new(), - selected_company_id: None, - has_complete_company_controller_context: false, - known_player_ids: BTreeSet::new(), - selected_player_id: None, - has_complete_player_controller_context: false, - known_chairman_profile_ids: BTreeSet::new(), - selected_chairman_profile_id: None, - known_territory_ids: BTreeSet::new(), - has_territory_context: false, - territory_name_to_id: BTreeMap::new(), - has_train_context: false, - has_train_territory_context: false, - locomotive_catalog_names_by_id: BTreeMap::new(), - } - } - - fn from_runtime_state(state: &RuntimeState) -> Self { - Self { - known_company_ids: state - .companies - .iter() - .map(|company| company.company_id) - .collect(), - selected_company_id: state.selected_company_id, - has_complete_company_controller_context: !state.companies.is_empty() - && state.companies.iter().all(|company| { - company.controller_kind != RuntimeCompanyControllerKind::Unknown - }), - known_player_ids: state - .players - .iter() - .map(|player| player.player_id) - .collect(), - selected_player_id: state.selected_player_id, - has_complete_player_controller_context: !state.players.is_empty() - && state - .players - .iter() - .all(|player| player.controller_kind != RuntimeCompanyControllerKind::Unknown), - known_chairman_profile_ids: state - .chairman_profiles - .iter() - .map(|profile| profile.profile_id) - .collect(), - selected_chairman_profile_id: state.selected_chairman_profile_id, - known_territory_ids: state - .territories - .iter() - .map(|territory| territory.territory_id) - .collect(), - has_territory_context: !state.territories.is_empty(), - territory_name_to_id: state - .territories - .iter() - .filter_map(|territory| { - territory - .name - .as_ref() - .map(|name| (name.clone(), territory.territory_id)) - }) - .collect(), - has_train_context: !state.trains.is_empty(), - has_train_territory_context: state - .trains - .iter() - .any(|train| train.territory_id.is_some()), - locomotive_catalog_names_by_id: state - .locomotive_catalog - .iter() - .map(|entry| (entry.locomotive_id, entry.name.clone())) - .collect(), - } - } -} - -pub fn project_save_slice_to_runtime_state_import( - save_slice: &SmpLoadedSaveSlice, - import_id: &str, - description: Option, -) -> Result { - if import_id.trim().is_empty() { - return Err("import_id must not be empty".to_string()); - } - let projection = project_save_slice_components( - save_slice, - &ImportRuntimeContext::standalone(), - SaveSliceProjectionMode::Standalone, - )?; - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: projection.world_flags, - save_profile: projection.save_profile, - world_restore: projection.world_restore, - metadata: projection.metadata, - companies: projection.companies, - selected_company_id: if projection.has_company_projection { - projection.selected_company_id - } else { - None - }, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: projection.chairman_profiles, - selected_chairman_profile_id: if projection.has_chairman_projection { - projection.selected_chairman_profile_id - } else { - None - }, - trains: Vec::new(), - locomotive_catalog: projection.locomotive_catalog.unwrap_or_default(), - cargo_catalog: projection.cargo_catalog.unwrap_or_default(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: projection.packed_event_collection, - event_runtime_records: projection.event_runtime_records, - candidate_availability: projection.candidate_availability, - named_locomotive_availability: projection.named_locomotive_availability, - named_locomotive_cost: projection.named_locomotive_cost, - all_cargo_price_override: projection.all_cargo_price_override, - named_cargo_price_overrides: projection.named_cargo_price_overrides, - all_cargo_production_override: projection.all_cargo_production_override, - factory_cargo_production_override: projection.factory_cargo_production_override, - farm_mine_cargo_production_override: projection.farm_mine_cargo_production_override, - named_cargo_production_overrides: projection.named_cargo_production_overrides, - cargo_production_overrides: projection.cargo_production_overrides, - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: projection.world_scalar_overrides, - special_conditions: projection.special_conditions, - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: projection - .world_issue_opinion_base_terms_raw_i32, - company_market_state: projection.company_market_state, - company_periodic_side_latch_state: projection.company_periodic_side_latch_state, - chairman_issue_opinion_terms_raw_i32: projection.chairman_issue_opinion_terms_raw_i32, - chairman_personality_raw_u8: projection.chairman_personality_raw_u8, - ..RuntimeServiceState::default() - }, - }; - let mut state = state; - state.refresh_derived_world_state(); - state.refresh_derived_market_state(); - state.validate()?; - - Ok(RuntimeStateImport { - import_id: import_id.to_string(), - description, - state, - }) -} - -pub fn project_save_slice_overlay_to_runtime_state_import( - base_state: &RuntimeState, - save_slice: &SmpLoadedSaveSlice, - import_id: &str, - description: Option, -) -> Result { - if import_id.trim().is_empty() { - return Err("import_id must not be empty".to_string()); - } - base_state.validate()?; - - let company_context = ImportRuntimeContext::from_runtime_state(base_state); - let projection = project_save_slice_components( - save_slice, - &company_context, - SaveSliceProjectionMode::Overlay, - )?; - - let mut world_flags = base_state.world_flags.clone(); - world_flags.retain(|key, _| !key.starts_with("save_slice.")); - world_flags.extend(projection.world_flags); - - let mut metadata = base_state.metadata.clone(); - metadata.retain(|key, _| !key.starts_with("save_slice.")); - metadata.extend(projection.metadata); - - let state = RuntimeState { - calendar: base_state.calendar, - world_flags, - save_profile: projection.save_profile, - world_restore: RuntimeWorldRestoreState { - territory_access_cost: base_state.world_restore.territory_access_cost, - ..projection.world_restore - }, - metadata, - companies: if projection.has_company_projection { - projection.companies - } else { - base_state.companies.clone() - }, - selected_company_id: if projection.has_company_projection - || projection.has_company_selection_override - { - projection.selected_company_id - } else { - base_state.selected_company_id - }, - players: base_state.players.clone(), - selected_player_id: base_state.selected_player_id, - chairman_profiles: if projection.has_chairman_projection { - projection.chairman_profiles - } else { - base_state.chairman_profiles.clone() - }, - selected_chairman_profile_id: if projection.has_chairman_projection - || projection.has_chairman_selection_override - { - projection.selected_chairman_profile_id - } else { - base_state.selected_chairman_profile_id - }, - trains: base_state.trains.clone(), - locomotive_catalog: projection - .locomotive_catalog - .unwrap_or_else(|| base_state.locomotive_catalog.clone()), - cargo_catalog: projection - .cargo_catalog - .unwrap_or_else(|| base_state.cargo_catalog.clone()), - territories: base_state.territories.clone(), - company_territory_track_piece_counts: base_state - .company_territory_track_piece_counts - .clone(), - company_territory_access: base_state.company_territory_access.clone(), - packed_event_collection: projection.packed_event_collection, - event_runtime_records: projection.event_runtime_records, - candidate_availability: projection.candidate_availability, - named_locomotive_availability: projection.named_locomotive_availability, - named_locomotive_cost: base_state.named_locomotive_cost.clone(), - all_cargo_price_override: base_state.all_cargo_price_override, - named_cargo_price_overrides: base_state.named_cargo_price_overrides.clone(), - all_cargo_production_override: base_state.all_cargo_production_override, - factory_cargo_production_override: base_state.factory_cargo_production_override, - farm_mine_cargo_production_override: base_state.farm_mine_cargo_production_override, - named_cargo_production_overrides: base_state.named_cargo_production_overrides.clone(), - cargo_production_overrides: base_state.cargo_production_overrides.clone(), - world_runtime_variables: base_state.world_runtime_variables.clone(), - company_runtime_variables: base_state.company_runtime_variables.clone(), - player_runtime_variables: base_state.player_runtime_variables.clone(), - territory_runtime_variables: base_state.territory_runtime_variables.clone(), - world_scalar_overrides: base_state.world_scalar_overrides.clone(), - special_conditions: projection.special_conditions, - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: if projection - .world_issue_opinion_base_terms_raw_i32 - .is_empty() - { - base_state - .service_state - .world_issue_opinion_base_terms_raw_i32 - .clone() - } else { - projection.world_issue_opinion_base_terms_raw_i32 - }, - company_market_state: if projection.has_company_market_projection { - projection.company_market_state - } else { - base_state.service_state.company_market_state.clone() - }, - company_periodic_side_latch_state: if projection.has_company_market_projection { - projection.company_periodic_side_latch_state - } else { - base_state - .service_state - .company_periodic_side_latch_state - .clone() - }, - chairman_issue_opinion_terms_raw_i32: if projection.has_chairman_projection { - projection.chairman_issue_opinion_terms_raw_i32 - } else { - base_state - .service_state - .chairman_issue_opinion_terms_raw_i32 - .clone() - }, - chairman_personality_raw_u8: if projection.has_chairman_projection { - projection.chairman_personality_raw_u8 - } else { - base_state.service_state.chairman_personality_raw_u8.clone() - }, - ..base_state.service_state.clone() - }, - }; - let mut state = state; - state.refresh_derived_world_state(); - state.refresh_derived_market_state(); - state.validate()?; - - Ok(RuntimeStateImport { - import_id: import_id.to_string(), - description, - state, - }) -} - -fn project_save_slice_components( - save_slice: &SmpLoadedSaveSlice, - company_context: &ImportRuntimeContext, - mode: SaveSliceProjectionMode, -) -> Result { - let mut world_flags = BTreeMap::new(); - world_flags.insert( - "save_slice.profile_present".to_string(), - save_slice.profile.is_some(), - ); - world_flags.insert( - "save_slice.candidate_availability_present".to_string(), - save_slice.candidate_availability_table.is_some(), - ); - world_flags.insert( - "save_slice.special_conditions_present".to_string(), - save_slice.special_conditions_table.is_some(), - ); - world_flags.insert( - "save_slice.named_locomotive_availability_present".to_string(), - save_slice.named_locomotive_availability_table.is_some(), - ); - world_flags.insert( - "save_slice.locomotive_catalog_present".to_string(), - save_slice.locomotive_catalog.is_some() - || save_slice.named_locomotive_availability_table.is_some(), - ); - world_flags.insert( - "save_slice.cargo_catalog_present".to_string(), - save_slice.cargo_catalog.is_some(), - ); - world_flags.insert( - "save_slice.world_issue_37_state_present".to_string(), - save_slice.world_issue_37_state.is_some(), - ); - world_flags.insert( - "save_slice.world_economic_tuning_state_present".to_string(), - save_slice.world_economic_tuning_state.is_some(), - ); - world_flags.insert( - "save_slice.world_finance_neighborhood_state_present".to_string(), - save_slice.world_finance_neighborhood_state.is_some(), - ); - world_flags.insert( - "save_slice.event_runtime_collection_present".to_string(), - save_slice.event_runtime_collection.is_some(), - ); - world_flags.insert( - "save_slice.mechanism_confidence_grounded".to_string(), - save_slice.mechanism_confidence == "grounded", - ); - if let Some(profile) = &save_slice.profile { - world_flags.insert( - "save_slice.profile_byte_0x82_nonzero".to_string(), - profile.profile_byte_0x82 != 0, - ); - world_flags.insert( - "save_slice.profile_byte_0x97_nonzero".to_string(), - profile.profile_byte_0x97 != 0, - ); - world_flags.insert( - "save_slice.profile_byte_0xc5_nonzero".to_string(), - profile.profile_byte_0xc5 != 0, - ); - } - - let mut metadata = BTreeMap::new(); - metadata.insert( - "save_slice.import_projection".to_string(), - match mode { - SaveSliceProjectionMode::Standalone => "partial-runtime-restore-v1", - SaveSliceProjectionMode::Overlay => "overlay-runtime-restore-v1", - } - .to_string(), - ); - metadata.insert( - "save_slice.calendar_source".to_string(), - match mode { - SaveSliceProjectionMode::Standalone => "default-1830-placeholder", - SaveSliceProjectionMode::Overlay => "base-snapshot-preserved", - } - .to_string(), - ); - metadata.insert( - "save_slice.selected_year_seed_tuple_source".to_string(), - "raw-lane-via-0x51d3f0".to_string(), - ); - metadata.insert( - "save_slice.selected_year_absolute_counter_source".to_string(), - if save_slice.world_finance_neighborhood_state.is_some() { - "save-direct-world-block-absolute-counter".to_string() - } else { - "mode-adjusted-lane-via-0x51d390-0x409e80".to_string() - }, - ); - metadata.insert( - "save_slice.selected_year_absolute_counter_reconstructible_from_save".to_string(), - save_slice - .world_finance_neighborhood_state - .is_some() - .to_string(), - ); - metadata.insert( - "save_slice.disable_cargo_economy_special_condition_slot".to_string(), - "30".to_string(), - ); - metadata.insert( - "save_slice.disable_cargo_economy_special_condition_reconstructible_from_save".to_string(), - "true".to_string(), - ); - metadata.insert( - "save_slice.disable_cargo_economy_special_condition_write_side_grounded".to_string(), - "true".to_string(), - ); - metadata.insert( - "save_slice.selected_year_absolute_counter_adjustment_context".to_string(), - if save_slice.world_finance_neighborhood_state.is_some() { - "save-direct-world-block-0x32c8".to_string() - } else { - "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30" - .to_string() - }, - ); - metadata.insert( - "save_slice.mechanism_family".to_string(), - save_slice.mechanism_family.clone(), - ); - metadata.insert( - "save_slice.mechanism_confidence".to_string(), - save_slice.mechanism_confidence.clone(), - ); - if let Some(family) = &save_slice.container_profile_family { - metadata.insert( - "save_slice.container_profile_family".to_string(), - family.clone(), - ); - } - if let Some(family) = &save_slice.trailer_family { - metadata.insert("save_slice.trailer_family".to_string(), family.clone()); - } - if let Some(family) = &save_slice.bridge_family { - metadata.insert("save_slice.bridge_family".to_string(), family.clone()); - } - if let Some(issue_state) = &save_slice.world_issue_37_state { - metadata.insert( - "save_slice.world_issue_37_source_kind".to_string(), - issue_state.source_kind.clone(), - ); - metadata.insert( - "save_slice.world_issue_37_semantic_family".to_string(), - issue_state.semantic_family.clone(), - ); - metadata.insert( - "save_slice.world_issue_37_value".to_string(), - issue_state.issue_value.to_string(), - ); - metadata.insert( - "save_slice.world_issue_37_value_hex".to_string(), - issue_state.issue_value_hex.clone(), - ); - metadata.insert( - "save_slice.world_issue_38_value".to_string(), - issue_state.issue_38_value.to_string(), - ); - metadata.insert( - "save_slice.world_issue_38_value_hex".to_string(), - issue_state.issue_38_value_hex.clone(), - ); - metadata.insert( - "save_slice.world_issue_39_value".to_string(), - issue_state.issue_39_value.to_string(), - ); - metadata.insert( - "save_slice.world_issue_39_value_hex".to_string(), - issue_state.issue_39_value_hex.clone(), - ); - metadata.insert( - "save_slice.world_issue_3a_value".to_string(), - issue_state.issue_3a_value.to_string(), - ); - metadata.insert( - "save_slice.world_issue_3a_value_hex".to_string(), - issue_state.issue_3a_value_hex.clone(), - ); - metadata.insert( - "save_slice.world_issue_37_multiplier_raw_hex".to_string(), - issue_state.multiplier_raw_hex.clone(), - ); - metadata.insert( - "save_slice.world_issue_37_multiplier_value_f32".to_string(), - issue_state.multiplier_value_f32_text.clone(), - ); - } - if let Some(tuning_state) = &save_slice.world_economic_tuning_state { - metadata.insert( - "save_slice.world_economic_tuning_source_kind".to_string(), - tuning_state.source_kind.clone(), - ); - metadata.insert( - "save_slice.world_economic_tuning_semantic_family".to_string(), - tuning_state.semantic_family.clone(), - ); - metadata.insert( - "save_slice.world_economic_tuning_mirror_raw_hex".to_string(), - tuning_state.mirror_raw_hex.clone(), - ); - metadata.insert( - "save_slice.world_economic_tuning_mirror_value_f32".to_string(), - tuning_state.mirror_value_f32_text.clone(), - ); - metadata.insert( - "save_slice.world_economic_tuning_lane_count".to_string(), - tuning_state.lane_raw_u32.len().to_string(), - ); - for (index, value) in tuning_state.lane_value_f32_text.iter().enumerate() { - metadata.insert( - format!("save_slice.world_economic_tuning_lane_{index}_f32"), - value.clone(), - ); - } - } - if let Some(finance_state) = &save_slice.world_finance_neighborhood_state { - metadata.insert( - "save_slice.world_finance_neighborhood_source_kind".to_string(), - finance_state.source_kind.clone(), - ); - metadata.insert( - "save_slice.world_finance_neighborhood_semantic_family".to_string(), - finance_state.semantic_family.clone(), - ); - metadata.insert( - "save_slice.world_finance_neighborhood_candidate_count".to_string(), - finance_state.raw_u32.len().to_string(), - ); - for (index, label) in finance_state.labels.iter().enumerate() { - metadata.insert( - format!("save_slice.world_finance_neighborhood_label_{index}"), - label.clone(), - ); - } - } - - let save_profile = if let Some(profile) = &save_slice.profile { - metadata.insert( - "save_slice.profile_kind".to_string(), - profile.profile_kind.clone(), - ); - metadata.insert( - "save_slice.profile_family".to_string(), - profile.profile_family.clone(), - ); - metadata.insert( - "save_slice.packed_profile_offset".to_string(), - profile.packed_profile_offset.to_string(), - ); - metadata.insert( - "save_slice.packed_profile_len".to_string(), - profile.packed_profile_len.to_string(), - ); - metadata.insert( - "save_slice.leading_word_0_hex".to_string(), - profile.leading_word_0_hex.clone(), - ); - metadata.insert( - "save_slice.profile_byte_0x77_hex".to_string(), - profile.profile_byte_0x77_hex.clone(), - ); - metadata.insert( - "save_slice.profile_byte_0x82_hex".to_string(), - profile.profile_byte_0x82_hex.clone(), - ); - metadata.insert( - "save_slice.profile_byte_0x97_hex".to_string(), - profile.profile_byte_0x97_hex.clone(), - ); - metadata.insert( - "save_slice.profile_byte_0xc5_hex".to_string(), - profile.profile_byte_0xc5_hex.clone(), - ); - if let Some(header_flag_word_3_hex) = &profile.header_flag_word_3_hex { - metadata.insert( - "save_slice.header_flag_word_3_hex".to_string(), - header_flag_word_3_hex.clone(), - ); - } - if let Some(map_path) = &profile.map_path { - metadata.insert("save_slice.map_path".to_string(), map_path.clone()); - } - if let Some(display_name) = &profile.display_name { - metadata.insert("save_slice.display_name".to_string(), display_name.clone()); - } - RuntimeSaveProfileState { - profile_kind: Some(profile.profile_kind.clone()), - profile_family: Some(profile.profile_family.clone()), - map_path: profile.map_path.clone(), - display_name: profile.display_name.clone(), - selected_year_profile_lane: Some(profile.profile_byte_0x77), - sandbox_enabled: Some(profile.profile_byte_0x82 != 0), - campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0), - staged_profile_copy_on_restore: Some(profile.profile_byte_0x97 != 0), - } - } else { - RuntimeSaveProfileState::default() - }; - - let special_condition_enabled = |slot_index: u8| { - save_slice.special_conditions_table.as_ref().map(|table| { - table - .entries - .iter() - .find(|entry| entry.slot_index == slot_index) - .map(|entry| entry.value != 0) - .unwrap_or(false) - }) - }; - - let world_restore = if let Some(profile) = &save_slice.profile { - let disable_cargo_economy_special_condition_enabled = special_condition_enabled(30); - RuntimeWorldRestoreState { - selected_year_profile_lane: Some(profile.profile_byte_0x77), - campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0), - sandbox_enabled: Some(profile.profile_byte_0x82 != 0), - seed_tuple_written_from_raw_lane: Some(true), - absolute_counter_requires_shell_context: Some( - save_slice.world_finance_neighborhood_state.is_none(), - ), - absolute_counter_reconstructible_from_save: Some( - save_slice.world_finance_neighborhood_state.is_some(), - ), - packed_year_word_raw_u16: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.packed_year_word_raw_u16), - partial_year_progress_raw_u8: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.partial_year_progress_raw_u8), - current_calendar_tuple_word_raw_u32: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.current_calendar_tuple_word_raw_u32), - current_calendar_tuple_word_2_raw_u32: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.current_calendar_tuple_word_2_raw_u32), - absolute_counter_raw_u32: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.absolute_counter_raw_u32), - absolute_counter_mirror_raw_u32: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.absolute_counter_mirror_raw_u32), - disable_cargo_economy_special_condition_slot: Some(30), - disable_cargo_economy_special_condition_reconstructible_from_save: Some(true), - disable_cargo_economy_special_condition_write_side_grounded: Some(true), - disable_cargo_economy_special_condition_enabled, - use_bio_accelerator_cars_enabled: special_condition_enabled(29), - use_wartime_cargos_enabled: special_condition_enabled(31), - disable_train_crashes_enabled: special_condition_enabled(32), - disable_train_crashes_and_breakdowns_enabled: special_condition_enabled(33), - ai_ignore_territories_at_startup_enabled: special_condition_enabled(34), - limited_track_building_amount: None, - economic_status_code: None, - territory_access_cost: None, - linked_site_removal_follow_on_gate_raw_u8: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.linked_site_removal_follow_on_gate_raw_u8), - linked_site_removal_follow_on_gate_enabled: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.linked_site_removal_follow_on_gate_raw_u8) - .map(|raw| raw != 0), - auto_show_grade_during_track_lay_raw_u8: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.auto_show_grade_during_track_lay_raw_u8), - starting_building_density_level_raw_u8: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.starting_building_density_level_raw_u8), - post_text_building_density_growth_raw_u8: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.building_density_growth_raw_u8), - leftover_simulation_time_accumulator_raw_u32: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.leftover_simulation_time_accumulator_raw_u32), - leftover_simulation_time_accumulator_value_f32_text: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| { - state - .leftover_simulation_time_accumulator_value_f32_text - .clone() - }), - selected_year_lane_snapshot_raw_u8: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.selected_year_lane_snapshot_raw_u8), - all_steam_locomotives_available_raw_u8: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.all_steam_locomotives_available_raw_u8), - all_steam_locomotives_available_enabled: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.all_steam_locomotives_available_raw_u8) - .map(|raw| raw != 0), - all_diesel_locomotives_available_raw_u8: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.all_diesel_locomotives_available_raw_u8), - all_diesel_locomotives_available_enabled: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.all_diesel_locomotives_available_raw_u8) - .map(|raw| raw != 0), - all_electric_locomotives_available_raw_u8: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.all_electric_locomotives_available_raw_u8), - all_electric_locomotives_available_enabled: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.all_electric_locomotives_available_raw_u8) - .map(|raw| raw != 0), - issue_37_value: save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.issue_value), - issue_38_value: save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.issue_38_value), - issue_39_value: save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.issue_39_value), - issue_3a_value: save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.issue_3a_value), - issue_37_multiplier_raw_u32: save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.multiplier_raw_u32), - issue_37_multiplier_value_f32_text: save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.multiplier_value_f32_text.clone()), - stock_issue_and_buyback_policy_raw_u8: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.stock_policy_raw_u8), - bond_issue_and_repayment_policy_raw_u8: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.bond_policy_raw_u8), - bankruptcy_policy_raw_u8: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.bankruptcy_policy_raw_u8), - dividend_policy_raw_u8: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.dividend_policy_raw_u8), - building_density_growth_setting_raw_u32: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.building_density_growth_setting_raw_u32), - stock_issue_and_buyback_allowed: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.stock_policy_raw_u8 == 0), - bond_issue_and_repayment_allowed: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.bond_policy_raw_u8 == 0), - bankruptcy_allowed: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.bankruptcy_policy_raw_u8 == 0), - dividend_adjustment_allowed: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| state.dividend_policy_raw_u8 == 0), - finance_neighborhood_candidates: save_slice - .world_finance_neighborhood_state - .as_ref() - .map(|state| { - state - .labels - .iter() - .enumerate() - .map(|(index, label)| RuntimeWorldFinanceNeighborhoodCandidate { - label: label.clone(), - relative_offset: state.relative_offsets[index], - relative_offset_hex: state.relative_offset_hex[index].clone(), - raw_u32: state.raw_u32[index], - raw_u32_hex: state.raw_hex[index].clone(), - value_i32: state.value_i32[index], - value_f32_text: state.value_f32_text[index].clone(), - }) - .collect::>() - }) - .unwrap_or_default(), - economic_tuning_mirror_raw_u32: save_slice - .world_economic_tuning_state - .as_ref() - .map(|state| state.mirror_raw_u32), - economic_tuning_mirror_value_f32_text: save_slice - .world_economic_tuning_state - .as_ref() - .map(|state| state.mirror_value_f32_text.clone()), - economic_tuning_lane_raw_u32: save_slice - .world_economic_tuning_state - .as_ref() - .map(|state| state.lane_raw_u32.clone()) - .unwrap_or_default(), - economic_tuning_lane_value_f32_text: save_slice - .world_economic_tuning_state - .as_ref() - .map(|state| state.lane_value_f32_text.clone()) - .unwrap_or_default(), - cached_available_locomotive_rating_raw_u32: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| state.cached_available_locomotive_rating_raw_u32), - cached_available_locomotive_rating_value_f32_text: save_slice - .world_locomotive_policy_state - .as_ref() - .and_then(|state| { - state - .cached_available_locomotive_rating_value_f32_text - .clone() - }), - selected_year_bucket_direct_lane_raw_u32: Vec::new(), - selected_year_bucket_direct_lane_value_f32_text: Vec::new(), - selected_year_bucket_complement_lane_raw_u32: Vec::new(), - selected_year_bucket_complement_lane_value_f32_text: Vec::new(), - selected_year_bucket_scaled_companion_lane_raw_u32: Vec::new(), - selected_year_bucket_scaled_companion_lane_value_f32_text: Vec::new(), - selected_year_bucket_scalar_raw_u32: None, - selected_year_bucket_scalar_value_f32_text: None, - selected_year_gap_scalar_raw_u32: None, - selected_year_gap_scalar_value_f32_text: None, - absolute_counter_restore_kind: Some( - if save_slice.world_finance_neighborhood_state.is_some() { - "save-direct-world-absolute-counter".to_string() - } else { - "mode-adjusted-selected-year-lane".to_string() - }, - ), - absolute_counter_adjustment_context: Some( - if save_slice.world_finance_neighborhood_state.is_some() { - "save-direct-world-block-0x32c8".to_string() - } else { - "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30" - .to_string() - }, - ), - } - } else { - RuntimeWorldRestoreState::default() - }; - - let mut candidate_availability = BTreeMap::new(); - if let Some(table) = &save_slice.candidate_availability_table { - metadata.insert( - "save_slice.candidate_table_source_kind".to_string(), - table.source_kind.clone(), - ); - metadata.insert( - "save_slice.candidate_table_semantic_family".to_string(), - table.semantic_family.clone(), - ); - metadata.insert( - "save_slice.candidate_table_entry_count".to_string(), - table.observed_entry_count.to_string(), - ); - metadata.insert( - "save_slice.candidate_table_zero_count".to_string(), - table.zero_availability_count.to_string(), - ); - for entry in &table.entries { - candidate_availability.insert(entry.text.clone(), entry.availability_dword); - } - } - - let mut special_conditions = BTreeMap::new(); - if let Some(table) = &save_slice.special_conditions_table { - metadata.insert( - "save_slice.special_conditions_source_kind".to_string(), - table.source_kind.clone(), - ); - metadata.insert( - "save_slice.special_conditions_table_offset".to_string(), - table.table_offset.to_string(), - ); - metadata.insert( - "save_slice.special_conditions_enabled_visible_count".to_string(), - table.enabled_visible_count.to_string(), - ); - for entry in &table.entries { - if !entry.hidden { - special_conditions.insert(entry.label.clone(), entry.value); - } - } - } - - let mut named_locomotive_availability = BTreeMap::new(); - if let Some(table) = &save_slice.named_locomotive_availability_table { - metadata.insert( - "save_slice.named_locomotive_availability_source_kind".to_string(), - table.source_kind.clone(), - ); - metadata.insert( - "save_slice.named_locomotive_availability_semantic_family".to_string(), - table.semantic_family.clone(), - ); - metadata.insert( - "save_slice.named_locomotive_availability_entry_count".to_string(), - table.observed_entry_count.to_string(), - ); - metadata.insert( - "save_slice.named_locomotive_availability_zero_count".to_string(), - table.zero_availability_count.to_string(), - ); - if let Some(header_offset) = table.header_offset { - metadata.insert( - "save_slice.named_locomotive_availability_header_offset".to_string(), - header_offset.to_string(), - ); - } - for entry in &table.entries { - named_locomotive_availability.insert(entry.text.clone(), entry.availability_dword); - } - } - let locomotive_catalog = if let Some(catalog) = &save_slice.locomotive_catalog { - metadata.insert( - "save_slice.locomotive_catalog_source_kind".to_string(), - catalog.source_kind.clone(), - ); - metadata.insert( - "save_slice.locomotive_catalog_semantic_family".to_string(), - catalog.semantic_family.clone(), - ); - metadata.insert( - "save_slice.locomotive_catalog_entry_count".to_string(), - catalog.observed_entry_count.to_string(), - ); - if let Some(entries_offset) = catalog.entries_offset { - metadata.insert( - "save_slice.locomotive_catalog_entries_offset".to_string(), - entries_offset.to_string(), - ); - } - Some( - catalog - .entries - .iter() - .map(|entry| RuntimeLocomotiveCatalogEntry { - locomotive_id: entry.locomotive_id, - name: entry.name.clone(), - }) - .collect::>(), - ) - } else if let Some(table) = &save_slice.named_locomotive_availability_table { - metadata.insert( - "save_slice.locomotive_catalog_source_kind".to_string(), - "derived-from-named-locomotive-availability-table".to_string(), - ); - metadata.insert( - "save_slice.locomotive_catalog_semantic_family".to_string(), - "scenario-save-derived-locomotive-catalog".to_string(), - ); - metadata.insert( - "save_slice.locomotive_catalog_entry_count".to_string(), - table.observed_entry_count.to_string(), - ); - if let Some(entries_offset) = table.entries_offset { - metadata.insert( - "save_slice.locomotive_catalog_entries_offset".to_string(), - entries_offset.to_string(), - ); - } - Some( - table - .entries - .iter() - .enumerate() - .map(|(index, entry)| RuntimeLocomotiveCatalogEntry { - locomotive_id: (index + 1) as u32, - name: entry.text.clone(), - }) - .collect::>(), - ) - } else { - None - }; - let cargo_catalog = if let Some(catalog) = &save_slice.cargo_catalog { - metadata.insert( - "save_slice.cargo_catalog_source_kind".to_string(), - catalog.source_kind.clone(), - ); - metadata.insert( - "save_slice.cargo_catalog_semantic_family".to_string(), - catalog.semantic_family.clone(), - ); - metadata.insert( - "save_slice.cargo_catalog_entry_count".to_string(), - catalog.observed_entry_count.to_string(), - ); - if let Some(root_offset) = catalog.root_offset { - metadata.insert( - "save_slice.cargo_catalog_root_offset".to_string(), - root_offset.to_string(), - ); - } - Some( - catalog - .entries - .iter() - .map(|entry| RuntimeCargoCatalogEntry { - slot_id: entry.slot_id, - label: entry.label.clone(), - cargo_class: entry.cargo_class, - supplied_token_stem: entry - .supplied_cargo_token_probable_high16_ascii_stem - .clone(), - demanded_token_stem: entry - .demanded_cargo_token_probable_high16_ascii_stem - .clone(), - }) - .collect::>(), - ) - } else { - None - }; - - let ( - companies, - has_company_projection, - has_company_selection_override, - selected_company_id, - company_market_state, - company_periodic_side_latch_state, - world_issue_opinion_base_terms_raw_i32, - has_company_market_projection, - ) = if let Some(roster) = &save_slice.company_roster { - metadata.insert( - "save_slice.company_roster_source_kind".to_string(), - roster.source_kind.clone(), - ); - metadata.insert( - "save_slice.company_roster_semantic_family".to_string(), - roster.semantic_family.clone(), - ); - metadata.insert( - "save_slice.company_roster_entry_count".to_string(), - roster.observed_entry_count.to_string(), - ); - let market_state = roster - .entries - .iter() - .filter_map(|entry| { - entry - .market_state - .as_ref() - .map(|state| (entry.company_id, state.clone())) - }) - .collect::>(); - let periodic_side_latch_state = roster - .entries - .iter() - .map(|entry| { - ( - entry.company_id, - crate::RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: entry - .preferred_locomotive_engine_type_raw_u8, - city_connection_latch: entry - .market_state - .as_ref() - .map(|state| state.city_connection_latch) - .unwrap_or(false), - linked_transit_latch: entry - .market_state - .as_ref() - .map(|state| state.linked_transit_latch) - .unwrap_or(false), - }, - ) - }) - .collect::>(); - metadata.insert( - "save_slice.company_market_state_owner_count".to_string(), - market_state.len().to_string(), - ); - if let Some(selected_company_id) = roster.selected_company_id { - metadata.insert( - "save_slice.selected_company_id".to_string(), - selected_company_id.to_string(), - ); - } - if roster.entries.is_empty() { - ( - Vec::new(), - false, - roster.selected_company_id.is_some(), - roster.selected_company_id, - BTreeMap::new(), - periodic_side_latch_state, - save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.issue_opinion_base_terms_raw_i32.clone()) - .unwrap_or_default(), - false, - ) - } else { - ( - roster - .entries - .iter() - .map(|entry| RuntimeCompany { - company_id: entry.company_id, - current_cash: entry.current_cash, - debt: entry.debt, - credit_rating_score: entry.credit_rating_score, - prime_rate: entry.prime_rate, - active: entry.active, - available_track_laying_capacity: entry.available_track_laying_capacity, - controller_kind: entry.controller_kind, - linked_chairman_profile_id: entry.linked_chairman_profile_id, - book_value_per_share: entry.book_value_per_share, - investor_confidence: entry.investor_confidence, - management_attitude: entry.management_attitude, - takeover_cooldown_year: entry.takeover_cooldown_year, - merger_cooldown_year: entry.merger_cooldown_year, - track_piece_counts: entry.track_piece_counts, - }) - .collect::>(), - true, - roster.selected_company_id.is_some(), - roster.selected_company_id, - market_state, - periodic_side_latch_state, - save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.issue_opinion_base_terms_raw_i32.clone()) - .unwrap_or_default(), - true, - ) - } - } else { - ( - Vec::new(), - false, - false, - None, - BTreeMap::new(), - BTreeMap::new(), - save_slice - .world_issue_37_state - .as_ref() - .map(|state| state.issue_opinion_base_terms_raw_i32.clone()) - .unwrap_or_default(), - false, - ) - }; - - let ( - chairman_profiles, - has_chairman_projection, - has_chairman_selection_override, - selected_chairman_profile_id, - chairman_issue_opinion_terms_raw_i32, - chairman_personality_raw_u8, - ) = if let Some(table) = &save_slice.chairman_profile_table { - metadata.insert( - "save_slice.chairman_profile_table_source_kind".to_string(), - table.source_kind.clone(), - ); - metadata.insert( - "save_slice.chairman_profile_table_semantic_family".to_string(), - table.semantic_family.clone(), - ); - metadata.insert( - "save_slice.chairman_profile_table_entry_count".to_string(), - table.observed_entry_count.to_string(), - ); - if let Some(selected_chairman_profile_id) = table.selected_chairman_profile_id { - metadata.insert( - "save_slice.selected_chairman_profile_id".to_string(), - selected_chairman_profile_id.to_string(), - ); - } - if table.entries.is_empty() { - ( - Vec::new(), - false, - table.selected_chairman_profile_id.is_some(), - table.selected_chairman_profile_id, - BTreeMap::new(), - BTreeMap::new(), - ) - } else { - ( - table - .entries - .iter() - .map(|entry| RuntimeChairmanProfile { - profile_id: entry.profile_id, - name: entry.name.clone(), - active: entry.active, - current_cash: entry.current_cash, - linked_company_id: entry.linked_company_id, - company_holdings: entry.company_holdings.clone(), - holdings_value_total: entry.holdings_value_total, - net_worth_total: entry.net_worth_total, - purchasing_power_total: entry.purchasing_power_total, - }) - .collect::>(), - true, - table.selected_chairman_profile_id.is_some(), - table.selected_chairman_profile_id, - table - .entries - .iter() - .map(|entry| (entry.profile_id, entry.issue_opinion_terms_raw_i32.clone())) - .collect::>(), - table - .entries - .iter() - .filter_map(|entry| { - entry - .personality_byte_0x291 - .map(|value| (entry.profile_id, value)) - }) - .collect::>(), - ) - } - } else { - ( - Vec::new(), - false, - false, - None, - BTreeMap::new(), - BTreeMap::new(), - ) - }; - - if let Some(collection) = &save_slice.placed_structure_collection { - metadata.insert( - "save_slice.placed_structure_collection_source_kind".to_string(), - collection.source_kind.clone(), - ); - metadata.insert( - "save_slice.placed_structure_collection_semantic_family".to_string(), - collection.semantic_family.clone(), - ); - metadata.insert( - "save_slice.placed_structure_collection_entry_count".to_string(), - collection.observed_entry_count.to_string(), - ); - metadata.insert( - "save_slice.placed_structure_collection_farm_growth_stage_count".to_string(), - collection - .entries - .iter() - .filter(|entry| entry.farm_growth_stage_index.is_some()) - .count() - .to_string(), - ); - metadata.insert( - "save_slice.placed_structure_collection_nondefault_status_count".to_string(), - collection - .entries - .iter() - .filter(|entry| entry.profile_status_kind != "unset") - .count() - .to_string(), - ); - } - if let Some(summary) = &save_slice.placed_structure_dynamic_side_buffer_summary { - metadata.insert( - "save_slice.placed_structure_dynamic_side_buffer_source_kind".to_string(), - summary.source_kind.clone(), - ); - metadata.insert( - "save_slice.placed_structure_dynamic_side_buffer_semantic_family".to_string(), - summary.semantic_family.clone(), - ); - metadata.insert( - "save_slice.placed_structure_dynamic_side_buffer_entry_count".to_string(), - summary.observed_entry_count.to_string(), - ); - metadata.insert( - "save_slice.placed_structure_dynamic_side_buffer_name_pair_count".to_string(), - summary.unique_embedded_name_pair_count.to_string(), - ); - metadata.insert( - "save_slice.placed_structure_dynamic_side_buffer_triplet_overlap_count".to_string(), - summary.triplet_alignment_overlap_count.to_string(), - ); - metadata.insert( - "save_slice.placed_structure_dynamic_side_buffer_side_buffer_only_name_pair_count" - .to_string(), - summary - .triplet_alignment_side_buffer_only_name_pair_count - .to_string(), - ); - } - if let Some(collection) = &save_slice.region_collection { - metadata.insert( - "save_slice.region_collection_source_kind".to_string(), - collection.source_kind.clone(), - ); - metadata.insert( - "save_slice.region_collection_semantic_family".to_string(), - collection.semantic_family.clone(), - ); - metadata.insert( - "save_slice.region_collection_entry_count".to_string(), - collection.observed_entry_count.to_string(), - ); - metadata.insert( - "save_slice.region_collection_profile_entry_count".to_string(), - collection - .entries - .iter() - .map(|entry| { - entry - .profile_collection - .as_ref() - .map(|collection| collection.entries.len()) - .unwrap_or_default() - }) - .sum::() - .to_string(), - ); - metadata.insert( - "save_slice.region_collection_nonzero_prefix_count".to_string(), - collection - .entries - .iter() - .filter(|entry| entry.pre_name_prefix_len != 0) - .count() - .to_string(), - ); - metadata.insert( - "save_slice.region_collection_nonzero_reserved_policy_count".to_string(), - collection - .entries - .iter() - .filter(|entry| entry.policy_reserved_dwords.iter().any(|raw| *raw != 0)) - .count() - .to_string(), - ); - } - if let Some(summary) = &save_slice.region_fixed_row_run_summary { - metadata.insert( - "save_slice.region_fixed_row_run_source_kind".to_string(), - summary.source_kind.clone(), - ); - metadata.insert( - "save_slice.region_fixed_row_run_semantic_family".to_string(), - summary.semantic_family.clone(), - ); - metadata.insert( - "save_slice.region_fixed_row_run_candidate_count".to_string(), - summary.candidates.len().to_string(), - ); - metadata.insert( - "save_slice.region_fixed_row_run_target_row_stride_hex".to_string(), - summary.target_row_stride_hex.clone(), - ); - metadata.insert( - "save_slice.region_fixed_row_run_best_rows_offset_hex".to_string(), - summary - .candidates - .first() - .map(|candidate| candidate.rows_offset_hex.clone()) - .unwrap_or_default(), - ); - metadata.insert( - "save_slice.region_fixed_row_run_best_shape_signature".to_string(), - summary - .candidates - .first() - .map(|candidate| candidate.shape_signature.clone()) - .unwrap_or_default(), - ); - } - - let named_locomotive_cost = BTreeMap::new(); - let all_cargo_price_override = None; - let named_cargo_price_overrides = BTreeMap::new(); - let all_cargo_production_override = None; - let factory_cargo_production_override = None; - let farm_mine_cargo_production_override = None; - let named_cargo_production_overrides = BTreeMap::new(); - let cargo_production_overrides = BTreeMap::new(); - let world_scalar_overrides = BTreeMap::new(); - - let mut packed_event_context = company_context.clone(); - if has_company_projection { - packed_event_context.known_company_ids = - companies.iter().map(|company| company.company_id).collect(); - packed_event_context.selected_company_id = selected_company_id; - packed_event_context.has_complete_company_controller_context = !companies.is_empty() - && companies - .iter() - .all(|company| company.controller_kind != RuntimeCompanyControllerKind::Unknown); - } else if has_company_selection_override { - packed_event_context.selected_company_id = selected_company_id; - } - if has_chairman_projection { - packed_event_context.known_chairman_profile_ids = chairman_profiles - .iter() - .map(|profile| profile.profile_id) - .collect(); - packed_event_context.selected_chairman_profile_id = selected_chairman_profile_id; - } else if has_chairman_selection_override { - packed_event_context.selected_chairman_profile_id = selected_chairman_profile_id; - } - if let Some(catalog) = &locomotive_catalog { - packed_event_context.locomotive_catalog_names_by_id = catalog - .iter() - .map(|entry| (entry.locomotive_id, entry.name.clone())) - .collect(); - } - - let (packed_event_collection, event_runtime_records) = project_packed_event_collection( - save_slice, - &packed_event_context, - cargo_catalog.as_deref().unwrap_or(&[]), - )?; - if let Some(summary) = &save_slice.event_runtime_collection { - metadata.insert( - "save_slice.event_runtime_collection_source_kind".to_string(), - summary.source_kind.clone(), - ); - metadata.insert( - "save_slice.event_runtime_collection_version_hex".to_string(), - summary.packed_state_version_hex.clone(), - ); - metadata.insert( - "save_slice.event_runtime_collection_record_count".to_string(), - summary.live_record_count.to_string(), - ); - metadata.insert( - "save_slice.event_runtime_collection_decoded_record_count".to_string(), - summary.decoded_record_count.to_string(), - ); - metadata.insert( - "save_slice.event_runtime_collection_imported_runtime_record_count".to_string(), - event_runtime_records.len().to_string(), - ); - } - - for (index, note) in save_slice.notes.iter().enumerate() { - metadata.insert(format!("save_slice.note.{index}"), note.clone()); - } - - Ok(SaveSliceProjection { - world_flags, - save_profile, - world_restore, - metadata, - packed_event_collection, - event_runtime_records, - companies, - has_company_projection, - has_company_selection_override, - selected_company_id, - company_market_state, - company_periodic_side_latch_state, - has_company_market_projection, - world_issue_opinion_base_terms_raw_i32, - chairman_profiles, - has_chairman_projection, - has_chairman_selection_override, - selected_chairman_profile_id, - chairman_issue_opinion_terms_raw_i32, - chairman_personality_raw_u8, - candidate_availability, - named_locomotive_availability, - locomotive_catalog, - cargo_catalog, - named_locomotive_cost, - all_cargo_price_override, - named_cargo_price_overrides, - all_cargo_production_override, - factory_cargo_production_override, - farm_mine_cargo_production_override, - named_cargo_production_overrides, - cargo_production_overrides, - world_scalar_overrides, - special_conditions, - }) -} - -fn project_packed_event_collection( - save_slice: &SmpLoadedSaveSlice, - company_context: &ImportRuntimeContext, - cargo_catalog: &[RuntimeCargoCatalogEntry], -) -> Result< - ( - Option, - Vec, - ), - String, -> { - let Some(summary) = save_slice.event_runtime_collection.as_ref() else { - return Ok((None, Vec::new())); - }; - - let mut imported_runtime_records = Vec::new(); - let mut imported_record_ids = BTreeSet::new(); - for record in &summary.records { - if let Some(import_result) = - smp_packed_record_to_runtime_event_record(record, company_context) - { - let runtime_record = import_result?; - imported_record_ids.insert(record.live_entry_id); - imported_runtime_records.push(runtime_record); - } - } - - let records = summary - .records - .iter() - .map(|record| { - runtime_packed_event_record_summary_from_smp( - record, - company_context, - cargo_catalog, - imported_record_ids.contains(&record.live_entry_id), - ) - }) - .collect::>(); - - Ok(( - Some(RuntimePackedEventCollectionSummary { - source_kind: summary.source_kind.clone(), - mechanism_family: summary.mechanism_family.clone(), - mechanism_confidence: summary.mechanism_confidence.clone(), - container_profile_family: summary.container_profile_family.clone(), - packed_state_version: summary.packed_state_version, - packed_state_version_hex: summary.packed_state_version_hex.clone(), - live_id_bound: summary.live_id_bound, - live_record_count: summary.live_record_count, - live_entry_ids: summary.live_entry_ids.clone(), - decoded_record_count: records - .iter() - .filter(|record| record.decode_status != "unsupported_framing") - .count(), - imported_runtime_record_count: imported_runtime_records.len(), - records, - }), - imported_runtime_records, - )) -} - -fn runtime_packed_event_record_summary_from_smp( - record: &SmpLoadedPackedEventRecordSummary, - company_context: &ImportRuntimeContext, - cargo_catalog: &[RuntimeCargoCatalogEntry], - imported: bool, -) -> RuntimePackedEventRecordSummary { - let lowered_decoded_conditions = lowered_record_decoded_conditions(record, company_context) - .unwrap_or_else(|_| record.decoded_conditions.clone()); - let lowered_decoded_actions = lowered_record_decoded_actions(record, company_context) - .unwrap_or_else(|_| record.decoded_actions.clone()); - RuntimePackedEventRecordSummary { - record_index: record.record_index, - live_entry_id: record.live_entry_id, - payload_offset: record.payload_offset, - payload_len: record.payload_len, - decode_status: record.decode_status.clone(), - payload_family: record.payload_family.clone(), - trigger_kind: record.trigger_kind, - active: record.active, - marks_collection_dirty: record.marks_collection_dirty, - one_shot: record.one_shot, - compact_control: record - .compact_control - .as_ref() - .map(runtime_packed_event_compact_control_summary_from_smp), - text_bands: record - .text_bands - .iter() - .map(runtime_packed_event_text_band_summary_from_smp) - .collect(), - standalone_condition_row_count: record.standalone_condition_row_count, - standalone_condition_rows: record - .standalone_condition_rows - .iter() - .map(|row| runtime_packed_event_condition_row_summary_from_smp(row, cargo_catalog)) - .collect(), - negative_sentinel_scope: record - .negative_sentinel_scope - .as_ref() - .map(runtime_packed_event_negative_sentinel_scope_summary_from_smp), - grouped_effect_row_counts: record.grouped_effect_row_counts.clone(), - grouped_effect_rows: record - .grouped_effect_rows - .iter() - .map(|row| runtime_packed_event_grouped_effect_row_summary_from_smp(row, cargo_catalog)) - .collect(), - grouped_company_targets: classify_real_grouped_company_targets(record), - decoded_conditions: lowered_decoded_conditions, - decoded_actions: lowered_decoded_actions, - executable_import_ready: record.executable_import_ready, - import_outcome: Some(determine_packed_event_import_outcome( - record, - company_context, - imported, - )), - notes: record.notes.clone(), - } -} - -fn runtime_packed_event_negative_sentinel_scope_summary_from_smp( - scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, -) -> RuntimePackedEventNegativeSentinelScopeSummary { - RuntimePackedEventNegativeSentinelScopeSummary { - company_test_scope: scope.company_test_scope, - player_test_scope: scope.player_test_scope, - territory_scope_selector_is_0x63: scope.territory_scope_selector_is_0x63, - source_row_indexes: scope.source_row_indexes.clone(), - } -} - -fn runtime_packed_event_compact_control_summary_from_smp( - control: &crate::SmpLoadedPackedEventCompactControlSummary, -) -> RuntimePackedEventCompactControlSummary { - RuntimePackedEventCompactControlSummary { - mode_byte_0x7ef: control.mode_byte_0x7ef, - primary_selector_0x7f0: control.primary_selector_0x7f0, - grouped_mode_0x7f4: control.grouped_mode_0x7f4, - one_shot_header_0x7f5: control.one_shot_header_0x7f5, - modifier_flag_0x7f9: control.modifier_flag_0x7f9, - modifier_flag_0x7fa: control.modifier_flag_0x7fa, - grouped_target_scope_ordinals_0x7fb: control.grouped_target_scope_ordinals_0x7fb.clone(), - grouped_scope_checkboxes_0x7ff: control.grouped_scope_checkboxes_0x7ff.clone(), - summary_toggle_0x800: control.summary_toggle_0x800, - grouped_territory_selectors_0x80f: control.grouped_territory_selectors_0x80f.clone(), - } -} - -fn runtime_packed_event_text_band_summary_from_smp( - band: &SmpLoadedPackedEventTextBandSummary, -) -> RuntimePackedEventTextBandSummary { - RuntimePackedEventTextBandSummary { - label: band.label.clone(), - packed_len: band.packed_len, - present: band.present, - preview: band.preview.clone(), - } -} - -fn runtime_packed_event_condition_row_summary_from_smp( - row: &crate::SmpLoadedPackedEventConditionRowSummary, - cargo_catalog: &[RuntimeCargoCatalogEntry], -) -> RuntimePackedEventConditionRowSummary { - let cargo_entry = row - .recovered_cargo_slot - .and_then(|slot| cargo_catalog.iter().find(|entry| entry.slot_id == slot)); - RuntimePackedEventConditionRowSummary { - row_index: row.row_index, - raw_condition_id: row.raw_condition_id, - subtype: row.subtype, - flag_bytes: row.flag_bytes.clone(), - candidate_name: row.candidate_name.clone(), - comparator: row.comparator.clone(), - metric: row.metric.clone(), - semantic_family: row.semantic_family.clone(), - semantic_preview: row.semantic_preview.clone(), - requires_candidate_name_binding: row.requires_candidate_name_binding, - recovered_cargo_slot: row.recovered_cargo_slot, - recovered_cargo_class: cargo_entry - .map(|entry| format!("{:?}", entry.cargo_class).to_ascii_lowercase()) - .map(|value| value.replace("farmmine", "farm_mine")) - .or_else(|| row.recovered_cargo_class.clone()), - recovered_cargo_label: cargo_entry - .map(|entry| entry.label.clone()) - .or_else(|| row.recovered_cargo_slot.map(default_cargo_slot_label)), - recovered_cargo_supplied_token_stem: cargo_entry - .and_then(|entry| entry.supplied_token_stem.clone()), - recovered_cargo_demanded_token_stem: cargo_entry - .and_then(|entry| entry.demanded_token_stem.clone()), - notes: row.notes.clone(), - } -} - -fn runtime_packed_event_grouped_effect_row_summary_from_smp( - row: &crate::SmpLoadedPackedEventGroupedEffectRowSummary, - cargo_catalog: &[RuntimeCargoCatalogEntry], -) -> RuntimePackedEventGroupedEffectRowSummary { - let cargo_entry = row - .recovered_cargo_slot - .and_then(|slot| cargo_catalog.iter().find(|entry| entry.slot_id == slot)); - RuntimePackedEventGroupedEffectRowSummary { - group_index: row.group_index, - row_index: row.row_index, - descriptor_id: row.descriptor_id, - descriptor_label: row.descriptor_label.clone(), - target_mask_bits: row.target_mask_bits, - parameter_family: row.parameter_family.clone(), - grouped_target_subject: row.grouped_target_subject.clone(), - grouped_target_scope: row.grouped_target_scope.clone(), - opcode: row.opcode, - raw_scalar_value: row.raw_scalar_value, - value_byte_0x09: row.value_byte_0x09, - value_dword_0x0d: row.value_dword_0x0d, - value_byte_0x11: row.value_byte_0x11, - value_byte_0x12: row.value_byte_0x12, - value_word_0x14: row.value_word_0x14, - value_word_0x16: row.value_word_0x16, - row_shape: row.row_shape.clone(), - semantic_family: row.semantic_family.clone(), - semantic_preview: row.semantic_preview.clone(), - recovered_cargo_slot: row.recovered_cargo_slot, - recovered_cargo_class: cargo_entry - .map(|entry| format!("{:?}", entry.cargo_class).to_ascii_lowercase()) - .map(|value| value.replace("farmmine", "farm_mine")) - .or_else(|| row.recovered_cargo_class.clone()), - recovered_cargo_label: cargo_entry - .map(|entry| entry.label.clone()) - .or_else(|| row.recovered_cargo_label.clone()) - .or_else(|| row.recovered_cargo_slot.map(default_cargo_slot_label)), - recovered_cargo_supplied_token_stem: cargo_entry - .and_then(|entry| entry.supplied_token_stem.clone()), - recovered_cargo_demanded_token_stem: cargo_entry - .and_then(|entry| entry.demanded_token_stem.clone()), - recovered_locomotive_id: row.recovered_locomotive_id, - locomotive_name: row.locomotive_name.clone(), - notes: row.notes.clone(), - } -} - -fn default_cargo_slot_label(slot: u32) -> String { - format!("Cargo Production Slot {slot}") -} - -fn smp_packed_record_to_runtime_event_record( - record: &SmpLoadedPackedEventRecordSummary, - company_context: &ImportRuntimeContext, -) -> Option> { - if record.decode_status == "unsupported_framing" { - return None; - } - if record.payload_family == "real_packed_v1" && record.compact_control.is_none() { - return None; - } - - let lowered_conditions = match lowered_record_decoded_conditions(record, company_context) { - Ok(conditions) => conditions, - Err(_) => return None, - }; - let lowered_effects = match lowered_record_decoded_actions(record, company_context) { - Ok(effects) => effects, - Err(_) => return None, - }; - let effects = match smp_runtime_effects_to_runtime_effects( - &lowered_effects, - company_context, - conditions_provide_company_context(&lowered_conditions), - false, - ) { - Ok(effects) => effects, - Err(_) => return None, - }; - - Some((|| { - let trigger_kind = record.trigger_kind.ok_or_else(|| { - format!( - "packed event record {} is missing trigger_kind", - record.live_entry_id - ) - })?; - let active = record.active.unwrap_or(true); - let marks_collection_dirty = record.marks_collection_dirty.unwrap_or(false); - let one_shot = record.one_shot.unwrap_or(false); - Ok(RuntimeEventRecordTemplate { - record_id: record.live_entry_id, - trigger_kind, - active, - marks_collection_dirty, - one_shot, - conditions: lowered_conditions, - effects, - } - .into_runtime_record()) - })()) -} - -fn lowered_record_decoded_conditions( - record: &SmpLoadedPackedEventRecordSummary, - company_context: &ImportRuntimeContext, -) -> Result, ImportBlocker> { - if let Some(blocker) = packed_record_condition_scope_import_blocker(record, company_context) { - return Err(blocker); - } - - let lowered_company_target = lowered_condition_true_company_target(record)?; - let lowered_player_target = lowered_condition_true_player_target(record)?; - let ordinary_rows = record - .standalone_condition_rows - .iter() - .filter(|row| row.raw_condition_id >= 0); - ordinary_rows - .zip(record.decoded_conditions.iter()) - .map(|(row, condition)| { - lower_condition_targets_in_condition( - condition, - row, - lowered_company_target.as_ref(), - lowered_player_target.as_ref(), - company_context, - ) - }) - .collect() -} - -fn lowered_record_decoded_actions( - record: &SmpLoadedPackedEventRecordSummary, - company_context: &ImportRuntimeContext, -) -> Result, ImportBlocker> { - if let Some(blocker) = packed_record_condition_scope_import_blocker(record, company_context) { - return Err(blocker); - } - ensure_condition_true_chairman_context(record)?; - - let lowered_company_target = lowered_condition_true_company_target(record)?; - let lowered_player_target = lowered_condition_true_player_target(record)?; - let base_effects = if record.payload_family != "real_packed_v1" - || record.decoded_actions.len() == record.grouped_effect_rows.len() - { - record.decoded_actions.clone() - } else { - lower_contextual_real_grouped_effects(record, company_context)? - }; - base_effects - .iter() - .map(|effect| { - lower_condition_targets_in_effect( - effect, - lowered_company_target.as_ref(), - lowered_player_target.as_ref(), - ) - }) - .collect() -} - -fn lower_contextual_real_grouped_effects( - record: &SmpLoadedPackedEventRecordSummary, - company_context: &ImportRuntimeContext, -) -> Result, ImportBlocker> { - if record.payload_family != "real_packed_v1" || record.compact_control.is_none() { - return Err(ImportBlocker::UnmappedWorldCondition); - } - - let mut effects = Vec::with_capacity(record.grouped_effect_rows.len()); - for row in &record.grouped_effect_rows { - if real_grouped_row_is_unsupported_chairman_target_scope(row) { - return Err(ImportBlocker::ChairmanTargetScope); - } - if let Some(effect) = lower_contextual_cargo_price_effect(row)? { - effects.push(effect); - continue; - } - if let Some(effect) = lower_contextual_world_scalar_override_effect(row)? { - effects.push(effect); - continue; - } - if let Some(effect) = lower_contextual_runtime_variable_effect(row)? { - effects.push(effect); - continue; - } - if let Some(effect) = lower_contextual_cargo_production_effect(row)? { - effects.push(effect); - continue; - } - if let Some(effect) = lower_contextual_territory_access_cost_effect(row)? { - effects.push(effect); - continue; - } - if let Some(effect) = lower_contextual_locomotive_cost_effect(row, company_context)? { - effects.push(effect); - continue; - } - if let Some(effect) = lower_contextual_locomotive_availability_effect(row, company_context)? - { - effects.push(effect); - continue; - } - return Err(if real_grouped_row_is_world_state_family(row) { - ImportBlocker::UnmappedWorldCondition - } else { - ImportBlocker::UnmappedOrdinaryCondition - }); - } - - if effects.is_empty() { - return Err(ImportBlocker::UnmappedWorldCondition); - } - - Ok(effects) -} - -fn lower_contextual_cargo_price_effect( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> Result, ImportBlocker> { - if row.parameter_family.as_deref() != Some("cargo_price_scalar") { - return Ok(None); - } - if row.row_shape != "scalar_assignment" { - return Ok(None); - } - let Some(value) = u32::try_from(row.raw_scalar_value).ok() else { - return Ok(None); - }; - let target = if row.descriptor_id == 105 { - RuntimeCargoPriceTarget::All - } else if let Some(name) = row.recovered_cargo_label.as_deref() { - RuntimeCargoPriceTarget::Named { - name: name.to_string(), - } - } else { - return Ok(None); - }; - Ok(Some(RuntimeEffect::SetCargoPriceOverride { target, value })) -} - -fn lower_contextual_world_scalar_override_effect( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> Result, ImportBlocker> { - if row.parameter_family.as_deref() != Some("world_scalar_override") { - return Ok(None); - } - if row.row_shape != "scalar_assignment" { - return Ok(None); - } - let Some(key) = row - .descriptor_label - .as_deref() - .map(crate::smp::runtime_world_scalar_key_from_label) - else { - return Ok(None); - }; - Ok(Some(RuntimeEffect::SetWorldScalarOverride { - key, - value: i64::from(row.raw_scalar_value), - })) -} - -fn lower_contextual_runtime_variable_effect( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> Result, ImportBlocker> { - if row.parameter_family.as_deref() != Some("runtime_variable_scalar") { - return Ok(None); - } - if row.row_shape != "scalar_assignment" { - return Ok(None); - } - let value = i64::from(row.raw_scalar_value); - Ok(match row.descriptor_id { - 39..=42 => Some(RuntimeEffect::SetWorldVariable { - index: row.descriptor_id - 38, - value, - }), - _ => None, - }) -} - -fn lower_contextual_locomotive_availability_effect( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, - company_context: &ImportRuntimeContext, -) -> Result, ImportBlocker> { - if row.parameter_family.as_deref() != Some("locomotive_availability_scalar") { - return Ok(None); - } - if row.row_shape != "scalar_assignment" { - return Ok(None); - } - let Some(value) = u32::try_from(row.raw_scalar_value).ok() else { - return Ok(None); - }; - let Some(locomotive_id) = row.recovered_locomotive_id else { - return Ok(None); - }; - let Some(name) = company_context - .locomotive_catalog_names_by_id - .get(&locomotive_id) - .cloned() - else { - return Err(ImportBlocker::MissingLocomotiveCatalogContext); - }; - Ok(Some(RuntimeEffect::SetNamedLocomotiveAvailabilityValue { - name, - value, - })) -} - -fn lower_contextual_cargo_production_effect( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> Result, ImportBlocker> { - if row.parameter_family.as_deref() != Some("cargo_production_scalar") { - return Ok(None); - } - if row.row_shape != "scalar_assignment" { - return Ok(None); - } - let Some(value) = u32::try_from(row.raw_scalar_value).ok() else { - return Ok(None); - }; - match row.descriptor_id { - 177 => Ok(Some(RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::All, - value, - })), - 178 => Ok(Some(RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::Factory, - value, - })), - 179 => Ok(Some(RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::FarmMine, - value, - })), - 180..=229 => { - let Some(name) = row.recovered_cargo_label.clone() else { - return Err(ImportBlocker::EvidenceBlockedDescriptor); - }; - Ok(Some(RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::Named { name }, - value, - })) - } - 230..=240 => { - let Some(slot) = row.descriptor_id.checked_sub(229) else { - return Ok(None); - }; - Ok(Some(RuntimeEffect::SetCargoProductionSlot { slot, value })) - } - _ => Ok(None), - } -} - -fn lower_contextual_territory_access_cost_effect( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> Result, ImportBlocker> { - if row.parameter_family.as_deref() != Some("territory_access_cost_scalar") { - return Ok(None); - } - if row.row_shape != "scalar_assignment" { - return Ok(None); - } - let Some(value) = u32::try_from(row.raw_scalar_value).ok() else { - return Ok(None); - }; - Ok(Some(RuntimeEffect::SetTerritoryAccessCost { value })) -} - -fn lower_contextual_locomotive_cost_effect( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, - company_context: &ImportRuntimeContext, -) -> Result, ImportBlocker> { - if row.parameter_family.as_deref() != Some("locomotive_cost_scalar") { - return Ok(None); - } - if row.row_shape != "scalar_assignment" { - return Ok(None); - } - let value = u32::try_from(row.raw_scalar_value).ok(); - let Some(value) = value else { - return Ok(None); - }; - let Some(locomotive_id) = row.recovered_locomotive_id else { - return Ok(None); - }; - let Some(name) = company_context - .locomotive_catalog_names_by_id - .get(&locomotive_id) - .cloned() - else { - return Err(ImportBlocker::MissingLocomotiveCatalogContext); - }; - Ok(Some(RuntimeEffect::SetNamedLocomotiveCost { name, value })) -} - -fn packed_record_condition_scope_import_blocker( - record: &SmpLoadedPackedEventRecordSummary, - company_context: &ImportRuntimeContext, -) -> Option { - if record.standalone_condition_rows.is_empty() { - return None; - } - - let ordinary_condition_row_count = record - .standalone_condition_rows - .iter() - .filter(|row| row.raw_condition_id >= 0) - .count(); - if ordinary_condition_row_count != 0 { - if ordinary_condition_row_count != record.decoded_conditions.len() { - return Some(if record_has_world_state_condition_rows(record) { - ImportBlocker::UnmappedWorldCondition - } else { - ImportBlocker::UnmappedOrdinaryCondition - }); - } - if (!company_context.has_territory_context) - && (record - .standalone_condition_rows - .iter() - .any(|row| row.requires_candidate_name_binding) - || record.decoded_conditions.iter().any(|condition| { - matches!( - condition, - RuntimeCondition::TerritoryNumericThreshold { .. } - | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } - ) - })) - { - return Some(ImportBlocker::MissingTerritoryContext); - } - } - - let negative_sentinel_row_count = record - .standalone_condition_rows - .iter() - .filter(|row| row.raw_condition_id == -1) - .count(); - if negative_sentinel_row_count == 0 { - return if ordinary_condition_row_count == 0 { - Some(ImportBlocker::MissingConditionContext) - } else { - None - }; - } - if ordinary_condition_row_count == 0 - && negative_sentinel_row_count != record.standalone_condition_rows.len() - { - return Some(ImportBlocker::MissingConditionContext); - } - - if record.negative_sentinel_scope.is_none() { - return Some(ImportBlocker::MissingConditionContext); - } - - None -} - -fn lowered_condition_true_company_target( - record: &SmpLoadedPackedEventRecordSummary, -) -> Result, ImportBlocker> { - if !record_uses_condition_true_company(record) { - return Ok(None); - } - let scope = record - .negative_sentinel_scope - .as_ref() - .ok_or(ImportBlocker::MissingConditionContext)?; - match scope.company_test_scope { - RuntimeCompanyConditionTestScope::Disabled => { - Err(ImportBlocker::CompanyConditionScopeDisabled) - } - RuntimeCompanyConditionTestScope::AllCompanies => Ok(Some(RuntimeCompanyTarget::AllActive)), - RuntimeCompanyConditionTestScope::SelectedCompanyOnly => { - Ok(Some(RuntimeCompanyTarget::SelectedCompany)) - } - RuntimeCompanyConditionTestScope::AiCompaniesOnly => { - Ok(Some(RuntimeCompanyTarget::AiCompanies)) - } - RuntimeCompanyConditionTestScope::HumanCompaniesOnly => { - Ok(Some(RuntimeCompanyTarget::HumanCompanies)) - } - } -} - -fn lowered_condition_true_player_target( - record: &SmpLoadedPackedEventRecordSummary, -) -> Result, ImportBlocker> { - if !record_uses_condition_true_player(record) { - return Ok(None); - } - let scope = record - .negative_sentinel_scope - .as_ref() - .ok_or(ImportBlocker::MissingPlayerConditionContext)?; - match scope.player_test_scope { - RuntimePlayerConditionTestScope::Disabled => { - Err(ImportBlocker::MissingPlayerConditionContext) - } - RuntimePlayerConditionTestScope::AllPlayers => Ok(Some(RuntimePlayerTarget::AllActive)), - RuntimePlayerConditionTestScope::SelectedPlayerOnly => { - Ok(Some(RuntimePlayerTarget::SelectedPlayer)) - } - RuntimePlayerConditionTestScope::AiPlayersOnly => Ok(Some(RuntimePlayerTarget::AiPlayers)), - RuntimePlayerConditionTestScope::HumanPlayersOnly => { - Ok(Some(RuntimePlayerTarget::HumanPlayers)) - } - } -} - -fn lower_condition_targets_in_effect( - effect: &RuntimeEffect, - lowered_company_target: Option<&RuntimeCompanyTarget>, - lowered_player_target: Option<&RuntimePlayerTarget>, -) -> Result { - Ok(match effect { - RuntimeEffect::SetWorldFlag { key, value } => RuntimeEffect::SetWorldFlag { - key: key.clone(), - value: *value, - }, - RuntimeEffect::SetWorldScalarOverride { key, value } => { - RuntimeEffect::SetWorldScalarOverride { - key: key.clone(), - value: *value, - } - } - RuntimeEffect::SetWorldVariable { index, value } => RuntimeEffect::SetWorldVariable { - index: *index, - value: *value, - }, - RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { - RuntimeEffect::SetLimitedTrackBuildingAmount { value: *value } - } - RuntimeEffect::SetEconomicStatusCode { value } => { - RuntimeEffect::SetEconomicStatusCode { value: *value } - } - RuntimeEffect::SetCompanyVariable { - target, - index, - value, - } => RuntimeEffect::SetCompanyVariable { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - index: *index, - value: *value, - }, - RuntimeEffect::SetCompanyCash { target, value } => RuntimeEffect::SetCompanyCash { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - value: *value, - }, - RuntimeEffect::SetPlayerVariable { - target, - index, - value, - } => RuntimeEffect::SetPlayerVariable { - target: lower_condition_true_player_target_in_player_target( - target, - lowered_player_target, - )?, - index: *index, - value: *value, - }, - RuntimeEffect::SetPlayerCash { target, value } => RuntimeEffect::SetPlayerCash { - target: lower_condition_true_player_target_in_player_target( - target, - lowered_player_target, - )?, - value: *value, - }, - RuntimeEffect::SetCompanyGovernanceScalar { - target, - metric, - value, - } => RuntimeEffect::SetCompanyGovernanceScalar { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - metric: *metric, - value: *value, - }, - RuntimeEffect::SetChairmanCash { target, value } => RuntimeEffect::SetChairmanCash { - target: target.clone(), - value: *value, - }, - RuntimeEffect::DeactivatePlayer { target } => RuntimeEffect::DeactivatePlayer { - target: lower_condition_true_player_target_in_player_target( - target, - lowered_player_target, - )?, - }, - RuntimeEffect::DeactivateChairman { target } => RuntimeEffect::DeactivateChairman { - target: target.clone(), - }, - RuntimeEffect::SetCompanyTerritoryAccess { - target, - territory, - value, - } => RuntimeEffect::SetCompanyTerritoryAccess { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - territory: territory.clone(), - value: *value, - }, - RuntimeEffect::ConfiscateCompanyAssets { target } => { - RuntimeEffect::ConfiscateCompanyAssets { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - } - } - RuntimeEffect::DeactivateCompany { target } => RuntimeEffect::DeactivateCompany { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - }, - RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { - RuntimeEffect::SetCompanyTrackLayingCapacity { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - value: *value, - } - } - RuntimeEffect::RetireTrains { - company_target, - territory_target, - locomotive_name, - } => RuntimeEffect::RetireTrains { - company_target: company_target - .as_ref() - .map(|target| { - lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - ) - }) - .transpose()?, - territory_target: territory_target.clone(), - locomotive_name: locomotive_name.clone(), - }, - RuntimeEffect::AdjustCompanyCash { target, delta } => RuntimeEffect::AdjustCompanyCash { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - delta: *delta, - }, - RuntimeEffect::AdjustCompanyDebt { target, delta } => RuntimeEffect::AdjustCompanyDebt { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - delta: *delta, - }, - RuntimeEffect::SetCandidateAvailability { name, value } => { - RuntimeEffect::SetCandidateAvailability { - name: name.clone(), - value: *value, - } - } - RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => { - RuntimeEffect::SetNamedLocomotiveAvailability { - name: name.clone(), - value: *value, - } - } - RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => { - RuntimeEffect::SetNamedLocomotiveAvailabilityValue { - name: name.clone(), - value: *value, - } - } - RuntimeEffect::SetNamedLocomotiveCost { name, value } => { - RuntimeEffect::SetNamedLocomotiveCost { - name: name.clone(), - value: *value, - } - } - RuntimeEffect::SetCargoPriceOverride { target, value } => { - RuntimeEffect::SetCargoPriceOverride { - target: target.clone(), - value: *value, - } - } - RuntimeEffect::SetTerritoryVariable { - target, - index, - value, - } => RuntimeEffect::SetTerritoryVariable { - target: target.clone(), - index: *index, - value: *value, - }, - RuntimeEffect::SetCargoProductionOverride { target, value } => { - RuntimeEffect::SetCargoProductionOverride { - target: target.clone(), - value: *value, - } - } - RuntimeEffect::SetCargoProductionSlot { slot, value } => { - RuntimeEffect::SetCargoProductionSlot { - slot: *slot, - value: *value, - } - } - RuntimeEffect::SetTerritoryAccessCost { value } => { - RuntimeEffect::SetTerritoryAccessCost { value: *value } - } - RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition { - label: label.clone(), - value: *value, - }, - RuntimeEffect::AppendEventRecord { record } => RuntimeEffect::AppendEventRecord { - record: Box::new(RuntimeEventRecordTemplate { - record_id: record.record_id, - trigger_kind: record.trigger_kind, - active: record.active, - marks_collection_dirty: record.marks_collection_dirty, - one_shot: record.one_shot, - conditions: record.conditions.clone(), - effects: record - .effects - .iter() - .map(|nested| { - lower_condition_targets_in_effect( - nested, - lowered_company_target, - lowered_player_target, - ) - }) - .collect::, _>>()?, - }), - }, - RuntimeEffect::ActivateEventRecord { record_id } => RuntimeEffect::ActivateEventRecord { - record_id: *record_id, - }, - RuntimeEffect::DeactivateEventRecord { record_id } => { - RuntimeEffect::DeactivateEventRecord { - record_id: *record_id, - } - } - RuntimeEffect::RemoveEventRecord { record_id } => RuntimeEffect::RemoveEventRecord { - record_id: *record_id, - }, - }) -} - -fn lower_condition_targets_in_condition( - condition: &RuntimeCondition, - row: &SmpLoadedPackedEventConditionRowSummary, - lowered_company_target: Option<&RuntimeCompanyTarget>, - lowered_player_target: Option<&RuntimePlayerTarget>, - company_context: &ImportRuntimeContext, -) -> Result { - Ok(match condition { - RuntimeCondition::WorldVariableThreshold { - index, - comparator, - value, - } => RuntimeCondition::WorldVariableThreshold { - index: *index, - comparator: *comparator, - value: *value, - }, - RuntimeCondition::CompanyNumericThreshold { - target, - metric, - comparator, - value, - } => RuntimeCondition::CompanyNumericThreshold { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - metric: *metric, - comparator: *comparator, - value: *value, - }, - RuntimeCondition::CompanyVariableThreshold { - target, - index, - comparator, - value, - } => RuntimeCondition::CompanyVariableThreshold { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - index: *index, - comparator: *comparator, - value: *value, - }, - RuntimeCondition::ChairmanNumericThreshold { - target, - metric, - comparator, - value, - } => RuntimeCondition::ChairmanNumericThreshold { - target: target.clone(), - metric: *metric, - comparator: *comparator, - value: *value, - }, - RuntimeCondition::PlayerVariableThreshold { - target, - index, - comparator, - value, - } => RuntimeCondition::PlayerVariableThreshold { - target: lower_condition_true_player_target_in_player_target( - target, - lowered_player_target, - )?, - index: *index, - comparator: *comparator, - value: *value, - }, - RuntimeCondition::TerritoryNumericThreshold { - target, - metric, - comparator, - value, - } => RuntimeCondition::TerritoryNumericThreshold { - target: lower_territory_target_in_condition(target, row, company_context)?, - metric: *metric, - comparator: *comparator, - value: *value, - }, - RuntimeCondition::TerritoryVariableThreshold { - target, - index, - comparator, - value, - } => RuntimeCondition::TerritoryVariableThreshold { - target: lower_territory_target_in_condition(target, row, company_context)?, - index: *index, - comparator: *comparator, - value: *value, - }, - RuntimeCondition::CompanyTerritoryNumericThreshold { - target, - territory, - metric, - comparator, - value, - } => RuntimeCondition::CompanyTerritoryNumericThreshold { - target: lower_condition_true_company_target_in_company_target( - target, - lowered_company_target, - )?, - territory: lower_territory_target_in_condition(territory, row, company_context)?, - metric: *metric, - comparator: *comparator, - value: *value, - }, - RuntimeCondition::SpecialConditionThreshold { - label, - comparator, - value, - } => RuntimeCondition::SpecialConditionThreshold { - label: label.clone(), - comparator: *comparator, - value: *value, - }, - RuntimeCondition::CandidateAvailabilityThreshold { - name, - comparator, - value, - } => RuntimeCondition::CandidateAvailabilityThreshold { - name: name.clone(), - comparator: *comparator, - value: *value, - }, - RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name, - comparator, - value, - } => RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name: name.clone(), - comparator: *comparator, - value: *value, - }, - RuntimeCondition::NamedLocomotiveCostThreshold { - name, - comparator, - value, - } => RuntimeCondition::NamedLocomotiveCostThreshold { - name: name.clone(), - comparator: *comparator, - value: *value, - }, - RuntimeCondition::CargoProductionSlotThreshold { - slot, - label, - comparator, - value, - } => RuntimeCondition::CargoProductionSlotThreshold { - slot: *slot, - label: label.clone(), - comparator: *comparator, - value: *value, - }, - RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => { - RuntimeCondition::CargoProductionTotalThreshold { - comparator: *comparator, - value: *value, - } - } - RuntimeCondition::FactoryProductionTotalThreshold { comparator, value } => { - RuntimeCondition::FactoryProductionTotalThreshold { - comparator: *comparator, - value: *value, - } - } - RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value } => { - RuntimeCondition::FarmMineProductionTotalThreshold { - comparator: *comparator, - value: *value, - } - } - RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value } => { - RuntimeCondition::OtherCargoProductionTotalThreshold { - comparator: *comparator, - value: *value, - } - } - RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => { - RuntimeCondition::LimitedTrackBuildingAmountThreshold { - comparator: *comparator, - value: *value, - } - } - RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => { - RuntimeCondition::TerritoryAccessCostThreshold { - comparator: *comparator, - value: *value, - } - } - RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => { - RuntimeCondition::EconomicStatusCodeThreshold { - comparator: *comparator, - value: *value, - } - } - RuntimeCondition::WorldFlagEquals { key, value } => RuntimeCondition::WorldFlagEquals { - key: key.clone(), - value: *value, - }, - }) -} - -fn lower_condition_true_company_target_in_company_target( - target: &RuntimeCompanyTarget, - lowered_target: Option<&RuntimeCompanyTarget>, -) -> Result { - match target { - RuntimeCompanyTarget::ConditionTrueCompany => lowered_target - .cloned() - .ok_or(ImportBlocker::MissingConditionContext), - _ => Ok(target.clone()), - } -} - -fn lower_condition_true_player_target_in_player_target( - target: &RuntimePlayerTarget, - lowered_target: Option<&RuntimePlayerTarget>, -) -> Result { - match target { - RuntimePlayerTarget::ConditionTruePlayer => lowered_target - .cloned() - .ok_or(ImportBlocker::MissingPlayerConditionContext), - _ => Ok(target.clone()), - } -} - -fn ensure_condition_true_chairman_context( - record: &SmpLoadedPackedEventRecordSummary, -) -> Result<(), ImportBlocker> { - if !record_uses_condition_true_chairman(record) { - return Ok(()); - } - if record - .decoded_conditions - .iter() - .any(|condition| matches!(condition, RuntimeCondition::ChairmanNumericThreshold { .. })) - { - Ok(()) - } else { - Err(ImportBlocker::MissingConditionContext) - } -} - -fn lower_territory_target_in_condition( - target: &RuntimeTerritoryTarget, - row: &SmpLoadedPackedEventConditionRowSummary, - company_context: &ImportRuntimeContext, -) -> Result { - if !company_context.has_territory_context { - return Err(ImportBlocker::MissingTerritoryContext); - } - if !row.requires_candidate_name_binding { - return Ok(target.clone()); - } - let candidate_name = row - .candidate_name - .as_ref() - .ok_or(ImportBlocker::NamedTerritoryBinding)?; - let territory_id = company_context - .territory_name_to_id - .get(candidate_name) - .copied() - .ok_or(ImportBlocker::NamedTerritoryBinding)?; - Ok(RuntimeTerritoryTarget::Ids { - ids: vec![territory_id], - }) -} - -fn record_uses_condition_true_company(record: &SmpLoadedPackedEventRecordSummary) -> bool { - record - .decoded_conditions - .iter() - .any(condition_uses_condition_true_company) - || record - .decoded_actions - .iter() - .any(runtime_effect_uses_condition_true_company) -} - -fn record_uses_condition_true_player(record: &SmpLoadedPackedEventRecordSummary) -> bool { - record - .decoded_actions - .iter() - .any(runtime_effect_uses_condition_true_player) -} - -fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool { - match condition { - RuntimeCondition::CompanyNumericThreshold { target, .. } - | RuntimeCondition::CompanyVariableThreshold { target, .. } - | RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => { - matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) - } - RuntimeCondition::PlayerVariableThreshold { target, .. } => { - matches!(target, RuntimePlayerTarget::ConditionTruePlayer) - } - RuntimeCondition::ChairmanNumericThreshold { .. } => false, - RuntimeCondition::TerritoryNumericThreshold { .. } - | RuntimeCondition::TerritoryVariableThreshold { .. } - | RuntimeCondition::WorldVariableThreshold { .. } - | RuntimeCondition::SpecialConditionThreshold { .. } - | RuntimeCondition::CandidateAvailabilityThreshold { .. } - | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } - | RuntimeCondition::NamedLocomotiveCostThreshold { .. } - | RuntimeCondition::CargoProductionSlotThreshold { .. } - | RuntimeCondition::CargoProductionTotalThreshold { .. } - | RuntimeCondition::FactoryProductionTotalThreshold { .. } - | RuntimeCondition::FarmMineProductionTotalThreshold { .. } - | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } - | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } - | RuntimeCondition::TerritoryAccessCostThreshold { .. } - | RuntimeCondition::EconomicStatusCodeThreshold { .. } - | RuntimeCondition::WorldFlagEquals { .. } => false, - } -} - -fn chairman_target_import_blocker( - target: &RuntimeChairmanTarget, - company_context: &ImportRuntimeContext, -) -> Option { - match target { - RuntimeChairmanTarget::AllActive => { - if company_context.known_chairman_profile_ids.is_empty() { - Some(ImportBlocker::MissingChairmanContext) - } else { - None - } - } - RuntimeChairmanTarget::HumanChairmen | RuntimeChairmanTarget::AiChairmen => { - if company_context.known_chairman_profile_ids.is_empty() { - Some(ImportBlocker::MissingChairmanContext) - } else if !company_context.has_complete_company_controller_context { - Some(ImportBlocker::MissingCompanyRoleContext) - } else { - None - } - } - RuntimeChairmanTarget::SelectedChairman => { - if company_context.selected_chairman_profile_id.is_some() { - None - } else { - Some(ImportBlocker::MissingChairmanContext) - } - } - RuntimeChairmanTarget::ConditionTrueChairman => { - if company_context.known_chairman_profile_ids.is_empty() { - Some(ImportBlocker::MissingChairmanContext) - } else { - None - } - } - RuntimeChairmanTarget::Ids { ids } => { - if company_context.known_chairman_profile_ids.is_empty() { - Some(ImportBlocker::MissingChairmanContext) - } else if ids - .iter() - .all(|id| company_context.known_chairman_profile_ids.contains(id)) - { - None - } else { - Some(ImportBlocker::MissingChairmanContext) - } - } - } -} - -fn smp_runtime_effects_to_runtime_effects( - effects: &[RuntimeEffect], - company_context: &ImportRuntimeContext, - allow_condition_true_company: bool, - allow_condition_true_player: bool, -) -> Result, String> { - effects - .iter() - .map(|effect| { - smp_runtime_effect_to_runtime_effect( - effect, - company_context, - allow_condition_true_company, - allow_condition_true_player, - ) - }) - .collect() -} - -fn smp_runtime_effect_to_runtime_effect( - effect: &RuntimeEffect, - company_context: &ImportRuntimeContext, - allow_condition_true_company: bool, - allow_condition_true_player: bool, -) -> Result { - match effect { - RuntimeEffect::SetWorldFlag { key, value } => Ok(RuntimeEffect::SetWorldFlag { - key: key.clone(), - value: *value, - }), - RuntimeEffect::SetWorldVariable { index, value } => Ok(RuntimeEffect::SetWorldVariable { - index: *index, - value: *value, - }), - RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { - Ok(RuntimeEffect::SetLimitedTrackBuildingAmount { value: *value }) - } - RuntimeEffect::SetEconomicStatusCode { value } => { - Ok(RuntimeEffect::SetEconomicStatusCode { value: *value }) - } - RuntimeEffect::SetCompanyVariable { - target, - index, - value, - } => { - if company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) { - Ok(RuntimeEffect::SetCompanyVariable { - target: target.clone(), - index: *index, - value: *value, - }) - } else { - Err(company_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::SetCompanyCash { target, value } => { - if company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) { - Ok(RuntimeEffect::SetCompanyCash { - target: target.clone(), - value: *value, - }) - } else { - Err(company_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::SetPlayerVariable { - target, - index, - value, - } => { - if player_target_allowed_for_import( - target, - company_context, - allow_condition_true_player, - ) { - Ok(RuntimeEffect::SetPlayerVariable { - target: target.clone(), - index: *index, - value: *value, - }) - } else { - Err(player_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::SetPlayerCash { target, value } => { - if player_target_allowed_for_import( - target, - company_context, - allow_condition_true_player, - ) { - Ok(RuntimeEffect::SetPlayerCash { - target: target.clone(), - value: *value, - }) - } else { - Err(player_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::SetTerritoryVariable { - target, - index, - value, - } => { - if territory_target_import_blocker(target, company_context).is_none() { - Ok(RuntimeEffect::SetTerritoryVariable { - target: target.clone(), - index: *index, - value: *value, - }) - } else { - Err("packed effect requires territory runtime context".to_string()) - } - } - RuntimeEffect::SetChairmanCash { target, value } => { - if chairman_target_import_blocker(target, company_context).is_none() { - Ok(RuntimeEffect::SetChairmanCash { - target: target.clone(), - value: *value, - }) - } else { - Err("packed effect requires chairman runtime context".to_string()) - } - } - RuntimeEffect::DeactivatePlayer { target } => { - if player_target_allowed_for_import( - target, - company_context, - allow_condition_true_player, - ) { - Ok(RuntimeEffect::DeactivatePlayer { - target: target.clone(), - }) - } else { - Err(player_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::DeactivateChairman { target } => { - if chairman_target_import_blocker(target, company_context).is_none() { - Ok(RuntimeEffect::DeactivateChairman { - target: target.clone(), - }) - } else { - Err("packed effect requires chairman runtime context".to_string()) - } - } - RuntimeEffect::SetCompanyTerritoryAccess { - target, - territory, - value, - } => { - if !company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) { - Err(company_target_import_error_message(target, company_context)) - } else if territory_target_import_blocker(territory, company_context).is_some() { - Err("packed effect requires territory runtime context".to_string()) - } else { - Ok(RuntimeEffect::SetCompanyTerritoryAccess { - target: target.clone(), - territory: territory.clone(), - value: *value, - }) - } - } - RuntimeEffect::ConfiscateCompanyAssets { target } => { - if company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) && company_context.has_train_context - { - Ok(RuntimeEffect::ConfiscateCompanyAssets { - target: target.clone(), - }) - } else if !company_context.has_train_context { - Err("packed effect requires runtime train context".to_string()) - } else { - Err(company_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::DeactivateCompany { target } => { - if company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) { - Ok(RuntimeEffect::DeactivateCompany { - target: target.clone(), - }) - } else { - Err(company_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { - if company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) { - Ok(RuntimeEffect::SetCompanyTrackLayingCapacity { - target: target.clone(), - value: *value, - }) - } else { - Err(company_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::SetCompanyGovernanceScalar { - target, - metric, - value, - } => { - if company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) { - Ok(RuntimeEffect::SetCompanyGovernanceScalar { - target: target.clone(), - metric: *metric, - value: *value, - }) - } else { - Err(company_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::RetireTrains { - company_target, - territory_target, - locomotive_name, - } => { - if !company_context.has_train_context { - Err("packed effect requires runtime train context".to_string()) - } else if territory_target.is_some() && !company_context.has_train_territory_context { - Err("packed train effect requires runtime train territory context".to_string()) - } else if let Some(company_target) = company_target { - if !company_target_allowed_for_import( - company_target, - company_context, - allow_condition_true_company, - ) { - Err(company_target_import_error_message( - company_target, - company_context, - )) - } else if let Some(territory_target) = territory_target { - if territory_target_import_blocker(territory_target, company_context).is_some() - { - Err("packed condition requires territory runtime context".to_string()) - } else { - Ok(RuntimeEffect::RetireTrains { - company_target: Some(company_target.clone()), - territory_target: Some(territory_target.clone()), - locomotive_name: locomotive_name.clone(), - }) - } - } else { - Ok(RuntimeEffect::RetireTrains { - company_target: Some(company_target.clone()), - territory_target: None, - locomotive_name: locomotive_name.clone(), - }) - } - } else if let Some(territory_target) = territory_target { - if territory_target_import_blocker(territory_target, company_context).is_some() { - Err("packed condition requires territory runtime context".to_string()) - } else { - Ok(RuntimeEffect::RetireTrains { - company_target: None, - territory_target: Some(territory_target.clone()), - locomotive_name: locomotive_name.clone(), - }) - } - } else { - Ok(RuntimeEffect::RetireTrains { - company_target: None, - territory_target: None, - locomotive_name: locomotive_name.clone(), - }) - } - } - RuntimeEffect::AdjustCompanyCash { target, delta } => { - if company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) { - Ok(RuntimeEffect::AdjustCompanyCash { - target: target.clone(), - delta: *delta, - }) - } else { - Err(company_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::AdjustCompanyDebt { target, delta } => { - if company_target_allowed_for_import( - target, - company_context, - allow_condition_true_company, - ) { - Ok(RuntimeEffect::AdjustCompanyDebt { - target: target.clone(), - delta: *delta, - }) - } else { - Err(company_target_import_error_message(target, company_context)) - } - } - RuntimeEffect::SetCandidateAvailability { name, value } => { - Ok(RuntimeEffect::SetCandidateAvailability { - name: name.clone(), - value: *value, - }) - } - RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => { - Ok(RuntimeEffect::SetNamedLocomotiveAvailability { - name: name.clone(), - value: *value, - }) - } - RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => { - Ok(RuntimeEffect::SetNamedLocomotiveAvailabilityValue { - name: name.clone(), - value: *value, - }) - } - RuntimeEffect::SetNamedLocomotiveCost { name, value } => { - Ok(RuntimeEffect::SetNamedLocomotiveCost { - name: name.clone(), - value: *value, - }) - } - RuntimeEffect::SetCargoPriceOverride { target, value } => { - Ok(RuntimeEffect::SetCargoPriceOverride { - target: target.clone(), - value: *value, - }) - } - RuntimeEffect::SetCargoProductionOverride { target, value } => { - Ok(RuntimeEffect::SetCargoProductionOverride { - target: target.clone(), - value: *value, - }) - } - RuntimeEffect::SetWorldScalarOverride { key, value } => { - Ok(RuntimeEffect::SetWorldScalarOverride { - key: key.clone(), - value: *value, - }) - } - RuntimeEffect::SetCargoProductionSlot { slot, value } => { - Ok(RuntimeEffect::SetCargoProductionSlot { - slot: *slot, - value: *value, - }) - } - RuntimeEffect::SetTerritoryAccessCost { value } => { - Ok(RuntimeEffect::SetTerritoryAccessCost { value: *value }) - } - RuntimeEffect::SetSpecialCondition { label, value } => { - Ok(RuntimeEffect::SetSpecialCondition { - label: label.clone(), - value: *value, - }) - } - RuntimeEffect::AppendEventRecord { record } => Ok(RuntimeEffect::AppendEventRecord { - record: Box::new(smp_runtime_record_template_to_runtime( - record, - company_context, - allow_condition_true_company, - allow_condition_true_player, - )?), - }), - RuntimeEffect::ActivateEventRecord { record_id } => { - Ok(RuntimeEffect::ActivateEventRecord { - record_id: *record_id, - }) - } - RuntimeEffect::DeactivateEventRecord { record_id } => { - Ok(RuntimeEffect::DeactivateEventRecord { - record_id: *record_id, - }) - } - RuntimeEffect::RemoveEventRecord { record_id } => Ok(RuntimeEffect::RemoveEventRecord { - record_id: *record_id, - }), - } -} - -fn smp_runtime_record_template_to_runtime( - record: &RuntimeEventRecordTemplate, - company_context: &ImportRuntimeContext, - allow_condition_true_company: bool, - allow_condition_true_player: bool, -) -> Result { - Ok(RuntimeEventRecordTemplate { - record_id: record.record_id, - trigger_kind: record.trigger_kind, - active: record.active, - marks_collection_dirty: record.marks_collection_dirty, - one_shot: record.one_shot, - conditions: record.conditions.clone(), - effects: smp_runtime_effects_to_runtime_effects( - &record.effects, - company_context, - allow_condition_true_company, - allow_condition_true_player, - )?, - }) -} - -fn company_target_allowed_for_import( - target: &RuntimeCompanyTarget, - company_context: &ImportRuntimeContext, - allow_condition_true_company: bool, -) -> bool { - match company_target_import_blocker(target, company_context) { - None => true, - Some(ImportBlocker::MissingConditionContext) - if allow_condition_true_company - && matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) => - { - true - } - Some(_) => false, - } -} - -fn player_target_allowed_for_import( - target: &RuntimePlayerTarget, - company_context: &ImportRuntimeContext, - allow_condition_true_player: bool, -) -> bool { - match player_target_import_blocker(target, company_context) { - None => true, - Some(ImportBlocker::MissingPlayerConditionContext) - if allow_condition_true_player - && matches!(target, RuntimePlayerTarget::ConditionTruePlayer) => - { - true - } - Some(_) => false, - } -} - -fn conditions_provide_company_context(conditions: &[RuntimeCondition]) -> bool { - conditions.iter().any(|condition| { - matches!( - condition, - RuntimeCondition::CompanyNumericThreshold { .. } - | RuntimeCondition::CompanyVariableThreshold { .. } - | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } - ) - }) -} - -fn company_target_import_blocker( - target: &RuntimeCompanyTarget, - company_context: &ImportRuntimeContext, -) -> Option { - match target { - RuntimeCompanyTarget::AllActive => None, - RuntimeCompanyTarget::Ids { ids } => { - if ids.is_empty() - || ids - .iter() - .any(|company_id| !company_context.known_company_ids.contains(company_id)) - { - Some(ImportBlocker::MissingCompanyContext) - } else { - None - } - } - RuntimeCompanyTarget::HumanCompanies | RuntimeCompanyTarget::AiCompanies => { - if !company_context.has_complete_company_controller_context { - Some(ImportBlocker::MissingCompanyRoleContext) - } else { - None - } - } - RuntimeCompanyTarget::SelectedCompany => { - if company_context.selected_company_id.is_some() { - None - } else { - Some(ImportBlocker::MissingSelectionContext) - } - } - RuntimeCompanyTarget::ConditionTrueCompany => Some(ImportBlocker::MissingConditionContext), - } -} - -fn company_target_import_error_message( - target: &RuntimeCompanyTarget, - company_context: &ImportRuntimeContext, -) -> String { - match company_target_import_blocker(target, company_context) { - Some(ImportBlocker::MissingCompanyContext) => { - "packed company effect requires resolved company ids".to_string() - } - Some(ImportBlocker::MissingSelectionContext) => { - "packed company effect requires selected_company_id context".to_string() - } - Some(ImportBlocker::MissingCompanyRoleContext) => { - "packed company effect requires company controller role context".to_string() - } - Some(ImportBlocker::MissingConditionContext) => { - "packed company effect requires condition-relative context".to_string() - } - Some(ImportBlocker::CompanyConditionScopeDisabled) => { - "packed company effect disables company-side negative-sentinel condition scope" - .to_string() - } - Some(ImportBlocker::MissingTerritoryContext) => { - "packed condition requires territory runtime context".to_string() - } - Some(ImportBlocker::NamedTerritoryBinding) => { - "packed condition requires named territory binding".to_string() - } - Some(ImportBlocker::EvidenceBlockedDescriptor) => { - "packed descriptor is still evidence-blocked".to_string() - } - Some(ImportBlocker::UnmappedOrdinaryCondition) => { - "packed ordinary condition is not yet mapped".to_string() - } - Some(ImportBlocker::UnmappedWorldCondition) => { - "packed whole-game condition is not yet mapped".to_string() - } - Some(ImportBlocker::MissingTrainContext) => { - "packed effect requires runtime train context".to_string() - } - Some(ImportBlocker::MissingTrainTerritoryContext) => { - "packed train effect requires runtime train territory context".to_string() - } - Some(ImportBlocker::MissingLocomotiveCatalogContext) => { - "packed locomotive availability row requires locomotive catalog context".to_string() - } - Some(ImportBlocker::MissingPlayerContext) - | Some(ImportBlocker::MissingPlayerSelectionContext) - | Some(ImportBlocker::MissingPlayerRoleContext) - | Some(ImportBlocker::MissingChairmanContext) - | Some(ImportBlocker::ChairmanTargetScope) - | Some(ImportBlocker::MissingPlayerConditionContext) => { - "packed company effect is blocked by non-company import context".to_string() - } - None => "packed company effect is importable".to_string(), - } -} - -fn player_target_import_blocker( - target: &RuntimePlayerTarget, - company_context: &ImportRuntimeContext, -) -> Option { - match target { - RuntimePlayerTarget::AllActive => { - if company_context.known_player_ids.is_empty() { - Some(ImportBlocker::MissingPlayerContext) - } else { - None - } - } - RuntimePlayerTarget::Ids { ids } => { - if ids.is_empty() - || ids - .iter() - .any(|player_id| !company_context.known_player_ids.contains(player_id)) - { - Some(ImportBlocker::MissingPlayerContext) - } else { - None - } - } - RuntimePlayerTarget::HumanPlayers | RuntimePlayerTarget::AiPlayers => { - if !company_context.has_complete_player_controller_context { - Some(ImportBlocker::MissingPlayerRoleContext) - } else { - None - } - } - RuntimePlayerTarget::SelectedPlayer => { - if company_context.selected_player_id.is_some() { - None - } else { - Some(ImportBlocker::MissingPlayerSelectionContext) - } - } - RuntimePlayerTarget::ConditionTruePlayer => { - Some(ImportBlocker::MissingPlayerConditionContext) - } - } -} - -fn player_target_import_error_message( - target: &RuntimePlayerTarget, - company_context: &ImportRuntimeContext, -) -> String { - match player_target_import_blocker(target, company_context) { - Some(ImportBlocker::MissingPlayerContext) => { - "packed player effect requires resolved player ids".to_string() - } - Some(ImportBlocker::MissingPlayerSelectionContext) => { - "packed player effect requires selected_player_id context".to_string() - } - Some(ImportBlocker::MissingPlayerRoleContext) => { - "packed player effect requires player controller role context".to_string() - } - Some(ImportBlocker::MissingPlayerConditionContext) => { - "packed player effect requires player condition-relative context".to_string() - } - _ => "packed player effect is importable".to_string(), - } -} - -fn determine_packed_event_import_outcome( - record: &SmpLoadedPackedEventRecordSummary, - company_context: &ImportRuntimeContext, - imported: bool, -) -> String { - if imported { - return "imported".to_string(); - } - if record.decode_status == "unsupported_framing" { - return "blocked_unsupported_decode".to_string(); - } - if record.payload_family == "real_packed_v1" { - if record.compact_control.is_none() { - return "blocked_missing_compact_control".to_string(); - } - if !record.executable_import_ready { - if let Err(blocker) = lowered_record_decoded_actions(record, company_context) { - if matches!( - blocker, - ImportBlocker::MissingLocomotiveCatalogContext - | ImportBlocker::MissingChairmanContext - | ImportBlocker::ChairmanTargetScope - ) { - return company_target_import_outcome(blocker).to_string(); - } - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_unsupported_chairman_target_scope) - { - return "blocked_chairman_target_scope".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_unsupported_territory_access_scope) - { - return "blocked_territory_access_scope".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_unsupported_territory_access_variant) - { - return "blocked_territory_access_variant".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_unsupported_confiscation_variant) - { - return "blocked_confiscation_variant".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_unsupported_retire_train_scope) - { - return "blocked_retire_train_scope".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_unsupported_retire_train_variant) - { - return "blocked_retire_train_variant".to_string(); - } - if record - .standalone_condition_rows - .iter() - .any(|row| row.raw_condition_id >= 0) - { - if record_has_world_state_condition_rows(record) { - return "blocked_unmapped_world_condition".to_string(); - } else { - return "blocked_unmapped_ordinary_condition".to_string(); - } - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_shell_owned_descriptor_family) - { - return "blocked_shell_owned_descriptor".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_evidence_blocked_descriptor_family) - { - return "blocked_evidence_blocked_descriptor".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_variant_or_scope_blocked_descriptor_family) - { - return "blocked_variant_or_scope_blocked_descriptor".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_unsupported_executable_descriptor_variant) - { - return "blocked_variant_or_scope_blocked_descriptor".to_string(); - } - if record - .grouped_effect_rows - .iter() - .any(real_grouped_row_is_world_state_family) - { - return "blocked_unmapped_world_descriptor".to_string(); - } - return "blocked_unmapped_real_descriptor".to_string(); - } - if let Some(blocker) = packed_record_condition_scope_import_blocker(record, company_context) - { - return company_target_import_outcome(blocker).to_string(); - } - if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) - { - return company_target_import_outcome(blocker).to_string(); - } - return "blocked_unsupported_decode".to_string(); - } - if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) { - return company_target_import_outcome(blocker).to_string(); - } - "blocked_unsupported_decode".to_string() -} - -fn record_has_world_state_condition_rows(record: &SmpLoadedPackedEventRecordSummary) -> bool { - record - .decoded_conditions - .iter() - .any(runtime_condition_is_world_state) - || record - .standalone_condition_rows - .iter() - .any(ordinary_condition_row_is_world_state_family) -} - -fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool { - matches!( - condition, - RuntimeCondition::WorldVariableThreshold { .. } - | RuntimeCondition::SpecialConditionThreshold { .. } - | RuntimeCondition::CandidateAvailabilityThreshold { .. } - | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } - | RuntimeCondition::NamedLocomotiveCostThreshold { .. } - | RuntimeCondition::CargoProductionSlotThreshold { .. } - | RuntimeCondition::CargoProductionTotalThreshold { .. } - | RuntimeCondition::FactoryProductionTotalThreshold { .. } - | RuntimeCondition::FarmMineProductionTotalThreshold { .. } - | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } - | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } - | RuntimeCondition::TerritoryAccessCostThreshold { .. } - | RuntimeCondition::EconomicStatusCodeThreshold { .. } - | RuntimeCondition::WorldFlagEquals { .. } - ) -} - -fn ordinary_condition_row_is_world_state_family( - row: &SmpLoadedPackedEventConditionRowSummary, -) -> bool { - row.metric.as_deref().is_some_and(|metric| { - metric.contains("Special Condition") - || metric.contains("Candidate Availability") - || metric.contains("Named Locomotive") - || metric.contains("Cargo Production") - || metric.contains("Factory Production") - || metric.contains("Farm/Mine Production") - || metric.contains("Other Cargo Production") - || metric.contains("Limited Track Building Amount") - || metric.contains("Territory Access Cost") - || metric.contains("Economic Status") - || metric.contains("World Flag") - }) || row.semantic_family.as_deref().is_some_and(|family| { - matches!( - family, - "world_state_threshold" | "world_scalar_threshold" | "world_flag_equals" - ) - }) -} - -fn real_grouped_row_is_world_state_family( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - row.target_mask_bits == Some(0x08) - || row.parameter_family.as_deref().is_some_and(|family| { - family.starts_with("whole_game_") - || family.starts_with("special_condition") - || family.starts_with("candidate_availability") - || family.starts_with("world_flag") - }) - || row.descriptor_label.as_deref().is_some_and(|label| { - label.contains("Special Condition") - || label.contains("Candidate Availability") - || label.contains("World Flag") - || label == "Economic Status" - }) -} - -fn real_grouped_row_is_shell_owned_descriptor_family( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - real_grouped_row_has_runtime_status(row, "shell_owned") -} - -fn real_grouped_row_is_evidence_blocked_descriptor_family( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - real_grouped_row_has_runtime_status(row, "evidence_blocked") -} - -fn real_grouped_row_is_variant_or_scope_blocked_descriptor_family( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - real_grouped_row_has_runtime_status(row, "variant_or_scope_blocked") -} - -fn real_grouped_row_has_runtime_status( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, - status: &str, -) -> bool { - crate::smp::grouped_effect_descriptor_runtime_status_name(row.descriptor_id) - .is_some_and(|runtime_status| runtime_status == status) -} - -fn real_grouped_row_is_unsupported_executable_descriptor_variant( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - if !real_grouped_row_has_runtime_status(row, "executable") { - return false; - } - match row.descriptor_id { - 1 => !(row.opcode == 8 && row.row_shape == "multivalue_scalar"), - 2 => !(row.opcode == 8 && row.row_shape == "multivalue_scalar"), - 8 | 108 | 109 | 122 => row.row_shape != "scalar_assignment", - 13 | 14 => !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0), - 56 | 57 => row.row_shape != "scalar_assignment", - _ => match row.parameter_family.as_deref() { - Some("world_scalar_override") => row.row_shape != "scalar_assignment", - Some("runtime_variable_scalar") => row.row_shape != "scalar_assignment", - Some("locomotive_availability_scalar") => { - !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) - } - Some("locomotive_cost_scalar") => { - !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) - } - Some("cargo_price_scalar") => { - !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) - } - Some("cargo_production_scalar") => { - matches!(row.descriptor_id, 177 | 178 | 179 | 230..=240) - && !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) - } - Some("territory_access_cost_scalar") => { - !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) - } - _ => false, - }, - } -} - -fn packed_record_company_target_import_blocker( - record: &SmpLoadedPackedEventRecordSummary, - company_context: &ImportRuntimeContext, -) -> Option { - if record.decoded_actions.iter().any(|effect| { - runtime_effect_uses_condition_true_company(effect) - || runtime_effect_uses_condition_true_player(effect) - }) && record.negative_sentinel_scope.is_none() - { - return Some(ImportBlocker::MissingConditionContext); - } - let lowered_conditions = match lowered_record_decoded_conditions(record, company_context) { - Ok(conditions) => conditions, - Err(blocker) => return Some(blocker), - }; - if let Some(blocker) = lowered_conditions.iter().find_map(|condition| { - runtime_condition_company_target_import_blocker(condition, company_context) - }) { - return Some(blocker); - } - let lowered_effects = match lowered_record_decoded_actions(record, company_context) { - Ok(effects) => effects, - Err(blocker) => return Some(blocker), - }; - lowered_effects - .iter() - .find_map(|effect| runtime_effect_company_target_import_blocker(effect, company_context)) -} - -fn runtime_condition_company_target_import_blocker( - condition: &RuntimeCondition, - company_context: &ImportRuntimeContext, -) -> Option { - match condition { - RuntimeCondition::WorldVariableThreshold { .. } => None, - RuntimeCondition::CompanyNumericThreshold { target, .. } => { - company_target_import_blocker(target, company_context) - } - RuntimeCondition::CompanyVariableThreshold { target, .. } => { - company_target_import_blocker(target, company_context) - } - RuntimeCondition::PlayerVariableThreshold { target, .. } => { - player_target_import_blocker(target, company_context) - } - RuntimeCondition::ChairmanNumericThreshold { target, .. } => { - chairman_target_import_blocker(target, company_context) - } - RuntimeCondition::TerritoryNumericThreshold { target, .. } => { - territory_target_import_blocker(target, company_context) - } - RuntimeCondition::TerritoryVariableThreshold { target, .. } => { - territory_target_import_blocker(target, company_context) - } - RuntimeCondition::CompanyTerritoryNumericThreshold { - target, territory, .. - } => company_target_import_blocker(target, company_context) - .or_else(|| territory_target_import_blocker(territory, company_context)), - RuntimeCondition::SpecialConditionThreshold { .. } - | RuntimeCondition::CandidateAvailabilityThreshold { .. } - | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } - | RuntimeCondition::NamedLocomotiveCostThreshold { .. } - | RuntimeCondition::CargoProductionSlotThreshold { .. } - | RuntimeCondition::CargoProductionTotalThreshold { .. } - | RuntimeCondition::FactoryProductionTotalThreshold { .. } - | RuntimeCondition::FarmMineProductionTotalThreshold { .. } - | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } - | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } - | RuntimeCondition::TerritoryAccessCostThreshold { .. } - | RuntimeCondition::EconomicStatusCodeThreshold { .. } - | RuntimeCondition::WorldFlagEquals { .. } => None, - } -} - -fn territory_target_import_blocker( - target: &RuntimeTerritoryTarget, - company_context: &ImportRuntimeContext, -) -> Option { - if !company_context.has_territory_context { - return Some(ImportBlocker::MissingTerritoryContext); - } - match target { - RuntimeTerritoryTarget::AllTerritories => None, - RuntimeTerritoryTarget::Ids { ids } => { - if ids.is_empty() { - Some(ImportBlocker::NamedTerritoryBinding) - } else if !territory_ids_match_known_context(ids, company_context) { - Some(ImportBlocker::NamedTerritoryBinding) - } else { - None - } - } - } -} - -fn company_target_import_outcome(blocker: ImportBlocker) -> &'static str { - match blocker { - ImportBlocker::MissingCompanyContext => "blocked_missing_company_context", - ImportBlocker::MissingSelectionContext => "blocked_missing_selection_context", - ImportBlocker::MissingCompanyRoleContext => "blocked_missing_company_role_context", - ImportBlocker::MissingPlayerContext => "blocked_missing_player_context", - ImportBlocker::MissingPlayerSelectionContext => "blocked_missing_player_selection_context", - ImportBlocker::MissingPlayerRoleContext => "blocked_missing_player_role_context", - ImportBlocker::MissingChairmanContext => "blocked_missing_chairman_context", - ImportBlocker::ChairmanTargetScope => "blocked_chairman_target_scope", - ImportBlocker::MissingConditionContext => "blocked_missing_condition_context", - ImportBlocker::MissingPlayerConditionContext => "blocked_missing_player_condition_context", - ImportBlocker::CompanyConditionScopeDisabled => "blocked_company_condition_scope_disabled", - ImportBlocker::MissingTerritoryContext => "blocked_missing_territory_context", - ImportBlocker::NamedTerritoryBinding => "blocked_named_territory_binding", - ImportBlocker::UnmappedOrdinaryCondition => "blocked_unmapped_ordinary_condition", - ImportBlocker::UnmappedWorldCondition => "blocked_unmapped_world_condition", - ImportBlocker::EvidenceBlockedDescriptor => "blocked_evidence_blocked_descriptor", - ImportBlocker::MissingTrainContext => "blocked_missing_train_context", - ImportBlocker::MissingTrainTerritoryContext => "blocked_missing_train_territory_context", - ImportBlocker::MissingLocomotiveCatalogContext => { - "blocked_missing_locomotive_catalog_context" - } - } -} - -fn territory_ids_match_known_context(ids: &[u32], company_context: &ImportRuntimeContext) -> bool { - ids.iter() - .all(|territory_id| company_context.known_territory_ids.contains(territory_id)) -} - -fn real_grouped_row_is_unsupported_territory_access_variant( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - row.descriptor_id == 3 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0) -} - -fn real_grouped_row_is_unsupported_chairman_target_scope( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - matches!(row.grouped_target_subject.as_deref(), Some("chairman")) - && matches!(row.descriptor_id, 1 | 14) - && row.notes.iter().any(|note| { - note.starts_with("chairman row uses unsupported grouped target scope ordinal ") - }) -} - -fn real_grouped_row_is_unsupported_territory_access_scope( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - row.descriptor_id == 3 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - && row - .notes - .iter() - .any(|note| note == "territory access row is missing company or territory scope") -} - -fn real_grouped_row_is_unsupported_confiscation_variant( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - row.descriptor_id == 9 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0) -} - -fn real_grouped_row_is_unsupported_retire_train_variant( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - row.descriptor_id == 15 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0) -} - -fn real_grouped_row_is_unsupported_retire_train_scope( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, -) -> bool { - row.descriptor_id == 15 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - && row - .notes - .iter() - .any(|note| note == "retire train row is missing company and territory scope") -} - -fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { - match effect { - RuntimeEffect::SetCompanyCash { target, .. } - | RuntimeEffect::SetCompanyVariable { target, .. } - | RuntimeEffect::SetCompanyGovernanceScalar { target, .. } - | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } - | RuntimeEffect::ConfiscateCompanyAssets { target } - | RuntimeEffect::DeactivateCompany { target } - | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } - | RuntimeEffect::AdjustCompanyCash { target, .. } - | RuntimeEffect::AdjustCompanyDebt { target, .. } => { - matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) - } - RuntimeEffect::RetireTrains { company_target, .. } => matches!( - company_target, - Some(RuntimeCompanyTarget::ConditionTrueCompany) - ), - RuntimeEffect::AppendEventRecord { record } => record - .effects - .iter() - .any(runtime_effect_uses_condition_true_company), - RuntimeEffect::SetWorldFlag { .. } - | RuntimeEffect::SetWorldVariable { .. } - | RuntimeEffect::SetWorldScalarOverride { .. } - | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } - | RuntimeEffect::SetEconomicStatusCode { .. } - | RuntimeEffect::SetPlayerCash { .. } - | RuntimeEffect::SetPlayerVariable { .. } - | RuntimeEffect::SetChairmanCash { .. } - | RuntimeEffect::DeactivatePlayer { .. } - | RuntimeEffect::DeactivateChairman { .. } - | RuntimeEffect::SetCandidateAvailability { .. } - | RuntimeEffect::SetNamedLocomotiveAvailability { .. } - | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. } - | RuntimeEffect::SetNamedLocomotiveCost { .. } - | RuntimeEffect::SetCargoPriceOverride { .. } - | RuntimeEffect::SetCargoProductionOverride { .. } - | RuntimeEffect::SetCargoProductionSlot { .. } - | RuntimeEffect::SetTerritoryVariable { .. } - | RuntimeEffect::SetTerritoryAccessCost { .. } - | RuntimeEffect::SetSpecialCondition { .. } - | RuntimeEffect::ActivateEventRecord { .. } - | RuntimeEffect::DeactivateEventRecord { .. } - | RuntimeEffect::RemoveEventRecord { .. } => false, - } -} - -fn runtime_effect_uses_condition_true_player(effect: &RuntimeEffect) -> bool { - match effect { - RuntimeEffect::SetPlayerCash { target, .. } - | RuntimeEffect::SetPlayerVariable { target, .. } => { - matches!(target, RuntimePlayerTarget::ConditionTruePlayer) - } - RuntimeEffect::DeactivatePlayer { target } => { - matches!(target, RuntimePlayerTarget::ConditionTruePlayer) - } - RuntimeEffect::SetChairmanCash { .. } | RuntimeEffect::DeactivateChairman { .. } => false, - RuntimeEffect::AppendEventRecord { record } => record - .effects - .iter() - .any(runtime_effect_uses_condition_true_player), - _ => false, - } -} - -fn record_uses_condition_true_chairman(record: &SmpLoadedPackedEventRecordSummary) -> bool { - record - .decoded_actions - .iter() - .any(runtime_effect_uses_condition_true_chairman) -} - -fn runtime_effect_uses_condition_true_chairman(effect: &RuntimeEffect) -> bool { - match effect { - RuntimeEffect::SetChairmanCash { target, .. } - | RuntimeEffect::DeactivateChairman { target } => { - matches!(target, RuntimeChairmanTarget::ConditionTrueChairman) - } - RuntimeEffect::AppendEventRecord { record } => record - .effects - .iter() - .any(runtime_effect_uses_condition_true_chairman), - _ => false, - } -} - -fn runtime_effect_company_target_import_blocker( - effect: &RuntimeEffect, - company_context: &ImportRuntimeContext, -) -> Option { - match effect { - RuntimeEffect::SetCompanyCash { target, .. } - | RuntimeEffect::SetCompanyVariable { target, .. } - | RuntimeEffect::SetCompanyGovernanceScalar { target, .. } - | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } - | RuntimeEffect::ConfiscateCompanyAssets { target } - | RuntimeEffect::DeactivateCompany { target } - | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } - | RuntimeEffect::AdjustCompanyCash { target, .. } - | RuntimeEffect::AdjustCompanyDebt { target, .. } => { - if matches!(effect, RuntimeEffect::ConfiscateCompanyAssets { .. }) - && !company_context.has_train_context - { - Some(ImportBlocker::MissingTrainContext) - } else if let RuntimeEffect::SetCompanyTerritoryAccess { territory, .. } = effect { - company_target_import_blocker(target, company_context) - .or_else(|| territory_target_import_blocker(territory, company_context)) - } else { - company_target_import_blocker(target, company_context) - } - } - RuntimeEffect::SetPlayerCash { target, .. } - | RuntimeEffect::SetPlayerVariable { target, .. } - | RuntimeEffect::DeactivatePlayer { target } => { - player_target_import_blocker(target, company_context) - } - RuntimeEffect::SetTerritoryVariable { target, .. } => { - territory_target_import_blocker(target, company_context) - } - RuntimeEffect::SetChairmanCash { target, .. } - | RuntimeEffect::DeactivateChairman { target } => { - chairman_target_import_blocker(target, company_context) - } - RuntimeEffect::RetireTrains { - company_target, - territory_target, - .. - } => { - if !company_context.has_train_context { - return Some(ImportBlocker::MissingTrainContext); - } - if territory_target.is_some() && !company_context.has_train_territory_context { - return Some(ImportBlocker::MissingTrainTerritoryContext); - } - company_target - .as_ref() - .and_then(|target| company_target_import_blocker(target, company_context)) - .or_else(|| { - territory_target - .as_ref() - .and_then(|target| territory_target_import_blocker(target, company_context)) - }) - } - RuntimeEffect::AppendEventRecord { record } => record.effects.iter().find_map(|nested| { - runtime_effect_company_target_import_blocker(nested, company_context) - }), - RuntimeEffect::SetWorldFlag { .. } - | RuntimeEffect::SetWorldVariable { .. } - | RuntimeEffect::SetWorldScalarOverride { .. } - | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } - | RuntimeEffect::SetEconomicStatusCode { .. } - | RuntimeEffect::SetCandidateAvailability { .. } - | RuntimeEffect::SetNamedLocomotiveAvailability { .. } - | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. } - | RuntimeEffect::SetNamedLocomotiveCost { .. } - | RuntimeEffect::SetCargoPriceOverride { .. } - | RuntimeEffect::SetCargoProductionOverride { .. } - | RuntimeEffect::SetCargoProductionSlot { .. } - | RuntimeEffect::SetTerritoryAccessCost { .. } - | RuntimeEffect::SetSpecialCondition { .. } - | RuntimeEffect::ActivateEventRecord { .. } - | RuntimeEffect::DeactivateEventRecord { .. } - | RuntimeEffect::RemoveEventRecord { .. } => None, - } -} - -fn classify_real_grouped_company_targets( - record: &SmpLoadedPackedEventRecordSummary, -) -> Vec> { - let Some(control) = &record.compact_control else { - return Vec::new(); - }; - - control - .grouped_target_scope_ordinals_0x7fb - .iter() - .enumerate() - .map(|(group_index, ordinal)| { - if !record - .grouped_effect_rows - .iter() - .any(|row| row.group_index == group_index) - { - return None; - } - classify_real_grouped_company_target(*ordinal) - }) - .collect() -} - -fn classify_real_grouped_company_target(ordinal: u8) -> Option { - match ordinal { - 0 => Some(RuntimeCompanyTarget::ConditionTrueCompany), - 1 => Some(RuntimeCompanyTarget::SelectedCompany), - 2 => Some(RuntimeCompanyTarget::HumanCompanies), - 3 => Some(RuntimeCompanyTarget::AiCompanies), - _ => None, - } -} - -pub fn validate_runtime_state_dump_document( - document: &RuntimeStateDumpDocument, -) -> Result<(), String> { - if document.format_version != STATE_DUMP_FORMAT_VERSION { - return Err(format!( - "unsupported state dump format_version {} (expected {})", - document.format_version, STATE_DUMP_FORMAT_VERSION - )); - } - if document.dump_id.trim().is_empty() { - return Err("dump_id must not be empty".to_string()); - } - document.state.validate() -} - -pub fn validate_runtime_save_slice_document( - document: &RuntimeSaveSliceDocument, -) -> Result<(), String> { - if document.format_version != SAVE_SLICE_DOCUMENT_FORMAT_VERSION { - return Err(format!( - "unsupported save slice document format_version {} (expected {})", - document.format_version, SAVE_SLICE_DOCUMENT_FORMAT_VERSION - )); - } - if document.save_slice_id.trim().is_empty() { - return Err("save_slice_id must not be empty".to_string()); - } - if document - .source - .description - .as_deref() - .is_some_and(|text| text.trim().is_empty()) - { - return Err("save slice source.description must not be empty".to_string()); - } - if document - .source - .original_save_filename - .as_deref() - .is_some_and(|text| text.trim().is_empty()) - { - return Err("save slice source.original_save_filename must not be empty".to_string()); - } - if document - .source - .original_save_sha256 - .as_deref() - .is_some_and(|text| text.trim().is_empty()) - { - return Err("save slice source.original_save_sha256 must not be empty".to_string()); - } - for (index, note) in document.source.notes.iter().enumerate() { - if note.trim().is_empty() { - return Err(format!( - "save slice source.notes[{index}] must not be empty" - )); - } - } - if document.save_slice.mechanism_family.trim().is_empty() { - return Err("save_slice.mechanism_family must not be empty".to_string()); - } - if document.save_slice.mechanism_confidence.trim().is_empty() { - return Err("save_slice.mechanism_confidence must not be empty".to_string()); - } - Ok(()) -} - -pub fn validate_runtime_overlay_import_document( - document: &RuntimeOverlayImportDocument, -) -> Result<(), String> { - if document.format_version != OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION { - return Err(format!( - "unsupported overlay import document format_version {} (expected {})", - document.format_version, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION - )); - } - if document.import_id.trim().is_empty() { - return Err("import_id must not be empty".to_string()); - } - if document - .source - .description - .as_deref() - .is_some_and(|text| text.trim().is_empty()) - { - return Err("overlay import source.description must not be empty".to_string()); - } - for (index, note) in document.source.notes.iter().enumerate() { - if note.trim().is_empty() { - return Err(format!( - "overlay import source.notes[{index}] must not be empty" - )); - } - } - if document.base_snapshot_path.trim().is_empty() { - return Err("base_snapshot_path must not be empty".to_string()); - } - if document.save_slice_path.trim().is_empty() { - return Err("save_slice_path must not be empty".to_string()); - } - Ok(()) -} - -pub fn load_runtime_save_slice_document( - path: &Path, -) -> Result> { - let text = std::fs::read_to_string(path)?; - let document: RuntimeSaveSliceDocument = serde_json::from_str(&text)?; - Ok(document) -} - -pub fn load_runtime_overlay_import_document( - path: &Path, -) -> Result> { - let text = std::fs::read_to_string(path)?; - let document: RuntimeOverlayImportDocument = serde_json::from_str(&text)?; - Ok(document) -} - -pub fn save_runtime_save_slice_document( - path: &Path, - document: &RuntimeSaveSliceDocument, -) -> Result<(), Box> { - validate_runtime_save_slice_document(document) - .map_err(|err| format!("invalid runtime save slice document: {err}"))?; - let bytes = serde_json::to_vec_pretty(document)?; - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(path, bytes)?; - Ok(()) -} - -pub fn save_runtime_overlay_import_document( - path: &Path, - document: &RuntimeOverlayImportDocument, -) -> Result<(), Box> { - validate_runtime_overlay_import_document(document) - .map_err(|err| format!("invalid runtime overlay import document: {err}"))?; - let bytes = serde_json::to_vec_pretty(document)?; - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(path, bytes)?; - Ok(()) -} - -pub fn load_runtime_state_import( - path: &Path, -) -> Result> { - let text = std::fs::read_to_string(path)?; - load_runtime_state_import_from_str_with_base( - &text, - path.file_stem() - .and_then(|stem| stem.to_str()) - .unwrap_or("runtime-state"), - path.parent().unwrap_or_else(|| Path::new(".")), - ) -} - -pub fn load_runtime_state_import_from_str( - text: &str, - fallback_id: &str, -) -> Result> { - load_runtime_state_import_from_str_with_base(text, fallback_id, Path::new(".")) -} - -fn load_runtime_state_import_from_str_with_base( - text: &str, - fallback_id: &str, - base_dir: &Path, -) -> Result> { - if let Ok(document) = serde_json::from_str::(text) { - validate_runtime_state_dump_document(&document) - .map_err(|err| format!("invalid runtime state dump document: {err}"))?; - return Ok(RuntimeStateImport { - import_id: document.dump_id, - description: document.source.description, - state: document.state, - }); - } - - if let Ok(document) = serde_json::from_str::(text) { - validate_runtime_save_slice_document(&document) - .map_err(|err| format!("invalid runtime save slice document: {err}"))?; - let mut description_parts = Vec::new(); - if let Some(description) = document.source.description { - description_parts.push(description); - } - if let Some(filename) = document.source.original_save_filename { - description_parts.push(format!("source save {filename}")); - } - let import = project_save_slice_to_runtime_state_import( - &document.save_slice, - &document.save_slice_id, - if description_parts.is_empty() { - None - } else { - Some(description_parts.join(" | ")) - }, - )?; - return Ok(import); - } - - if let Ok(document) = serde_json::from_str::(text) { - validate_runtime_overlay_import_document(&document) - .map_err(|err| format!("invalid runtime overlay import document: {err}"))?; - let base_snapshot_path = resolve_document_path(base_dir, &document.base_snapshot_path); - let save_slice_path = resolve_document_path(base_dir, &document.save_slice_path); - - let snapshot = load_runtime_snapshot_document(&base_snapshot_path)?; - validate_runtime_snapshot_document(&snapshot).map_err(|err| { - format!( - "invalid runtime snapshot {}: {err}", - base_snapshot_path.display() - ) - })?; - let save_slice_document = load_runtime_save_slice_document(&save_slice_path)?; - validate_runtime_save_slice_document(&save_slice_document).map_err(|err| { - format!( - "invalid runtime save slice document {}: {err}", - save_slice_path.display() - ) - })?; - - let mut description_parts = Vec::new(); - if let Some(description) = document.source.description { - description_parts.push(description); - } - if let Some(description) = snapshot.source.description { - description_parts.push(format!("base snapshot {description}")); - } - if let Some(description) = save_slice_document.source.description { - description_parts.push(format!("save slice {description}")); - } - - return project_save_slice_overlay_to_runtime_state_import( - &snapshot.state, - &save_slice_document.save_slice, - &document.import_id, - if description_parts.is_empty() { - None - } else { - Some(description_parts.join(" | ")) - }, - ) - .map_err(Into::into); - } - - let state: RuntimeState = serde_json::from_str(text)?; - state - .validate() - .map_err(|err| format!("invalid runtime state: {err}"))?; - Ok(RuntimeStateImport { - import_id: fallback_id.to_string(), - description: None, - state, - }) -} - -fn resolve_document_path(base_dir: &Path, path: &str) -> PathBuf { - let candidate = PathBuf::from(path); - if candidate.is_absolute() { - candidate - } else { - base_dir.join(candidate) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - RuntimeConditionComparator, RuntimeTrackPieceCounts, RuntimeTrain, StepCommand, - execute_step_command, - }; - - fn state() -> RuntimeState { - RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - } - } - - fn packed_text_bands() -> Vec { - vec![ - crate::SmpLoadedPackedEventTextBandSummary { - label: "primary_text_band".to_string(), - packed_len: 5, - present: true, - preview: "Alpha".to_string(), - }, - crate::SmpLoadedPackedEventTextBandSummary { - label: "secondary_text_band_0".to_string(), - packed_len: 0, - present: false, - preview: "".to_string(), - }, - crate::SmpLoadedPackedEventTextBandSummary { - label: "secondary_text_band_1".to_string(), - packed_len: 0, - present: false, - preview: "".to_string(), - }, - crate::SmpLoadedPackedEventTextBandSummary { - label: "secondary_text_band_2".to_string(), - packed_len: 0, - present: false, - preview: "".to_string(), - }, - crate::SmpLoadedPackedEventTextBandSummary { - label: "secondary_text_band_3".to_string(), - packed_len: 0, - present: false, - preview: "".to_string(), - }, - crate::SmpLoadedPackedEventTextBandSummary { - label: "secondary_text_band_4".to_string(), - packed_len: 0, - present: false, - preview: "".to_string(), - }, - ] - } - - fn real_condition_rows() -> Vec { - vec![crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: -1, - subtype: 4, - flag_bytes: vec![0x30; 25], - candidate_name: Some("AutoPlant".to_string()), - comparator: None, - metric: None, - semantic_family: None, - semantic_preview: None, - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec!["negative sentinel-style condition row id".to_string()], - }] - } - - fn synthetic_packed_record( - record_index: usize, - live_entry_id: u32, - effect: RuntimeEffect, - ) -> crate::SmpLoadedPackedEventRecordSummary { - crate::SmpLoadedPackedEventRecordSummary { - record_index, - live_entry_id, - payload_offset: Some(0x7200 + (live_entry_id as usize * 0x20)), - payload_len: Some(64), - decode_status: "parity_only".to_string(), - payload_family: "synthetic_harness".to_string(), - trigger_kind: Some(7), - active: Some(true), - marks_collection_dirty: Some(false), - one_shot: Some(false), - compact_control: None, - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: vec![], - decoded_conditions: Vec::new(), - decoded_actions: vec![effect], - executable_import_ready: false, - notes: vec!["synthetic test record".to_string()], - } - } - - fn company_negative_sentinel_scope( - company_test_scope: RuntimeCompanyConditionTestScope, - ) -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope, - player_test_scope: RuntimePlayerConditionTestScope::Disabled, - territory_scope_selector_is_0x63: false, - source_row_indexes: vec![0], - } - } - - fn territory_negative_sentinel_scope() -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary - { - crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies, - player_test_scope: RuntimePlayerConditionTestScope::Disabled, - territory_scope_selector_is_0x63: true, - source_row_indexes: vec![0], - } - } - - fn player_negative_sentinel_scope() -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies, - player_test_scope: RuntimePlayerConditionTestScope::AllPlayers, - territory_scope_selector_is_0x63: false, - source_row_indexes: vec![0], - } - } - - fn selected_chairman_negative_sentinel_scope() - -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: RuntimeCompanyConditionTestScope::Disabled, - player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, - territory_scope_selector_is_0x63: false, - source_row_indexes: vec![0], - } - } - - fn real_grouped_rows() -> Vec { - vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 2, - descriptor_label: Some("Company Cash".to_string()), - target_mask_bits: Some(0x01), - parameter_family: Some("company_finance_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 8, - raw_scalar_value: 7, - value_byte_0x09: 1, - value_dword_0x0d: 12, - value_byte_0x11: 2, - value_byte_0x12: 3, - value_word_0x14: 24, - value_word_0x16: 36, - row_shape: "multivalue_scalar".to_string(), - semantic_family: Some("multivalue_scalar".to_string()), - semantic_preview: Some("Set Company Cash to 7 with aux [2, 3, 24, 36]".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: Some("Mikado".to_string()), - notes: vec!["grouped effect row carries locomotive-name side string".to_string()], - }] - } - - fn real_deactivate_company_row( - enabled: bool, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 13, - descriptor_label: Some("Deactivate Company".to_string()), - target_mask_bits: Some(0x01), - parameter_family: Some("company_lifecycle_toggle".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 1, - raw_scalar_value: if enabled { 1 } else { 0 }, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "bool_toggle".to_string(), - semantic_family: Some("bool_toggle".to_string()), - semantic_preview: Some(format!( - "Set Deactivate Company to {}", - if enabled { "TRUE" } else { "FALSE" } - )), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_track_capacity_row(value: i32) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 16, - descriptor_label: Some("Company Track Pieces Buildable".to_string()), - target_mask_bits: Some(0x01), - parameter_family: Some("company_build_limit_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_credit_rating_row(value: i32) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 56, - descriptor_label: Some("Credit Rating".to_string()), - target_mask_bits: Some(0x0b), - parameter_family: Some("company_governance_scalar".to_string()), - grouped_target_subject: Some("company".to_string()), - grouped_target_scope: Some("selected_company".to_string()), - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Credit Rating to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_merger_premium_shell_row( - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 58, - descriptor_label: Some("Merger Premium".to_string()), - target_mask_bits: Some(0x0b), - parameter_family: Some("company_finance_shell_scalar".to_string()), - grouped_target_subject: Some("company".to_string()), - grouped_target_scope: Some("selected_company".to_string()), - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Merger Premium to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![ - "descriptor is recovered in the checked-in effect table as shell_owned parity" - .to_string(), - ], - } - } - - fn real_stock_prices_shell_row( - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 55, - descriptor_label: Some("Stock Prices".to_string()), - target_mask_bits: Some(0x0b), - parameter_family: Some("company_finance_shell_scalar".to_string()), - grouped_target_subject: Some("company".to_string()), - grouped_target_scope: Some("selected_company".to_string()), - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Stock Prices to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![ - "descriptor is recovered in the checked-in effect table as shell_owned parity" - .to_string(), - ], - } - } - - fn real_deactivate_player_row( - enabled: bool, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 14, - descriptor_label: Some("Deactivate Player".to_string()), - target_mask_bits: Some(0x02), - parameter_family: Some("player_lifecycle_toggle".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 1, - raw_scalar_value: if enabled { 1 } else { 0 }, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "bool_toggle".to_string(), - semantic_family: Some("bool_toggle".to_string()), - semantic_preview: Some(format!( - "Set Deactivate Player to {}", - if enabled { "TRUE" } else { "FALSE" } - )), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_territory_access_row( - enabled: bool, - notes: Vec, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 3, - descriptor_label: Some("Territory - Allow All".to_string()), - target_mask_bits: Some(0x05), - parameter_family: Some("territory_access_toggle".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 1, - raw_scalar_value: if enabled { 1 } else { 0 }, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "bool_toggle".to_string(), - semantic_family: Some("bool_toggle".to_string()), - semantic_preview: Some(format!( - "Set Territory - Allow All to {}", - if enabled { "TRUE" } else { "FALSE" } - )), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes, - } - } - - fn real_economic_status_row(value: i32) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 8, - descriptor_label: Some("Economic Status".to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("whole_game_state_enum".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Economic Status to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_limited_track_building_amount_row( - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 122, - descriptor_label: Some("Limited Track Building Amount".to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("world_track_build_limit_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Limited Track Building Amount to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_special_condition_row( - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 108, - descriptor_label: Some("Use Wartime Cargos".to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("special_condition_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Use Wartime Cargos to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_candidate_availability_row( - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 109, - descriptor_label: Some("Turbo Diesel Availability".to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("candidate_availability_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Turbo Diesel Availability to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_locomotive_availability_row( - descriptor_id: u32, - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - fn grounded_locomotive_name(locomotive_id: u32) -> Option<&'static str> { - match locomotive_id { - 1 => Some("2-D-2"), - 2 => Some("E-88"), - 3 => Some("Adler 2-2-2"), - 4 => Some("USA 103"), - 5 => Some("American 4-4-0"), - 6 => Some("Atlantic 4-4-2"), - 7 => Some("Baldwin 0-6-0"), - 8 => Some("Be 5/7"), - 9 => Some("Beuth 2-2-2"), - 10 => Some("Big Boy 4-8-8-4"), - 11 => Some("C55 Deltic"), - 12 => Some("Camelback 0-6-0"), - 13 => Some("Challenger 4-6-6-4"), - 14 => Some("Class 01 4-6-2"), - 15 => Some("Class 103"), - 16 => Some("Class 132"), - 17 => Some("Class 500 4-6-0"), - 18 => Some("Class 9100"), - 19 => Some("Class EF 66"), - 20 => Some("Class 6E"), - 21 => Some("Consolidation 2-8-0"), - 22 => Some("Crampton 4-2-0"), - 23 => Some("DD 080-X"), - 24 => Some("DD40AX"), - 25 => Some("Duke Class 4-4-0"), - 26 => Some("E18"), - 27 => Some("E428"), - 28 => Some("Brenner E412"), - 29 => Some("E60CP"), - 30 => Some("Eight Wheeler 4-4-0"), - 31 => Some("EP-2 Bipolar"), - 32 => Some("ET22"), - 33 => Some("F3"), - 34 => Some("Fairlie 0-6-6-0"), - 35 => Some("Firefly 2-2-2"), - 36 => Some("FP45"), - 37 => Some("Ge 6/6 Crocodile"), - 38 => Some("GG1"), - 39 => Some("GP7"), - 40 => Some("H10 2-8-2"), - 41 => Some("HST 125"), - 42 => Some("Kriegslok 2-10-0"), - 43 => Some("Mallard 4-6-2"), - 44 => Some("Norris 4-2-0"), - 45 => Some("Northern 4-8-4"), - 46 => Some("Orca NX462"), - 47 => Some("Pacific 4-6-2"), - 48 => Some("Planet 2-2-0"), - 49 => Some("Re 6/6"), - 50 => Some("Red Devil 4-8-4"), - 51 => Some("S3 4-4-0"), - 52 => Some("NA-90D"), - 53 => Some("Shay (2-Truck)"), - 54 => Some("Shinkansen Series 0"), - 55 => Some("Stirling 4-2-2"), - 56 => Some("Trans-Euro"), - 57 => Some("V200"), - 58 => Some("VL80T"), - 59 => Some("GP 35"), - 60 => Some("U1"), - 61 => Some("Zephyr"), - _ => None, - } - } - - let recovered_locomotive_id = match descriptor_id { - 241..=351 => Some(descriptor_id - 240), - _ => None, - }; - let descriptor_label = match descriptor_id { - 457..=474 => { - format!( - "Upper-Band Locomotive Availability Slot {}", - descriptor_id - 456 - ) - } - _ => recovered_locomotive_id - .map(|loco_id| { - grounded_locomotive_name(loco_id) - .map(|name| format!("{name} Availability")) - .unwrap_or_else(|| format!("Locomotive {loco_id} Availability")) - }) - .unwrap_or_else(|| "Locomotive Availability".to_string()), - }; - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id, - descriptor_label: Some(descriptor_label.clone()), - target_mask_bits: Some(0x08), - parameter_family: Some("locomotive_availability_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set {descriptor_label} to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id, - locomotive_name: None, - notes: vec![], - } - } - - fn real_locomotive_cost_row( - descriptor_id: u32, - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - fn grounded_locomotive_name(locomotive_id: u32) -> Option<&'static str> { - match locomotive_id { - 1 => Some("2-D-2"), - 2 => Some("E-88"), - 3 => Some("Adler 2-2-2"), - 4 => Some("USA 103"), - 5 => Some("American 4-4-0"), - 6 => Some("Atlantic 4-4-2"), - 7 => Some("Baldwin 0-6-0"), - 8 => Some("Be 5/7"), - 9 => Some("Beuth 2-2-2"), - 10 => Some("Big Boy 4-8-8-4"), - 11 => Some("C55 Deltic"), - 12 => Some("Camelback 0-6-0"), - 13 => Some("Challenger 4-6-6-4"), - 14 => Some("Class 01 4-6-2"), - 15 => Some("Class 103"), - 16 => Some("Class 132"), - 17 => Some("Class 500 4-6-0"), - 18 => Some("Class 9100"), - 19 => Some("Class EF 66"), - 20 => Some("Class 6E"), - 21 => Some("Consolidation 2-8-0"), - 22 => Some("Crampton 4-2-0"), - 23 => Some("DD 080-X"), - 24 => Some("DD40AX"), - 25 => Some("Duke Class 4-4-0"), - 26 => Some("E18"), - 27 => Some("E428"), - 28 => Some("Brenner E412"), - 29 => Some("E60CP"), - 30 => Some("Eight Wheeler 4-4-0"), - 31 => Some("EP-2 Bipolar"), - 32 => Some("ET22"), - 33 => Some("F3"), - 34 => Some("Fairlie 0-6-6-0"), - 35 => Some("Firefly 2-2-2"), - 36 => Some("FP45"), - 37 => Some("Ge 6/6 Crocodile"), - 38 => Some("GG1"), - 39 => Some("GP7"), - 40 => Some("H10 2-8-2"), - 41 => Some("HST 125"), - 42 => Some("Kriegslok 2-10-0"), - 43 => Some("Mallard 4-6-2"), - 44 => Some("Norris 4-2-0"), - 45 => Some("Northern 4-8-4"), - 46 => Some("Orca NX462"), - 47 => Some("Pacific 4-6-2"), - 48 => Some("Planet 2-2-0"), - 49 => Some("Re 6/6"), - 50 => Some("Red Devil 4-8-4"), - 51 => Some("S3 4-4-0"), - 52 => Some("NA-90D"), - 53 => Some("Shay (2-Truck)"), - 54 => Some("Shinkansen Series 0"), - 55 => Some("Stirling 4-2-2"), - 56 => Some("Trans-Euro"), - 57 => Some("V200"), - 58 => Some("VL80T"), - 59 => Some("GP 35"), - 60 => Some("U1"), - 61 => Some("Zephyr"), - _ => None, - } - } - - let recovered_locomotive_id = match descriptor_id { - 352..=451 => Some(descriptor_id - 351), - _ => None, - }; - let descriptor_label = match descriptor_id { - 475..=502 => format!("Upper-Band Locomotive Cost Slot {}", descriptor_id - 474), - _ => recovered_locomotive_id - .map(|loco_id| { - grounded_locomotive_name(loco_id) - .map(|name| format!("{name} Cost")) - .unwrap_or_else(|| format!("Locomotive {loco_id} Cost")) - }) - .unwrap_or_else(|| "Locomotive Cost".to_string()), - }; - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id, - descriptor_label: Some(descriptor_label.clone()), - target_mask_bits: Some(0x08), - parameter_family: Some("locomotive_cost_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set {descriptor_label} to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id, - locomotive_name: None, - notes: vec![], - } - } - - fn save_named_locomotive_table( - count: usize, - ) -> crate::SmpLoadedNamedLocomotiveAvailabilityTable { - fn grounded_locomotive_name(index: usize) -> String { - match index { - 0 => "2-D-2", - 1 => "E-88", - 2 => "Adler 2-2-2", - 3 => "USA 103", - 4 => "American 4-4-0", - 5 => "Atlantic 4-4-2", - 6 => "Baldwin 0-6-0", - 7 => "Be 5/7", - 8 => "Beuth 2-2-2", - 9 => "Big Boy 4-8-8-4", - 10 => "C55 Deltic", - 11 => "Camelback 0-6-0", - 12 => "Challenger 4-6-6-4", - 13 => "Class 01 4-6-2", - 14 => "Class 103", - 15 => "Class 132", - 16 => "Class 500 4-6-0", - 17 => "Class 9100", - 18 => "Class EF 66", - 19 => "Class 6E", - 20 => "Consolidation 2-8-0", - 21 => "Crampton 4-2-0", - 22 => "DD 080-X", - 23 => "DD40AX", - 24 => "Duke Class 4-4-0", - 25 => "E18", - 26 => "E428", - 27 => "Brenner E412", - 28 => "E60CP", - 29 => "Eight Wheeler 4-4-0", - 30 => "EP-2 Bipolar", - 31 => "ET22", - 32 => "F3", - 33 => "Fairlie 0-6-6-0", - 34 => "Firefly 2-2-2", - 35 => "FP45", - 36 => "Ge 6/6 Crocodile", - 37 => "GG1", - 38 => "GP7", - 39 => "H10 2-8-2", - 40 => "HST 125", - 41 => "Kriegslok 2-10-0", - 42 => "Mallard 4-6-2", - 43 => "Norris 4-2-0", - 44 => "Northern 4-8-4", - 45 => "Orca NX462", - 46 => "Pacific 4-6-2", - 47 => "Planet 2-2-0", - 48 => "Re 6/6", - 49 => "Red Devil 4-8-4", - 50 => "S3 4-4-0", - 51 => "NA-90D", - 52 => "Shay (2-Truck)", - 53 => "Shinkansen Series 0", - 54 => "Stirling 4-2-2", - 55 => "Trans-Euro", - 56 => "V200", - 57 => "VL80T", - 58 => "GP 35", - 59 => "U1", - 60 => "Zephyr", - _ => return format!("Locomotive {}", index + 1), - } - .to_string() - } - - crate::SmpLoadedNamedLocomotiveAvailabilityTable { - source_kind: "runtime-save-direct-serializer".to_string(), - semantic_family: "scenario-named-locomotive-availability-table".to_string(), - header_offset: None, - entries_offset: Some(0x7c78), - entries_end_offset: Some(0x7c78 + count * 0x41), - observed_entry_count: count, - zero_availability_count: 0, - zero_availability_names: vec![], - entries: (0..count) - .map(|index| crate::SmpRt3105SaveNameTableEntry { - index, - offset: 0x7c78 + index * 0x41, - text: grounded_locomotive_name(index), - availability_dword: 1, - availability_dword_hex: "0x00000001".to_string(), - trailer_word: 1, - trailer_word_hex: "0x00000001".to_string(), - }) - .collect(), - } - } - - fn save_cargo_catalog( - entries: &[(u32, crate::RuntimeCargoClass)], - ) -> crate::SmpLoadedCargoCatalog { - crate::SmpLoadedCargoCatalog { - source_kind: "recipe-book-summary-slot-catalog".to_string(), - semantic_family: "scenario-save-derived-cargo-catalog".to_string(), - root_offset: Some(0x0fe7), - observed_entry_count: entries.len(), - entries: entries - .iter() - .enumerate() - .map( - |(index, (slot_id, cargo_class))| crate::SmpLoadedCargoCatalogEntry { - slot_id: *slot_id, - label: format!("Cargo Production Slot {slot_id}"), - cargo_class: *cargo_class, - book_index: index, - max_annual_production_word: 0, - mode_word: 0, - runtime_import_branch_kind: "zero-mode-skipped".to_string(), - annual_amount_word: 0, - supplied_cargo_token_word: 0, - supplied_cargo_token_probable_high16_ascii_stem: None, - demanded_cargo_token_word: 0, - demanded_cargo_token_probable_high16_ascii_stem: None, - }, - ) - .collect(), - } - } - - fn save_company_roster() -> crate::SmpLoadedCompanyRoster { - crate::SmpLoadedCompanyRoster { - source_kind: "tracked-save-slice-company-roster".to_string(), - semantic_family: "save-slice-runtime-company-context".to_string(), - observed_entry_count: 2, - selected_company_id: Some(1), - entries: vec![ - crate::SmpLoadedCompanyRosterEntry { - company_id: 1, - active: true, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 150, - debt: 80, - credit_rating_score: Some(650), - prime_rate: Some(5), - available_track_laying_capacity: Some(6), - track_piece_counts: RuntimeTrackPieceCounts { - total: 20, - single: 5, - double: 8, - transition: 1, - electric: 3, - non_electric: 17, - }, - linked_chairman_profile_id: Some(1), - book_value_per_share: 2620, - investor_confidence: 37, - management_attitude: 58, - takeover_cooldown_year: Some(1839), - merger_cooldown_year: Some(1838), - preferred_locomotive_engine_type_raw_u8: Some(2), - market_state: Some(crate::RuntimeCompanyMarketState { - outstanding_shares: 20_000, - bond_count: 2, - live_bond_slots: Vec::new(), - largest_live_bond_principal: Some(500_000), - highest_coupon_live_bond_principal: Some(350_000), - mutable_support_scalar_raw_u32: 0x3f99999a, - young_company_support_scalar_raw_u32: 0x42700000, - support_progress_word: 12, - recent_per_share_cache_absolute_counter: 0, - recent_per_share_cached_value_bits: 0, - recent_per_share_subscore_raw_u32: 0x420c0000, - cached_share_price_raw_u32: 0x42200000, - chairman_salary_baseline: 24, - chairman_salary_current: 30, - chairman_bonus_year: 1832, - chairman_bonus_amount: 900, - founding_year: 1831, - last_bankruptcy_year: 0, - last_dividend_year: 1837, - current_issue_calendar_word: 5, - current_issue_calendar_word_2: 6, - prior_issue_calendar_word: 4, - prior_issue_calendar_word_2: 5, - city_connection_latch: true, - linked_transit_latch: false, - linked_transit_route_anchor_entry_id: Some(77), - linked_transit_route_anchor_fallback_counts: vec![3, 5, 8], - stat_band_root_0cfb_candidates: Vec::new(), - stat_band_root_0d7f_candidates: Vec::new(), - stat_band_root_1c47_candidates: Vec::new(), - year_stat_family_qword_bits: Vec::new(), - special_stat_family_232a_qword_bits: Vec::new(), - issue_opinion_terms_raw_i32: Vec::new(), - direct_control_transfer_float_fields_raw_u32: BTreeMap::new(), - direct_control_transfer_int_fields_raw_u32: BTreeMap::new(), - }), - }, - crate::SmpLoadedCompanyRosterEntry { - company_id: 2, - active: true, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 90, - debt: 40, - credit_rating_score: Some(480), - prime_rate: Some(6), - available_track_laying_capacity: Some(2), - track_piece_counts: RuntimeTrackPieceCounts { - total: 8, - single: 2, - double: 2, - transition: 0, - electric: 1, - non_electric: 7, - }, - linked_chairman_profile_id: Some(2), - book_value_per_share: 1400, - investor_confidence: 22, - management_attitude: 31, - takeover_cooldown_year: None, - merger_cooldown_year: None, - preferred_locomotive_engine_type_raw_u8: Some(0), - market_state: Some(crate::RuntimeCompanyMarketState { - outstanding_shares: 18_000, - bond_count: 1, - live_bond_slots: Vec::new(), - largest_live_bond_principal: Some(300_000), - highest_coupon_live_bond_principal: Some(300_000), - mutable_support_scalar_raw_u32: 0x3f4ccccd, - young_company_support_scalar_raw_u32: 0x42580000, - support_progress_word: 9, - recent_per_share_cache_absolute_counter: 0, - recent_per_share_cached_value_bits: 0, - recent_per_share_subscore_raw_u32: 0x41f00000, - cached_share_price_raw_u32: 0x41f80000, - chairman_salary_baseline: 20, - chairman_salary_current: 22, - chairman_bonus_year: 0, - chairman_bonus_amount: 0, - founding_year: 1833, - last_bankruptcy_year: 0, - last_dividend_year: 0, - current_issue_calendar_word: 3, - current_issue_calendar_word_2: 4, - prior_issue_calendar_word: 2, - prior_issue_calendar_word_2: 3, - city_connection_latch: false, - linked_transit_latch: true, - linked_transit_route_anchor_entry_id: Some(41), - linked_transit_route_anchor_fallback_counts: vec![13, 21, 34], - stat_band_root_0cfb_candidates: Vec::new(), - stat_band_root_0d7f_candidates: Vec::new(), - stat_band_root_1c47_candidates: Vec::new(), - year_stat_family_qword_bits: Vec::new(), - special_stat_family_232a_qword_bits: Vec::new(), - issue_opinion_terms_raw_i32: Vec::new(), - direct_control_transfer_float_fields_raw_u32: BTreeMap::new(), - direct_control_transfer_int_fields_raw_u32: BTreeMap::new(), - }), - }, - ], - } - } - - fn save_placed_structure_collection() -> crate::SmpLoadedPlacedStructureCollection { - crate::SmpLoadedPlacedStructureCollection { - source_kind: "save-placed-structure-record-triplets".to_string(), - semantic_family: "scenario-save-placed-structure-triplet-collection".to_string(), - observed_entry_count: 2, - entries: vec![ - crate::SmpLoadedPlacedStructureEntry { - record_index: 0, - primary_name: "FarmCorn".to_string(), - secondary_name: "FarmSet".to_string(), - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_payload_dword: 0, - profile_payload_dword_hex: "0x00000000".to_string(), - profile_status_kind: "farm_growth_stage_bucket".to_string(), - farm_growth_stage_index: Some(4), - profile_companion_byte_u8: Some(0), - profile_companion_byte_hex: Some("0x00".to_string()), - }, - crate::SmpLoadedPlacedStructureEntry { - record_index: 1, - primary_name: "StationA".to_string(), - secondary_name: "StationSetA".to_string(), - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_payload_dword: 0x00005dc1, - profile_payload_dword_hex: "0x00005dc1".to_string(), - profile_status_kind: "opaque_nondefault".to_string(), - farm_growth_stage_index: None, - profile_companion_byte_u8: Some(7), - profile_companion_byte_hex: Some("0x07".to_string()), - }, - ], - } - } - - fn save_placed_structure_dynamic_side_buffer_summary() - -> crate::SmpLoadedPlacedStructureDynamicSideBufferSummary { - crate::SmpLoadedPlacedStructureDynamicSideBufferSummary { - source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(), - semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-summary" - .to_string(), - observed_entry_count: 118, - owner_shared_dword_hex: "0xff0000ff".to_string(), - unique_embedded_name_pair_count: 9, - decoded_embedded_name_row_count: 118, - first_prefix_leading_dword_hex: "0xff0000ff".to_string(), - first_prefix_trailing_word_hex: "0x0001".to_string(), - first_prefix_separator_byte_hex: "0xff".to_string(), - triplet_alignment_overlap_count: 2, - triplet_alignment_side_buffer_only_name_pair_count: 7, - compact_prefix_pattern_summaries: vec![], - name_pair_summaries: vec![], - } - } - - fn save_region_collection() -> crate::SmpLoadedRegionCollection { - crate::SmpLoadedRegionCollection { - source_kind: "save-region-record-triplets".to_string(), - semantic_family: "scenario-save-region-triplet-collection".to_string(), - observed_entry_count: 2, - entries: vec![ - crate::SmpLoadedRegionEntry { - record_index: 0, - name: "Marker09".to_string(), - pre_name_prefix_len: 0, - policy_leading_f32_0: 368.0, - policy_leading_f32_1: 0.0, - policy_leading_f32_2: 92.0, - policy_reserved_dwords: vec![0, 0, 0], - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_collection: Some(crate::SmpLoadedRegionProfileCollection { - direct_collection_flag: 1, - entry_stride: 0x22, - live_id_bound: 18, - live_record_count: 17, - trailing_padding_len: 2, - entries: vec![ - crate::SmpLoadedRegionProfileEntry { - entry_index: 0, - name: "House".to_string(), - trailing_weight_f32: 0.2, - }, - crate::SmpLoadedRegionProfileEntry { - entry_index: 1, - name: "Farm Corn".to_string(), - trailing_weight_f32: 0.2, - }, - ], - }), - }, - crate::SmpLoadedRegionEntry { - record_index: 1, - name: "Marker10".to_string(), - pre_name_prefix_len: 8, - policy_leading_f32_0: 552.0, - policy_leading_f32_1: 0.0, - policy_leading_f32_2: 276.0, - policy_reserved_dwords: vec![0, 4, 0], - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_collection: Some(crate::SmpLoadedRegionProfileCollection { - direct_collection_flag: 1, - entry_stride: 0x22, - live_id_bound: 26, - live_record_count: 24, - trailing_padding_len: 0, - entries: vec![crate::SmpLoadedRegionProfileEntry { - entry_index: 0, - name: "Farm Corn".to_string(), - trailing_weight_f32: 0.2, - }], - }), - }, - ], - } - } - - fn save_region_fixed_row_run_summary() -> crate::SmpLoadedRegionFixedRowRunSummary { - crate::SmpLoadedRegionFixedRowRunSummary { - source_kind: "save-region-fixed-row-run-candidates".to_string(), - semantic_family: "scenario-save-region-fixed-row-run-summary".to_string(), - target_row_count: 2, - target_row_stride: 0xbc, - target_row_stride_hex: "0xbc".to_string(), - candidates: vec![crate::smp::SmpSaveRegionFixedRowRunCandidate { - count_offset: 0x5300, - count_offset_hex: "0x5300".to_string(), - row_count: 2, - row_stride: 0xbc, - row_stride_hex: "0xbc".to_string(), - rows_offset: 0x5310, - rows_offset_hex: "0x5310".to_string(), - rows_end_offset: 0x5488, - rows_end_offset_hex: "0x5488".to_string(), - distance_to_region_metadata_tag: 0x110, - distance_to_region_metadata_tag_hex: "0x110".to_string(), - dword_lane_summaries: vec![], - shape_signature: "dword0:f32,dword1:zero".to_string(), - shape_family_signature: "family-a".to_string(), - trailing_byte_zero_count: 2, - trailing_byte_nonzero_count: 0, - trailing_byte_distinct_value_count: 1, - trailing_byte_sample_values_hex: vec!["0x00".to_string()], - best_probable_density_lane_relative_offset_hex: Some("0x24".to_string()), - }], - } - } - - fn save_chairman_profile_table() -> crate::SmpLoadedChairmanProfileTable { - crate::SmpLoadedChairmanProfileTable { - source_kind: "tracked-save-slice-chairman-profile-table".to_string(), - semantic_family: "save-slice-runtime-chairman-context".to_string(), - observed_entry_count: 2, - selected_chairman_profile_id: Some(1), - entries: vec![ - crate::SmpLoadedChairmanProfileEntry { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 500, - linked_company_id: Some(1), - company_holdings: BTreeMap::from([(1, 1000)]), - holdings_value_total: 700, - net_worth_total: 1200, - purchasing_power_total: 1500, - personality_byte_0x291: Some(12), - issue_opinion_terms_raw_i32: Vec::new(), - }, - crate::SmpLoadedChairmanProfileEntry { - profile_id: 2, - name: "Chairman Two".to_string(), - active: true, - current_cash: 250, - linked_company_id: Some(2), - company_holdings: BTreeMap::from([(2, 900)]), - holdings_value_total: 600, - net_worth_total: 900, - purchasing_power_total: 1100, - personality_byte_0x291: Some(20), - issue_opinion_terms_raw_i32: Vec::new(), - }, - ], - } - } - - fn real_cargo_production_row( - descriptor_id: u32, - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - let slot = descriptor_id.saturating_sub(229); - let descriptor_label = format!("Cargo Production Slot {slot}"); - let recovered_cargo_class = match slot { - 1..=4 => Some("factory".to_string()), - 5..=8 => Some("farm_mine".to_string()), - 9..=11 => Some("other".to_string()), - _ => None, - }; - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id, - descriptor_label: Some(descriptor_label.clone()), - target_mask_bits: Some(0x08), - parameter_family: Some("cargo_production_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set {descriptor_label} to {value}")), - recovered_cargo_slot: Some(slot), - recovered_cargo_class, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_all_cargo_price_row(value: i32) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 105, - descriptor_label: Some("All Cargo Prices".to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("cargo_price_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set All Cargo Prices to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![ - "descriptor recovered from checked-in EventEffects semantic catalog".to_string(), - ], - } - } - - fn real_named_cargo_price_row( - descriptor_id: u32, - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - let cargo_label = - crate::smp::grounded_named_cargo_price_label(descriptor_id).map(ToString::to_string); - let descriptor_label = cargo_label - .as_deref() - .map(|label| format!("{label} Price")) - .unwrap_or_else(|| { - format!( - "Named Cargo Price Slot {}", - descriptor_id.saturating_sub(105) - ) - }); - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id, - descriptor_label: Some(descriptor_label.clone()), - target_mask_bits: Some(0x08), - parameter_family: Some("cargo_price_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set {descriptor_label} to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: cargo_label, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![ - "descriptor recovered from checked-in EventEffects semantic catalog".to_string(), - ], - } - } - - fn real_aggregate_cargo_production_row( - descriptor_id: u32, - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - let (label, recovered_cargo_class) = match descriptor_id { - 177 => ("All Cargo Production", None), - 178 => ("All Factory Production", Some("factory".to_string())), - 179 => ("All Farm/Mine Production", Some("farm_mine".to_string())), - _ => ("Unknown Cargo Production", None), - }; - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id, - descriptor_label: Some(label.to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("cargo_production_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set {label} to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![ - "descriptor recovered from checked-in EventEffects semantic catalog".to_string(), - ], - } - } - - fn real_named_cargo_production_row( - descriptor_id: u32, - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - let cargo_label = match descriptor_id { - 180 => Some("Alcohol".to_string()), - _ => None, - }; - let descriptor_label = cargo_label - .as_ref() - .map(|label| format!("{label} Production")) - .unwrap_or_else(|| "Unknown Cargo Production".to_string()); - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id, - descriptor_label: Some(descriptor_label.clone()), - target_mask_bits: Some(0x08), - parameter_family: Some("cargo_production_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set {descriptor_label} to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: cargo_label, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![ - "descriptor recovered from checked-in EventEffects semantic catalog".to_string(), - ], - } - } - - fn real_territory_access_cost_row( - value: i32, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 453, - descriptor_label: Some("Territory Access Cost".to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("territory_access_cost_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: value, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some(format!("Set Territory Access Cost to {value}")), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_world_flag_row( - descriptor_id: u32, - label: &str, - enabled: bool, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id, - descriptor_label: Some(label.to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("world_flag_toggle".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 0, - raw_scalar_value: if enabled { 1 } else { 0 }, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "bool_toggle".to_string(), - semantic_family: Some("bool_toggle".to_string()), - semantic_preview: Some(format!( - "Set {label} to {}", - if enabled { "TRUE" } else { "FALSE" } - )), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_confiscate_all_row( - enabled: bool, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 9, - descriptor_label: Some("Confiscate All".to_string()), - target_mask_bits: Some(0x01), - parameter_family: Some("company_confiscation_variant".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 1, - raw_scalar_value: if enabled { 1 } else { 0 }, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "bool_toggle".to_string(), - semantic_family: Some("bool_toggle".to_string()), - semantic_preview: Some(format!( - "Set Confiscate All to {}", - if enabled { "TRUE" } else { "FALSE" } - )), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_retire_train_row( - enabled: bool, - locomotive_name: Option<&str>, - notes: Vec, - ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 15, - descriptor_label: Some("Retire Train".to_string()), - target_mask_bits: Some(0x0d), - parameter_family: Some("company_or_territory_asset_toggle".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 1, - raw_scalar_value: if enabled { 1 } else { 0 }, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "bool_toggle".to_string(), - semantic_family: Some("bool_toggle".to_string()), - semantic_preview: Some(format!( - "Set Retire Train to {}", - if enabled { "TRUE" } else { "FALSE" } - )), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: locomotive_name.map(ToString::to_string), - notes, - } - } - - fn unsupported_real_grouped_row() -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { - crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 1, - row_index: 0, - descriptor_id: 9, - descriptor_label: Some("Confiscate All".to_string()), - target_mask_bits: Some(0x01), - parameter_family: Some("company_confiscation_variant".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 1, - raw_scalar_value: 0, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "bool_toggle".to_string(), - semantic_family: Some("bool_toggle".to_string()), - semantic_preview: Some("Set Confiscate All to FALSE".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec![], - } - } - - fn real_compact_control() -> crate::SmpLoadedPackedEventCompactControlSummary { - crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 1, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22], - } - } - - fn real_compact_control_without_symbolic_company_scope() - -> crate::SmpLoadedPackedEventCompactControlSummary { - crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 1, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![8, 9, 10, 11], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22], - } - } - - #[test] - fn loads_dump_document() { - let text = serde_json::to_string(&RuntimeStateDumpDocument { - format_version: STATE_DUMP_FORMAT_VERSION, - dump_id: "dump-smoke".to_string(), - source: RuntimeStateDumpSource { - description: Some("test dump".to_string()), - source_binary: None, - }, - state: state(), - }) - .expect("dump should serialize"); - - let import = - load_runtime_state_import_from_str(&text, "fallback").expect("dump should load"); - assert_eq!(import.import_id, "dump-smoke"); - assert_eq!(import.description.as_deref(), Some("test dump")); - } - - #[test] - fn loads_bare_runtime_state() { - let text = serde_json::to_string(&state()).expect("state should serialize"); - let import = - load_runtime_state_import_from_str(&text, "fallback").expect("state should load"); - assert_eq!(import.import_id, "fallback"); - assert!(import.description.is_none()); - } - - #[test] - fn validates_and_roundtrips_save_slice_document() { - let document = RuntimeSaveSliceDocument { - format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, - save_slice_id: "save-slice-smoke".to_string(), - source: RuntimeSaveSliceDocumentSource { - description: Some("test save slice".to_string()), - original_save_filename: Some("smoke.gms".to_string()), - original_save_sha256: Some("deadbeef".to_string()), - notes: vec!["captured fixture".to_string()], - }, - save_slice: crate::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![], - }, - }; - assert!(validate_runtime_save_slice_document(&document).is_ok()); - - 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-save-slice-doc-{nonce}.json")); - save_runtime_save_slice_document(&path, &document).expect("save slice doc should save"); - let loaded = load_runtime_save_slice_document(&path).expect("save slice doc should load"); - assert_eq!(document, loaded); - let _ = std::fs::remove_file(path); - } - - #[test] - fn loads_save_slice_document_as_runtime_state_import() { - let text = serde_json::to_string(&RuntimeSaveSliceDocument { - format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, - save_slice_id: "save-slice-import".to_string(), - source: RuntimeSaveSliceDocumentSource { - description: Some("test save slice import".to_string()), - original_save_filename: Some("import.gms".to_string()), - original_save_sha256: None, - notes: vec![], - }, - save_slice: crate::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![], - }, - }) - .expect("save slice doc should serialize"); - - let import = load_runtime_state_import_from_str(&text, "fallback") - .expect("save slice document should load as runtime import"); - assert_eq!(import.import_id, "save-slice-import"); - assert_eq!( - import - .state - .metadata - .get("save_slice.import_projection") - .map(String::as_str), - Some("partial-runtime-restore-v1") - ); - } - - #[test] - fn projects_save_slice_into_runtime_state_import() { - let save_slice = SmpLoadedSaveSlice { - file_extension_hint: Some("gms".to_string()), - container_profile_family: Some("rt3-105-save-container-v1".to_string()), - mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), - mechanism_confidence: "mixed".to_string(), - trailer_family: Some("rt3-105-save-trailer-v1".to_string()), - bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), - profile: Some(crate::SmpLoadedProfile { - profile_kind: "rt3-105-packed-profile".to_string(), - profile_family: "rt3-105-save-container-v1".to_string(), - packed_profile_offset: 0x73c0, - packed_profile_len: 0x108, - packed_profile_len_hex: "0x108".to_string(), - leading_word_0: 3, - leading_word_0_hex: "0x00000003".to_string(), - header_flag_word_3: Some(0x01000000), - header_flag_word_3_hex: Some("0x01000000".to_string()), - map_path: Some("Alternate USA.gmp".to_string()), - 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(), - }), - candidate_availability_table: Some(crate::SmpLoadedCandidateAvailabilityTable { - source_kind: "save-bridge-secondary-block".to_string(), - semantic_family: "scenario-named-candidate-availability-table".to_string(), - header_offset: 0x6a70, - entries_offset: 0x6ad1, - entries_end_offset: 0x73b7, - observed_entry_count: 2, - zero_availability_count: 1, - zero_availability_names: vec!["Uranium Mine".to_string()], - footer_progress_hex_words: vec!["0x000032dc".to_string(), "0x00003714".to_string()], - entries: vec![ - crate::SmpRt3105SaveNameTableEntry { - index: 0, - offset: 0x6ad1, - text: "AutoPlant".to_string(), - availability_dword: 1, - availability_dword_hex: "0x00000001".to_string(), - trailer_word: 1, - trailer_word_hex: "0x00000001".to_string(), - }, - crate::SmpRt3105SaveNameTableEntry { - index: 1, - offset: 0x6af3, - text: "Uranium Mine".to_string(), - availability_dword: 0, - availability_dword_hex: "0x00000000".to_string(), - trailer_word: 0, - trailer_word_hex: "0x00000000".to_string(), - }, - ], - }), - named_locomotive_availability_table: Some( - crate::SmpLoadedNamedLocomotiveAvailabilityTable { - source_kind: "runtime-save-direct-serializer".to_string(), - semantic_family: "scenario-named-locomotive-availability-table".to_string(), - header_offset: None, - entries_offset: None, - entries_end_offset: None, - observed_entry_count: 2, - zero_availability_count: 1, - zero_availability_names: vec!["Big Boy".to_string()], - entries: vec![ - crate::SmpRt3105SaveNameTableEntry { - index: 0, - offset: 0, - text: "Big Boy".to_string(), - availability_dword: 0, - availability_dword_hex: "0x00000000".to_string(), - trailer_word: 0, - trailer_word_hex: "0x00000000".to_string(), - }, - crate::SmpRt3105SaveNameTableEntry { - index: 1, - offset: 0x41, - text: "GP7".to_string(), - availability_dword: 1, - availability_dword_hex: "0x00000001".to_string(), - trailer_word: 1, - trailer_word_hex: "0x00000001".to_string(), - }, - ], - }, - ), - locomotive_catalog: None, - cargo_catalog: Some(save_cargo_catalog(&[ - (1, crate::RuntimeCargoClass::Factory), - (5, crate::RuntimeCargoClass::FarmMine), - (9, crate::RuntimeCargoClass::Other), - ])), - world_issue_37_state: Some(crate::SmpLoadedWorldIssue37State { - source_kind: "save-fixed-world-block".to_string(), - semantic_family: "world-issue-0x37".to_string(), - issue_value: 3, - issue_value_hex: "0x00000003".to_string(), - issue_38_value: 1, - issue_38_value_hex: "0x01".to_string(), - issue_39_value: 2, - issue_39_value_hex: "0x02".to_string(), - issue_3a_value: 4, - issue_3a_value_hex: "0x04".to_string(), - multiplier_raw_u32: 0x3d75c28f, - multiplier_raw_hex: "0x3d75c28f".to_string(), - multiplier_value_f32_text: "0.060000".to_string(), - issue_opinion_base_terms_raw_i32: Vec::new(), - }), - world_economic_tuning_state: Some(crate::SmpLoadedWorldEconomicTuningState { - source_kind: "save-fixed-world-block".to_string(), - semantic_family: "world-economic-tuning".to_string(), - mirror_raw_u32: 0x3f46d093, - mirror_raw_hex: "0x3f46d093".to_string(), - mirror_value_f32_text: "0.776620".to_string(), - lane_raw_u32: vec![0x3f400000, 0x3be56042], - lane_raw_hex: vec!["0x3f400000".to_string(), "0x3be56042".to_string()], - lane_value_f32_text: vec!["0.750000".to_string(), "0.007000".to_string()], - }), - world_finance_neighborhood_state: Some(crate::SmpLoadedWorldFinanceNeighborhoodState { - source_kind: "save-fixed-world-block".to_string(), - semantic_family: "world-finance-neighborhood".to_string(), - packed_year_word_raw_u16: 0x0201, - packed_year_word_raw_hex: "0x0201".to_string(), - partial_year_progress_raw_u8: 3, - partial_year_progress_raw_hex: "0x03".to_string(), - current_calendar_tuple_word_raw_u32: 1, - current_calendar_tuple_word_raw_hex: "0x00000001".to_string(), - current_calendar_tuple_word_2_raw_u32: 2, - current_calendar_tuple_word_2_raw_hex: "0x00000002".to_string(), - absolute_counter_raw_u32: 3, - absolute_counter_raw_hex: "0x00000003".to_string(), - absolute_counter_mirror_raw_u32: 4, - absolute_counter_mirror_raw_hex: "0x00000004".to_string(), - stock_policy_raw_u8: 0, - stock_policy_raw_hex: "0x00".to_string(), - bond_policy_raw_u8: 1, - bond_policy_raw_hex: "0x01".to_string(), - bankruptcy_policy_raw_u8: 0, - bankruptcy_policy_raw_hex: "0x00".to_string(), - dividend_policy_raw_u8: 1, - dividend_policy_raw_hex: "0x01".to_string(), - building_density_growth_setting_raw_u32: 1, - building_density_growth_setting_raw_hex: "0x00000001".to_string(), - labels: vec![ - "current_calendar_tuple_word".to_string(), - "current_calendar_tuple_word_2".to_string(), - "absolute_calendar_counter".to_string(), - "absolute_calendar_counter_mirror".to_string(), - ], - relative_offsets: vec![0x0d, 0x11, 0x15, 0x19], - relative_offset_hex: vec![ - "0xd".to_string(), - "0x11".to_string(), - "0x15".to_string(), - "0x19".to_string(), - ], - raw_u32: vec![1, 2, 3, 4], - raw_hex: vec![ - "0x00000001".to_string(), - "0x00000002".to_string(), - "0x00000003".to_string(), - "0x00000004".to_string(), - ], - value_i32: vec![1, 2, 3, 4], - value_f32_text: vec![ - "0.000000".to_string(), - "0.000000".to_string(), - "0.000000".to_string(), - "0.000000".to_string(), - ], - }), - world_locomotive_policy_state: Some(crate::smp::SmpLoadedWorldLocomotivePolicyState { - source_kind: "save-fixed-world-block".to_string(), - semantic_family: "world-locomotive-policy".to_string(), - selected_year_gap_scalar_raw_u32: Some(0x3eaaaaab), - selected_year_gap_scalar_raw_hex: Some("0x3eaaaaab".to_string()), - selected_year_gap_scalar_value_f32_text: Some("0.333333".to_string()), - linked_site_removal_follow_on_gate_raw_u8: Some(1), - linked_site_removal_follow_on_gate_raw_hex: Some("0x01".to_string()), - auto_show_grade_during_track_lay_raw_u8: Some(2), - auto_show_grade_during_track_lay_raw_hex: Some("0x02".to_string()), - starting_building_density_level_raw_u8: Some(3), - starting_building_density_level_raw_hex: Some("0x03".to_string()), - building_density_growth_raw_u8: Some(1), - building_density_growth_raw_hex: Some("0x01".to_string()), - leftover_simulation_time_accumulator_raw_u32: Some(0x3f000000), - leftover_simulation_time_accumulator_raw_hex: Some("0x3f000000".to_string()), - leftover_simulation_time_accumulator_value_f32_text: Some("0.500000".to_string()), - selected_year_lane_snapshot_raw_u8: Some(7), - selected_year_lane_snapshot_raw_hex: Some("0x07".to_string()), - all_steam_locomotives_available_raw_u8: Some(1), - all_steam_locomotives_available_raw_hex: Some("0x01".to_string()), - all_diesel_locomotives_available_raw_u8: Some(0), - all_diesel_locomotives_available_raw_hex: Some("0x00".to_string()), - all_electric_locomotives_available_raw_u8: Some(1), - all_electric_locomotives_available_raw_hex: Some("0x01".to_string()), - cached_available_locomotive_rating_raw_u32: Some(0x41a00000), - cached_available_locomotive_rating_raw_hex: Some("0x41a00000".to_string()), - cached_available_locomotive_rating_value_f32_text: Some("20.000000".to_string()), - }), - 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: Some(crate::SmpLoadedSpecialConditionsTable { - source_kind: "save-fixed-special-conditions-range".to_string(), - table_offset: 0x0d64, - table_len: 36 * 4, - enabled_visible_count: 0, - enabled_visible_labels: vec![], - entries: vec![ - crate::SmpSpecialConditionEntry { - slot_index: 30, - hidden: false, - label_id: 3722, - help_id: 3723, - label: "Disable Cargo Economy".to_string(), - value: 0, - value_hex: "0x00000000".to_string(), - }, - crate::SmpSpecialConditionEntry { - slot_index: 35, - hidden: true, - label_id: 3, - help_id: 3, - label: "Hidden sentinel".to_string(), - value: 1, - value_hex: "0x00000001".to_string(), - }, - ], - }), - event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), - mechanism_confidence: "mixed".to_string(), - container_profile_family: Some("rt3-105-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 5, - live_record_count: 3, - live_entry_ids: vec![1, 3, 5], - decoded_record_count: 0, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![ - crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 1, - payload_offset: None, - payload_len: None, - decode_status: "unsupported_framing".to_string(), - payload_family: "unsupported_framing".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - notes: vec!["test".to_string()], - }, - crate::SmpLoadedPackedEventRecordSummary { - record_index: 1, - live_entry_id: 3, - payload_offset: None, - payload_len: None, - decode_status: "unsupported_framing".to_string(), - payload_family: "unsupported_framing".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - notes: vec!["test".to_string()], - }, - crate::SmpLoadedPackedEventRecordSummary { - record_index: 2, - live_entry_id: 5, - payload_offset: None, - payload_len: None, - decode_status: "unsupported_framing".to_string(), - payload_family: "unsupported_framing".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - notes: vec!["test".to_string()], - }, - ], - }), - notes: vec!["packed profile recovered".to_string()], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "save-import-smoke", - Some("test save import".to_string()), - ) - .expect("save slice should project"); - - assert_eq!(import.import_id, "save-import-smoke"); - assert_eq!( - import - .state - .metadata - .get("save_slice.map_path") - .map(String::as_str), - Some("Alternate USA.gmp") - ); - assert_eq!( - import.state.save_profile.selected_year_profile_lane, - Some(0x07) - ); - assert_eq!(import.state.save_profile.sandbox_enabled, Some(true)); - assert_eq!( - import.state.world_restore.selected_year_profile_lane, - Some(0x07) - ); - assert_eq!(import.state.world_restore.sandbox_enabled, Some(true)); - assert_eq!( - import.state.world_restore.campaign_scenario_enabled, - Some(false) - ); - assert_eq!( - import.state.world_restore.seed_tuple_written_from_raw_lane, - Some(true) - ); - assert_eq!( - import - .state - .world_restore - .absolute_counter_requires_shell_context, - Some(false) - ); - assert_eq!( - import - .state - .world_restore - .absolute_counter_reconstructible_from_save, - Some(true) - ); - assert_eq!( - import - .state - .world_restore - .auto_show_grade_during_track_lay_raw_u8, - Some(2) - ); - assert_eq!( - import - .state - .world_restore - .starting_building_density_level_raw_u8, - Some(3) - ); - assert_eq!( - import - .state - .world_restore - .post_text_building_density_growth_raw_u8, - Some(1) - ); - assert_eq!( - import - .state - .world_restore - .leftover_simulation_time_accumulator_raw_u32, - Some(0x3f000000) - ); - assert_eq!( - import - .state - .world_restore - .leftover_simulation_time_accumulator_value_f32_text - .as_deref(), - Some("0.500000") - ); - assert_eq!( - import - .state - .world_restore - .selected_year_lane_snapshot_raw_u8, - Some(7) - ); - assert_eq!( - import.state.world_restore.packed_year_word_raw_u16, - Some(0x0201) - ); - assert_eq!( - import.state.world_restore.partial_year_progress_raw_u8, - Some(3) - ); - assert_eq!( - import - .state - .world_restore - .current_calendar_tuple_word_raw_u32, - Some(1) - ); - assert_eq!( - import - .state - .world_restore - .current_calendar_tuple_word_2_raw_u32, - Some(2) - ); - assert_eq!(import.state.world_restore.absolute_counter_raw_u32, Some(3)); - assert_eq!( - import.state.world_restore.absolute_counter_mirror_raw_u32, - Some(4) - ); - assert_eq!( - import - .state - .world_restore - .disable_cargo_economy_special_condition_slot, - Some(30) - ); - assert_eq!( - import - .state - .world_restore - .disable_cargo_economy_special_condition_reconstructible_from_save, - Some(true) - ); - assert_eq!( - import - .state - .world_restore - .disable_cargo_economy_special_condition_write_side_grounded, - Some(true) - ); - assert_eq!( - import - .state - .world_restore - .disable_cargo_economy_special_condition_enabled, - Some(false) - ); - assert_eq!( - import.state.world_restore.use_bio_accelerator_cars_enabled, - Some(false) - ); - assert_eq!( - import.state.world_restore.use_wartime_cargos_enabled, - Some(false) - ); - assert_eq!( - import.state.world_restore.disable_train_crashes_enabled, - Some(false) - ); - assert_eq!( - import - .state - .world_restore - .disable_train_crashes_and_breakdowns_enabled, - Some(false) - ); - assert_eq!( - import - .state - .world_restore - .ai_ignore_territories_at_startup_enabled, - Some(false) - ); - assert_eq!(import.state.world_restore.issue_37_value, Some(3)); - assert_eq!(import.state.world_restore.issue_38_value, Some(1)); - assert_eq!(import.state.world_restore.issue_39_value, Some(2)); - assert_eq!(import.state.world_restore.issue_3a_value, Some(4)); - assert_eq!( - import.state.world_restore.issue_37_multiplier_raw_u32, - Some(0x3d75c28f) - ); - assert_eq!( - import - .state - .world_restore - .stock_issue_and_buyback_policy_raw_u8, - Some(0) - ); - assert_eq!( - import - .state - .world_restore - .bond_issue_and_repayment_policy_raw_u8, - Some(1) - ); - assert_eq!(import.state.world_restore.bankruptcy_policy_raw_u8, Some(0)); - assert_eq!(import.state.world_restore.dividend_policy_raw_u8, Some(1)); - assert_eq!( - import.state.world_restore.stock_issue_and_buyback_allowed, - Some(true) - ); - assert_eq!( - import.state.world_restore.bond_issue_and_repayment_allowed, - Some(false) - ); - assert_eq!(import.state.world_restore.bankruptcy_allowed, Some(true)); - assert_eq!( - import.state.world_restore.dividend_adjustment_allowed, - Some(false) - ); - assert_eq!( - import - .state - .world_restore - .issue_37_multiplier_value_f32_text - .as_deref(), - Some("0.060000") - ); - assert_eq!( - import.state.world_restore.economic_tuning_mirror_raw_u32, - Some(0x3f400000) - ); - assert_eq!( - import - .state - .world_restore - .economic_tuning_mirror_value_f32_text - .as_deref(), - Some("0.750000") - ); - assert_eq!( - import.state.world_restore.economic_tuning_lane_raw_u32, - vec![0x3f400000, 0x3be56042] - ); - assert_eq!( - import - .state - .world_restore - .economic_tuning_lane_value_f32_text, - vec!["0.750000".to_string(), "0.007000".to_string()] - ); - assert_eq!( - import - .state - .world_restore - .linked_site_removal_follow_on_gate_raw_u8, - Some(1) - ); - assert_eq!( - import - .state - .world_restore - .all_steam_locomotives_available_enabled, - Some(true) - ); - assert_eq!( - import - .state - .world_restore - .all_diesel_locomotives_available_enabled, - Some(false) - ); - assert_eq!( - import - .state - .world_restore - .all_electric_locomotives_available_enabled, - Some(true) - ); - assert_eq!( - import - .state - .world_restore - .cached_available_locomotive_rating_raw_u32, - Some(0x41a00000) - ); - assert_eq!( - import - .state - .world_restore - .absolute_counter_restore_kind - .as_deref(), - Some("save-direct-world-absolute-counter") - ); - assert_eq!( - import - .state - .world_restore - .absolute_counter_adjustment_context - .as_deref(), - Some("save-direct-world-block-0x32c8") - ); - assert_eq!( - import.state.save_profile.map_path.as_deref(), - Some("Alternate USA.gmp") - ); - assert_eq!( - import.state.candidate_availability.get("Uranium Mine"), - Some(&0) - ); - assert_eq!( - import.state.named_locomotive_availability.get("Big Boy"), - Some(&0) - ); - assert_eq!( - import.state.named_locomotive_availability.get("GP7"), - Some(&1) - ); - assert_eq!(import.state.locomotive_catalog.len(), 2); - assert_eq!(import.state.locomotive_catalog[0].locomotive_id, 1); - assert_eq!(import.state.locomotive_catalog[0].name, "Big Boy"); - assert_eq!(import.state.locomotive_catalog[1].locomotive_id, 2); - assert_eq!(import.state.locomotive_catalog[1].name, "GP7"); - assert_eq!( - import.state.special_conditions.get("Disable Cargo Economy"), - Some(&0) - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.world_issue_37_value") - .map(String::as_str), - Some("3") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.world_issue_39_value") - .map(String::as_str), - Some("2") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.world_economic_tuning_lane_count") - .map(String::as_str), - Some("2") - ); - assert_eq!( - import - .state - .world_flags - .get("save_slice.world_issue_37_state_present"), - Some(&true) - ); - assert_eq!( - import - .state - .world_flags - .get("save_slice.world_economic_tuning_state_present"), - Some(&true) - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.named_locomotive_availability_source_kind") - .map(String::as_str), - Some("runtime-save-direct-serializer") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.named_locomotive_availability_entry_count") - .map(String::as_str), - Some("2") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.locomotive_catalog_source_kind") - .map(String::as_str), - Some("derived-from-named-locomotive-availability-table") - ); - assert_eq!( - import - .state - .world_flags - .get("save_slice.profile_byte_0x82_nonzero"), - Some(&true) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| summary.live_record_count), - Some(3) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| summary.live_entry_ids.clone()), - Some(vec![1, 3, 5]) - ); - assert!(import.state.event_runtime_records.is_empty()); - } - - #[test] - fn projects_company_and_chairman_context_from_save_slice() { - let 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(), - 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: Some(save_company_roster()), - chairman_profile_table: Some(save_chairman_profile_table()), - 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![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "save-native-company-chairman", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.companies.len(), 2); - assert_eq!(import.state.selected_company_id, Some(1)); - assert_eq!(import.state.chairman_profiles.len(), 2); - assert_eq!(import.state.selected_chairman_profile_id, Some(1)); - assert_eq!(import.state.companies[0].book_value_per_share, 2620); - assert_eq!(import.state.chairman_profiles[0].current_cash, 500); - assert_eq!(import.state.service_state.company_market_state.len(), 2); - assert_eq!( - import - .state - .service_state - .company_market_state - .get(&1) - .map(|state| state.cached_share_price_raw_u32), - Some(0x42200000) - ); - } - - #[test] - fn projects_placed_structure_collection_metadata_from_save_slice() { - let 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(), - 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: Some(save_region_collection()), - region_fixed_row_run_summary: Some(save_region_fixed_row_run_summary()), - placed_structure_collection: Some(save_placed_structure_collection()), - placed_structure_dynamic_side_buffer_summary: Some( - save_placed_structure_dynamic_side_buffer_summary(), - ), - special_conditions_table: None, - event_runtime_collection: None, - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "save-native-placed-structures", - None, - ) - .expect("save slice should project"); - - assert_eq!( - import - .state - .metadata - .get("save_slice.region_collection_source_kind") - .map(String::as_str), - Some("save-region-record-triplets") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.region_collection_entry_count") - .map(String::as_str), - Some("2") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.region_collection_profile_entry_count") - .map(String::as_str), - Some("3") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.region_collection_nonzero_prefix_count") - .map(String::as_str), - Some("1") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.region_collection_nonzero_reserved_policy_count") - .map(String::as_str), - Some("1") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.region_fixed_row_run_source_kind") - .map(String::as_str), - Some("save-region-fixed-row-run-candidates") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.region_fixed_row_run_candidate_count") - .map(String::as_str), - Some("1") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.region_fixed_row_run_best_rows_offset_hex") - .map(String::as_str), - Some("0x5310") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.region_fixed_row_run_best_shape_signature") - .map(String::as_str), - Some("dword0:f32,dword1:zero") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.placed_structure_collection_source_kind") - .map(String::as_str), - Some("save-placed-structure-record-triplets") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.placed_structure_collection_entry_count") - .map(String::as_str), - Some("2") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.placed_structure_collection_farm_growth_stage_count") - .map(String::as_str), - Some("1") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.placed_structure_collection_nondefault_status_count") - .map(String::as_str), - Some("2") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.placed_structure_dynamic_side_buffer_source_kind") - .map(String::as_str), - Some("save-placed-structure-dynamic-side-buffer-records") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.placed_structure_dynamic_side_buffer_name_pair_count") - .map(String::as_str), - Some("9") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.placed_structure_dynamic_side_buffer_triplet_overlap_count") - .map(String::as_str), - Some("2") - ); - } - - #[test] - fn overlay_replaces_base_company_and_chairman_context_from_save_slice() { - let base_state = RuntimeState { - companies: vec![crate::RuntimeCompany { - company_id: 42, - current_cash: 5, - debt: 1, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: Some(1), - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 10, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(42), - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 9, - name: "Base Chairman".to_string(), - active: true, - current_cash: 10, - linked_company_id: None, - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: Some(9), - territories: vec![crate::RuntimeTerritory { - territory_id: 7, - name: Some("Base Territory".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - ..state() - }; - let 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(), - 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: Some(save_company_roster()), - chairman_profile_table: Some(save_chairman_profile_table()), - 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![], - }; - - let import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "overlay-save-native-company-chairman", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.companies.len(), 2); - assert_eq!(import.state.selected_company_id, Some(1)); - assert_eq!(import.state.chairman_profiles.len(), 2); - assert_eq!(import.state.selected_chairman_profile_id, Some(1)); - assert_eq!(import.state.territories, base_state.territories); - assert_eq!( - import - .state - .service_state - .company_market_state - .get(&2) - .map(|state| state.linked_transit_latch), - Some(true) - ); - } - - #[test] - fn overlay_applies_selection_only_company_and_chairman_context_from_save_slice() { - let base_state = RuntimeState { - companies: vec![ - crate::RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Human, - linked_chairman_profile_id: Some(1), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - crate::RuntimeCompany { - company_id: 42, - current_cash: 200, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Human, - linked_chairman_profile_id: Some(9), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - ], - selected_company_id: Some(42), - chairman_profiles: vec![ - crate::RuntimeChairmanProfile { - profile_id: 1, - name: "Selected".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(1), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }, - crate::RuntimeChairmanProfile { - profile_id: 9, - name: "Base".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(42), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }, - ], - selected_chairman_profile_id: Some(9), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 42, - crate::RuntimeCompanyMarketState { - outstanding_shares: 30_000, - bond_count: 3, - live_bond_slots: Vec::new(), - largest_live_bond_principal: Some(750_000), - highest_coupon_live_bond_principal: Some(500_000), - mutable_support_scalar_raw_u32: 0x3f19999a, - young_company_support_scalar_raw_u32: 0x42580000, - support_progress_word: 8, - recent_per_share_cache_absolute_counter: 0, - recent_per_share_cached_value_bits: 0, - recent_per_share_subscore_raw_u32: 0x42000000, - cached_share_price_raw_u32: 0x42180000, - chairman_salary_baseline: 21, - chairman_salary_current: 24, - chairman_bonus_year: 1836, - chairman_bonus_amount: 600, - founding_year: 1834, - last_bankruptcy_year: 0, - last_dividend_year: 1838, - current_issue_calendar_word: 4, - current_issue_calendar_word_2: 5, - prior_issue_calendar_word: 3, - prior_issue_calendar_word_2: 4, - city_connection_latch: false, - linked_transit_latch: true, - linked_transit_route_anchor_entry_id: Some(91), - linked_transit_route_anchor_fallback_counts: vec![2, 4, 6], - stat_band_root_0cfb_candidates: Vec::new(), - stat_band_root_0d7f_candidates: Vec::new(), - stat_band_root_1c47_candidates: Vec::new(), - year_stat_family_qword_bits: Vec::new(), - special_stat_family_232a_qword_bits: Vec::new(), - issue_opinion_terms_raw_i32: Vec::new(), - direct_control_transfer_float_fields_raw_u32: BTreeMap::new(), - direct_control_transfer_int_fields_raw_u32: BTreeMap::new(), - }, - )]), - world_issue_opinion_base_terms_raw_i32: Vec::new(), - chairman_issue_opinion_terms_raw_i32: BTreeMap::new(), - ..RuntimeServiceState::default() - }, - ..state() - }; - let save_slice = SmpLoadedSaveSlice { - file_extension_hint: Some("gms".to_string()), - container_profile_family: Some("rt3-105-save-container-v1".to_string()), - mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), - mechanism_confidence: "mixed".to_string(), - trailer_family: Some("rt3-105-save-trailer-v1".to_string()), - bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), - 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: Some(crate::SmpLoadedCompanyRoster { - source_kind: "save-direct-world-block-company-selection-only".to_string(), - semantic_family: "scenario-selected-company-context".to_string(), - observed_entry_count: 0, - selected_company_id: Some(1), - entries: Vec::new(), - }), - chairman_profile_table: Some(crate::SmpLoadedChairmanProfileTable { - source_kind: "save-direct-world-block-chairman-selection-only".to_string(), - semantic_family: "scenario-selected-chairman-context".to_string(), - observed_entry_count: 0, - selected_chairman_profile_id: Some(1), - entries: Vec::new(), - }), - 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![], - }; - - let import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "overlay-save-selection-only-context", - None, - ) - .expect("overlay import should project"); - - let mut expected_companies = base_state.companies.clone(); - expected_companies[1].investor_confidence = 38; - assert_eq!(import.state.companies, expected_companies); - assert_eq!(import.state.selected_company_id, Some(1)); - assert_eq!(import.state.chairman_profiles, base_state.chairman_profiles); - assert_eq!(import.state.selected_chairman_profile_id, Some(1)); - assert_eq!( - import.state.service_state.company_market_state, - base_state.service_state.company_market_state - ); - } - - #[test] - fn projects_executable_packed_records_into_runtime_and_services_follow_on() { - let 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(), - 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: Some(save_cargo_catalog(&[ - (1, crate::RuntimeCargoClass::Factory), - (5, crate::RuntimeCargoClass::FarmMine), - (9, crate::RuntimeCargoClass::Other), - ])), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(64), - decode_status: "executable".to_string(), - payload_family: "synthetic_harness".to_string(), - trigger_kind: Some(7), - active: Some(true), - marks_collection_dirty: Some(true), - one_shot: Some(false), - compact_control: None, - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 1, 0, 0], - grouped_effect_rows: vec![], - decoded_conditions: Vec::new(), - decoded_actions: vec![ - RuntimeEffect::SetWorldFlag { - key: "from_packed_root".to_string(), - value: true, - }, - RuntimeEffect::AppendEventRecord { - record: Box::new(RuntimeEventRecordTemplate { - record_id: 99, - trigger_kind: 0x0a, - active: true, - marks_collection_dirty: false, - one_shot: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetSpecialCondition { - label: "Imported Follow-On".to_string(), - value: 1, - }], - }), - }, - ], - executable_import_ready: true, - notes: vec!["decoded test record".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-exec", - Some("test packed event import".to_string()), - ) - .expect("save slice should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| summary.imported_runtime_record_count), - Some(1) - ); - - let result = execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("trigger service should succeed"); - - assert_eq!(result.final_summary.event_runtime_record_count, 2); - assert_eq!(result.final_summary.total_event_record_service_count, 2); - assert_eq!(result.final_summary.total_trigger_dispatch_count, 2); - assert_eq!(result.final_summary.dirty_rerun_count, 1); - assert_eq!( - import.state.world_flags.get("from_packed_root"), - Some(&true) - ); - assert_eq!( - import.state.special_conditions.get("Imported Follow-On"), - Some(&1) - ); - assert_eq!(import.state.event_runtime_records[0].service_count, 1); - assert_eq!(import.state.event_runtime_records[1].record_id, 99); - assert_eq!(import.state.event_runtime_records[1].service_count, 1); - } - - #[test] - fn leaves_parity_only_packed_records_out_of_runtime_event_records() { - let 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(), - 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: Some(save_cargo_catalog(&[ - (1, crate::RuntimeCargoClass::Factory), - (5, crate::RuntimeCargoClass::FarmMine), - (9, crate::RuntimeCargoClass::Other), - ])), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(48), - decode_status: "parity_only".to_string(), - payload_family: "synthetic_harness".to_string(), - trigger_kind: Some(7), - active: Some(true), - marks_collection_dirty: Some(false), - one_shot: Some(false), - compact_control: None, - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: vec![], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { - target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, - delta: 50, - }], - executable_import_ready: false, - notes: vec!["decoded but not importable".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-parity-only", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| summary.decoded_record_count), - Some(1) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| summary.imported_runtime_record_count), - Some(0) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_company_context") - ); - } - - #[test] - fn classifies_symbolic_company_target_blockers_for_standalone_import() { - let 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(), - 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: Some(save_cargo_catalog(&[ - (1, crate::RuntimeCargoClass::Factory), - (5, crate::RuntimeCargoClass::FarmMine), - (9, crate::RuntimeCargoClass::Other), - ])), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 12, - live_record_count: 3, - live_entry_ids: vec![10, 11, 12], - decoded_record_count: 3, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![ - synthetic_packed_record( - 0, - 10, - RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::SelectedCompany, - delta: 1, - }, - ), - synthetic_packed_record( - 1, - 11, - RuntimeEffect::AdjustCompanyDebt { - target: RuntimeCompanyTarget::HumanCompanies, - delta: 2, - }, - ), - synthetic_packed_record( - 2, - 12, - RuntimeEffect::AdjustCompanyDebt { - target: RuntimeCompanyTarget::ConditionTrueCompany, - delta: 3, - }, - ), - ], - }), - notes: vec![], - }; - - let import = - project_save_slice_to_runtime_state_import(&save_slice, "symbolic-blockers", None) - .expect("standalone projection should succeed"); - - assert!(import.state.event_runtime_records.is_empty()); - let outcomes = import - .state - .packed_event_collection - .as_ref() - .expect("packed event collection should be present") - .records - .iter() - .map(|record| record.import_outcome.clone()) - .collect::>(); - assert_eq!( - outcomes, - vec![ - Some("blocked_missing_selection_context".to_string()), - Some("blocked_missing_company_role_context".to_string()), - Some("blocked_missing_condition_context".to_string()), - ] - ); - } - - #[test] - fn overlays_symbolic_company_targets_into_executable_runtime_records() { - let base_state = RuntimeState { - companies: vec![ - crate::RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 10, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - crate::RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 50, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - selected_company_id: Some(1), - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 22, - live_record_count: 2, - live_entry_ids: vec![21, 22], - decoded_record_count: 2, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![ - synthetic_packed_record( - 0, - 21, - RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::SelectedCompany, - delta: 15, - }, - ), - synthetic_packed_record( - 1, - 22, - RuntimeEffect::AdjustCompanyDebt { - target: RuntimeCompanyTarget::AiCompanies, - delta: 4, - }, - ), - ], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "symbolic-overlay", - None, - ) - .expect("overlay projection should succeed"); - - assert_eq!(import.state.event_runtime_records.len(), 2); - let outcomes = import - .state - .packed_event_collection - .as_ref() - .expect("packed event collection should be present") - .records - .iter() - .map(|record| record.import_outcome.clone()) - .collect::>(); - assert_eq!( - outcomes, - vec![Some("imported".to_string()), Some("imported".to_string())] - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("symbolic overlay dispatch should succeed"); - - assert_eq!(import.state.companies[0].current_cash, 115); - assert_eq!(import.state.companies[1].debt, 24); - } - - #[test] - fn leaves_real_records_without_compact_control_blocked_missing_compact_control() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::AllCompanies, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::ConditionTrueCompany, - value: 7, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-structural-only", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_compact_control") - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| summary.records[0].standalone_condition_rows.len()), - Some(1) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| summary.records[0].grouped_effect_rows.len()), - Some(1) - ); - } - - #[test] - fn lowers_negative_sentinel_company_scopes_into_runtime_company_targets() { - let base_state = RuntimeState { - companies: vec![ - crate::RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 10, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - crate::RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 50, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - crate::RuntimeCompany { - company_id: 3, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 70, - debt: 30, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - selected_company_id: Some(3), - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 11, - live_record_count: 5, - live_entry_ids: vec![7, 8, 9, 10, 11], - decoded_record_count: 5, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![ - crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::AllCompanies, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::ConditionTrueCompany, - value: 7, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }, - crate::SmpLoadedPackedEventRecordSummary { - record_index: 1, - live_entry_id: 8, - payload_offset: Some(0x7282), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::SelectedCompanyOnly, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::ConditionTrueCompany, - value: 8, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }, - crate::SmpLoadedPackedEventRecordSummary { - record_index: 2, - live_entry_id: 9, - payload_offset: Some(0x7302), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::AiCompaniesOnly, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::ConditionTrueCompany, - value: 9, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }, - crate::SmpLoadedPackedEventRecordSummary { - record_index: 3, - live_entry_id: 10, - payload_offset: Some(0x7382), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::HumanCompaniesOnly, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::ConditionTrueCompany, - value: 10, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }, - crate::SmpLoadedPackedEventRecordSummary { - record_index: 4, - live_entry_id: 11, - payload_offset: Some(0x7402), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::Disabled, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::ConditionTrueCompany, - value: 11, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }, - ], - }), - notes: vec![], - }; - - let import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "packed-events-real-descriptor-frontier", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.event_runtime_records.len(), 4); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].compact_control.as_ref()) - .map(|control| control.mode_byte_0x7ef), - Some(6) - ); - let effects = import - .state - .event_runtime_records - .iter() - .map(|record| record.effects[0].clone()) - .collect::>(); - assert_eq!( - effects, - vec![ - RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::AllActive, - value: 7, - }, - RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::SelectedCompany, - value: 8, - }, - RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::AiCompanies, - value: 9, - }, - RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::HumanCompanies, - value: 10, - }, - ] - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .map(|record| record.import_outcome.clone()) - .collect::>() - }), - Some(vec![ - Some("imported".to_string()), - Some("imported".to_string()), - Some("imported".to_string()), - Some("imported".to_string()), - Some("blocked_company_condition_scope_disabled".to_string()), - ]) - ); - } - - #[test] - fn blocks_player_scoped_effects_without_player_runtime_context() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some(player_negative_sentinel_scope()), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetPlayerCash { - target: RuntimePlayerTarget::ConditionTruePlayer, - value: 7, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "negative-sentinel-player-scope", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_player_context") - ); - } - - #[test] - fn blocks_named_or_aggregate_territory_conditions_without_territory_context() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: vec![ - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: 2313, - subtype: 0, - flag_bytes: vec![ - 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, - ], - candidate_name: None, - comparator: Some("ge".to_string()), - metric: Some("Territory Track Pieces".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Territory Track Pieces >= 10".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: Vec::new(), - }, - ], - negative_sentinel_scope: Some(territory_negative_sentinel_scope()), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: vec![RuntimeCondition::TerritoryNumericThreshold { - target: RuntimeTerritoryTarget::AllTerritories, - metric: crate::RuntimeTerritoryMetric::TrackPiecesTotal, - comparator: crate::RuntimeConditionComparator::Ge, - value: 10, - }], - decoded_actions: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::ConditionTrueCompany, - value: 7, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "negative-sentinel-territory-scope", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_territory_context") - ); - } - - #[test] - fn leaves_real_records_with_unclassified_scope_blocked_unmapped_real_descriptor() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control_without_symbolic_company_scope()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: real_grouped_rows(), - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-real-descriptor-frontier", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_unmapped_real_descriptor") - ); - } - - #[test] - fn leaves_recovered_shell_owned_descriptor_rows_on_explicit_shell_owned_frontier() { - let 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(), - 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: Some(save_company_roster()), - chairman_profile_table: Some(save_chairman_profile_table()), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 8, - live_record_count: 1, - live_entry_ids: vec![8], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 8, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control_without_symbolic_company_scope()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![2, 0, 0, 0], - grouped_effect_rows: vec![ - real_stock_prices_shell_row(120), - real_merger_premium_shell_row(25), - ], - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - notes: vec!["synthetic shell-owned descriptor test record".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-shell-owned-descriptor-frontier", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_shell_owned_descriptor") - ); - } - - #[test] - fn imports_credit_rating_descriptor_from_save_slice_company_context() { - let 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(), - 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: Some(save_company_roster()), - chairman_profile_table: Some(save_chairman_profile_table()), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 9, - live_record_count: 1, - live_entry_ids: vec![9], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 9, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(true), - compact_control: Some(real_compact_control_without_symbolic_company_scope()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_credit_rating_row(640)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::CreditRating, - value: 640, - }], - executable_import_ready: true, - notes: vec!["synthetic governance descriptor test record".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-credit-rating-descriptor", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .event_runtime_records - .first() - .map(|record| record.effects.clone()), - Some(vec![RuntimeEffect::SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::CreditRating, - value: 640, - }]) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - } - - #[test] - fn blocks_scalar_locomotive_availability_rows_without_catalog_context() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 31, - live_record_count: 1, - live_entry_ids: vec![31], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 31, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 250, - descriptor_label: Some("Big Boy 4-8-8-4 Availability".to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("locomotive_availability_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: 42, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some( - "Set Big Boy 4-8-8-4 Availability to 42".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: Some(10), - locomotive_name: None, - notes: vec![], - }], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "decoded from grounded real 0x4e9a row framing".to_string(), - "scalar locomotive availability rows still need catalog context" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-recovered-locomotive-availability-frontier", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_locomotive_catalog_context") - ); - } - - #[test] - fn blocks_boolean_locomotive_availability_rows_without_catalog_context() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 32, - live_record_count: 1, - live_entry_ids: vec![32], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 32, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_locomotive_availability_row(250, 1)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "boolean locomotive availability row still needs catalog context" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-locomotive-availability-missing-catalog", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_locomotive_catalog_context") - ); - } - - #[test] - fn imports_scalar_locomotive_availability_rows_with_save_derived_catalog_context() { - let 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(), - mechanism_confidence: "grounded".to_string(), - trailer_family: None, - bridge_family: None, - profile: None, - candidate_availability_table: None, - named_locomotive_availability_table: Some(save_named_locomotive_table(61)), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 33, - live_record_count: 1, - live_entry_ids: vec![33], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 33, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![2, 0, 0, 0], - grouped_effect_rows: vec![ - real_locomotive_availability_row(250, 42), - real_locomotive_availability_row(301, 7), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "scalar locomotive availability rows use save-derived catalog context" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "save-derived-locomotive-availability", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.locomotive_catalog.len(), 61); - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("save-derived locomotive availability record should run"); - - assert_eq!( - import - .state - .named_locomotive_availability - .get("Big Boy 4-8-8-4"), - Some(&42) - ); - assert_eq!( - import.state.named_locomotive_availability.get("Zephyr"), - Some(&7) - ); - } - - #[test] - fn overlays_scalar_locomotive_availability_rows_into_named_availability_effects() { - let base_state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 2, - phase_slot: 1, - tick_slot: 3, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: vec![ - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 10, - name: "Big Boy 4-8-8-4".to_string(), - }, - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 61, - name: "Zephyr".to_string(), - }, - ], - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::from([ - ("Big Boy 4-8-8-4".to_string(), 0), - ("Zephyr".to_string(), 1), - ]), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 33, - live_record_count: 1, - live_entry_ids: vec![33], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 33, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![2, 0, 0, 0], - grouped_effect_rows: vec![ - real_locomotive_availability_row(250, 42), - real_locomotive_availability_row(301, 7), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "scalar locomotive availability rows use overlay catalog context" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "overlay-locomotive-availability", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("overlay-imported locomotive availability record should run"); - - assert_eq!( - import - .state - .named_locomotive_availability - .get("Big Boy 4-8-8-4"), - Some(&42) - ); - assert_eq!( - import.state.named_locomotive_availability.get("Zephyr"), - Some(&7) - ); - } - - #[test] - fn blocks_recovered_locomotive_cost_rows_without_catalog_context_lower_band() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 34, - live_record_count: 1, - live_entry_ids: vec![34], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 34, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_locomotive_cost_row(352, 250000)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "scalar locomotive cost row still needs catalog context".to_string(), - ], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-locomotive-cost-frontier-lower-band", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_locomotive_catalog_context") - ); - } - - #[test] - fn blocks_recovered_locomotive_cost_rows_without_catalog_context() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 35, - live_record_count: 1, - live_entry_ids: vec![35], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 35, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_locomotive_cost_row(352, 250000)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "scalar locomotive cost row still needs catalog context".to_string(), - ], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-locomotive-cost-missing-catalog", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_locomotive_catalog_context") - ); - } - - #[test] - fn imports_scalar_locomotive_cost_rows_with_save_derived_catalog_context() { - let 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(), - mechanism_confidence: "grounded".to_string(), - trailer_family: None, - bridge_family: None, - profile: None, - candidate_availability_table: None, - named_locomotive_availability_table: Some(save_named_locomotive_table(61)), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 41, - live_record_count: 1, - live_entry_ids: vec![41], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 41, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![2, 0, 0, 0], - grouped_effect_rows: vec![ - real_locomotive_cost_row(352, 250000), - real_locomotive_cost_row(412, 325000), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "scalar locomotive cost rows use save-derived catalog context".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "save-derived-locomotive-cost", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.locomotive_catalog.len(), 61); - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("save-derived locomotive cost record should run"); - - assert_eq!( - import.state.named_locomotive_cost.get("2-D-2"), - Some(&250000) - ); - assert_eq!( - import.state.named_locomotive_cost.get("Zephyr"), - Some(&325000) - ); - } - - #[test] - fn overlays_scalar_locomotive_cost_rows_into_named_cost_effects() { - let base_state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 2, - phase_slot: 1, - tick_slot: 3, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: vec![ - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 1, - name: "2-D-2".to_string(), - }, - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 61, - name: "Zephyr".to_string(), - }, - ], - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::from([ - ("2-D-2".to_string(), 100000), - ("Zephyr".to_string(), 200000), - ]), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 36, - live_record_count: 1, - live_entry_ids: vec![36], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 36, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![2, 0, 0, 0], - grouped_effect_rows: vec![ - real_locomotive_cost_row(352, 250000), - real_locomotive_cost_row(412, 325000), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "scalar locomotive cost rows use overlay catalog context".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "overlay-locomotive-cost", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("overlay-imported locomotive cost record should run"); - - assert_eq!( - import.state.named_locomotive_cost.get("2-D-2"), - Some(&250000) - ); - assert_eq!( - import.state.named_locomotive_cost.get("Zephyr"), - Some(&325000) - ); - } - - #[test] - fn keeps_negative_locomotive_cost_rows_parity_only() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 37, - live_record_count: 1, - live_entry_ids: vec![37], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 37, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_locomotive_cost_row(352, -1)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec!["negative locomotive cost rows remain parity-only".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-negative-locomotive-cost-frontier", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_variant_or_scope_blocked_descriptor") - ); - } - - #[test] - fn imports_recovered_cargo_production_rows_into_runtime_records() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 35, - live_record_count: 1, - live_entry_ids: vec![35], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 35, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_cargo_production_row(230, 125)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "cargo production rows now import through world overrides".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-cargo-production-frontier", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("cargo production runtime record should run"); - - assert_eq!(import.state.cargo_production_overrides.get(&1), Some(&125)); - } - - #[test] - fn imports_aggregate_cargo_economics_rows_into_runtime_records() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 38, - live_record_count: 1, - live_entry_ids: vec![38], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 38, - payload_offset: Some(0x7202), - payload_len: Some(144), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![4, 0, 0, 0], - grouped_effect_rows: vec![ - real_all_cargo_price_row(180), - real_aggregate_cargo_production_row(177, 210), - real_aggregate_cargo_production_row(178, 225), - real_aggregate_cargo_production_row(179, 175), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "grounded aggregate cargo economics descriptors import through bounded override surfaces" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-aggregate-cargo-economics", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("aggregate cargo economics runtime record should run"); - - assert_eq!(import.state.all_cargo_price_override, Some(180)); - assert_eq!(import.state.all_cargo_production_override, Some(210)); - assert_eq!(import.state.factory_cargo_production_override, Some(225)); - assert_eq!(import.state.farm_mine_cargo_production_override, Some(175)); - } - - #[test] - fn imports_named_cargo_price_rows_when_binding_is_grounded() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 39, - live_record_count: 1, - live_entry_ids: vec![39], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 39, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_named_cargo_price_row(106, 140)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCargoPriceOverride { - target: RuntimeCargoPriceTarget::Named { - name: "Alcohol".to_string(), - }, - value: 140, - }], - executable_import_ready: true, - notes: vec![ - "named cargo price descriptors now import through named cargo overrides" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-named-cargo-price-import", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("named cargo price runtime record should run"); - - assert_eq!( - import.state.named_cargo_price_overrides.get("Alcohol"), - Some(&140) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].grouped_effect_rows.first()) - .and_then(|row| row.descriptor_label.as_deref()), - Some("Alcohol Price") - ); - } - - #[test] - fn imports_named_cargo_production_rows_when_binding_is_grounded() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 40, - live_record_count: 1, - live_entry_ids: vec![40], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 40, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_named_cargo_production_row(180, 160)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::Named { - name: "Alcohol".to_string(), - }, - value: 160, - }], - executable_import_ready: true, - notes: vec!["named cargo production descriptors now import through named cargo overrides" - .to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-named-cargo-production-parity", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("named cargo production runtime record should run"); - - assert_eq!( - import.state.named_cargo_production_overrides.get("Alcohol"), - Some(&160) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - } - - #[test] - fn keeps_negative_all_cargo_price_rows_variant_blocked() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 41, - live_record_count: 1, - live_entry_ids: vec![41], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 41, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_all_cargo_price_row(-1)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "negative aggregate cargo price variants remain parity-only".to_string(), - ], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-negative-all-cargo-price", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_variant_or_scope_blocked_descriptor") - ); - } - - #[test] - fn imports_recovered_territory_access_cost_rows_into_runtime_records() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 36, - live_record_count: 1, - live_entry_ids: vec![36], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 36, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_territory_access_cost_row(750000)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec![ - "territory access cost rows now import through world restore".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "packed-events-territory-access-cost-frontier", - None, - ) - .expect("save slice should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("territory access cost runtime record should run"); - - assert_eq!( - import.state.world_restore.territory_access_cost, - Some(750000) - ); - } - - #[test] - fn overlays_real_company_cash_descriptor_into_executable_runtime_record() { - let base_state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 2, - phase_slot: 1, - tick_slot: 3, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 9, - live_record_count: 1, - live_entry_ids: vec![9], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 9, - payload_offset: Some(0x7202), - payload_len: Some(133), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 2, - descriptor_label: Some("Company Cash".to_string()), - target_mask_bits: Some(0x01), - parameter_family: Some("company_finance_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 8, - raw_scalar_value: 250, - value_byte_0x09: 1, - value_dword_0x0d: 12, - value_byte_0x11: 2, - value_byte_0x12: 3, - value_word_0x14: 24, - value_word_0x16: 36, - row_shape: "multivalue_scalar".to_string(), - semantic_family: Some("multivalue_scalar".to_string()), - semantic_preview: Some( - "Set Company Cash to 250 with aux [2, 3, 24, 36]".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: Some("Mikado".to_string()), - notes: vec![ - "grouped effect row carries locomotive-name side string".to_string(), - ], - }], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::SelectedCompany, - value: 250, - }], - executable_import_ready: true, - notes: vec![ - "decoded from grounded real 0x4e9a row framing".to_string(), - "grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-company-cash-overlay", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real company-cash descriptor should execute through the normal trigger path"); - - assert_eq!(import.state.companies[0].current_cash, 250); - } - - #[test] - fn overlays_real_territory_access_descriptor_into_executable_runtime_record() { - let base_state = RuntimeState { - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - territories: vec![crate::RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 11, - live_record_count: 1, - live_entry_ids: vec![11], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 11, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 12, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_territory_access_row(true, vec![])], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyTerritoryAccess { - target: RuntimeCompanyTarget::SelectedCompany, - territory: RuntimeTerritoryTarget::Ids { ids: vec![7] }, - value: true, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-territory-access-overlay", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("real territory-access descriptor should execute"); - - assert_eq!( - import.state.company_territory_access, - vec![crate::RuntimeCompanyTerritoryAccess { - company_id: 42, - territory_id: 7, - }] - ); - } - - #[test] - fn keeps_real_territory_access_false_row_parity_only() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 12, - live_record_count: 1, - live_entry_ids: vec![12], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 12, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 12, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_territory_access_row(false, vec![])], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "real-territory-access-false", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_territory_access_variant") - ); - } - - #[test] - fn keeps_real_territory_access_missing_scope_row_parity_only() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 13, - live_record_count: 1, - live_entry_ids: vec![13], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 13, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 12, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![9, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_territory_access_row( - true, - vec![ - "territory access row is missing company or territory scope" - .to_string(), - ], - )], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "real-territory-access-missing-scope", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_territory_access_scope") - ); - } - - #[test] - fn overlays_real_deactivate_company_descriptor_into_executable_runtime_record() { - let base_state = RuntimeState { - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 13, - live_record_count: 1, - live_entry_ids: vec![13], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 13, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_deactivate_company_row(true)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::DeactivateCompany { - target: RuntimeCompanyTarget::SelectedCompany, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-deactivate-company-overlay", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real deactivate-company descriptor should execute"); - - assert!(!import.state.companies[0].active); - assert_eq!(import.state.selected_company_id, None); - } - - #[test] - fn overlays_real_deactivate_player_descriptor_into_executable_runtime_record() { - let base_state = RuntimeState { - players: vec![ - crate::RuntimePlayer { - player_id: 7, - current_cash: 500, - active: true, - controller_kind: RuntimeCompanyControllerKind::Human, - }, - crate::RuntimePlayer { - player_id: 8, - current_cash: 250, - active: true, - controller_kind: RuntimeCompanyControllerKind::Ai, - }, - ], - selected_player_id: Some(7), - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 18, - live_record_count: 1, - live_entry_ids: vec![18], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 18, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 2, - grouped_target_scope_ordinals_0x7fb: vec![0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some( - crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: RuntimeCompanyConditionTestScope::Disabled, - player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, - territory_scope_selector_is_0x63: false, - source_row_indexes: vec![0], - }, - ), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_deactivate_player_row(true)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::DeactivatePlayer { - target: RuntimePlayerTarget::ConditionTruePlayer, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-deactivate-player-overlay", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real deactivate-player descriptor should execute"); - - assert!(!import.state.players[0].active); - assert!(import.state.players[1].active); - assert_eq!(import.state.selected_player_id, None); - } - - #[test] - fn keeps_real_deactivate_player_false_row_parity_only() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 19, - live_record_count: 1, - live_entry_ids: vec![19], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 19, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 2, - grouped_target_scope_ordinals_0x7fb: vec![0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: real_condition_rows(), - negative_sentinel_scope: Some( - crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: RuntimeCompanyConditionTestScope::Disabled, - player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, - territory_scope_selector_is_0x63: false, - source_row_indexes: vec![0], - }, - ), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_deactivate_player_row(false)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "real-deactivate-player-false", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_variant_or_scope_blocked_descriptor") - ); - } - - #[test] - fn keeps_real_deactivate_company_false_row_parity_only() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 14, - live_record_count: 1, - live_entry_ids: vec![14], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 14, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_deactivate_company_row(false)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "real-deactivate-company-false", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_variant_or_scope_blocked_descriptor") - ); - } - - #[test] - fn overlays_real_track_capacity_descriptor_into_executable_runtime_record() { - let base_state = RuntimeState { - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 16, - live_record_count: 1, - live_entry_ids: vec![16], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 16, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_track_capacity_row(18)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { - target: RuntimeCompanyTarget::SelectedCompany, - value: Some(18), - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-track-capacity-overlay", - None, - ) - .expect("overlay import should project"); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real track-capacity descriptor should execute"); - - assert_eq!( - import.state.companies[0].available_track_laying_capacity, - Some(18) - ); - } - - #[test] - fn overlays_real_economic_status_descriptor_into_executable_runtime_record() { - let base_state = state(); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 18, - live_record_count: 1, - live_entry_ids: vec![18], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 18, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_economic_status_row(2)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetEconomicStatusCode { value: 2 }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-economic-status-overlay", - None, - ) - .expect("overlay import should project"); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real economic-status descriptor should execute"); - - assert_eq!(import.state.world_restore.economic_status_code, Some(2)); - } - - #[test] - fn imports_real_limited_track_building_amount_descriptor_into_executable_runtime_record() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 52, - live_record_count: 1, - live_entry_ids: vec![52], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 52, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_limited_track_building_amount_row(18)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetLimitedTrackBuildingAmount { - value: 18, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "real-limited-track-building-amount-save-slice", - None, - ) - .expect("save-slice import should project"); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("real limited-track-building-amount descriptor should execute"); - - assert_eq!( - import.state.world_restore.limited_track_building_amount, - Some(18) - ); - } - - #[test] - fn overlays_real_special_condition_descriptor_into_executable_runtime_record() { - let base_state = state(); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 21, - live_record_count: 1, - live_entry_ids: vec![21], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 21, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_special_condition_row(1)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetSpecialCondition { - label: "Use Wartime Cargos".to_string(), - value: 1, - }], - executable_import_ready: true, - notes: vec![ - "decoded from grounded real 0x4e9a row framing".to_string(), - "whole-game descriptor labels and parameter families come from the checked-in effect table".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-special-condition-overlay", - None, - ) - .expect("overlay import should project"); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real special-condition descriptor should execute"); - - assert_eq!( - import.state.special_conditions.get("Use Wartime Cargos"), - Some(&1) - ); - } - - #[test] - fn overlays_real_candidate_availability_descriptor_into_executable_runtime_record() { - let base_state = state(); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 22, - live_record_count: 1, - live_entry_ids: vec![22], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 22, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_candidate_availability_row(1)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCandidateAvailability { - name: "Turbo Diesel".to_string(), - value: 1, - }], - executable_import_ready: true, - notes: vec![ - "decoded from grounded real 0x4e9a row framing".to_string(), - "whole-game descriptor labels and parameter families come from the checked-in effect table".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-candidate-availability-overlay", - None, - ) - .expect("overlay import should project"); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real candidate-availability descriptor should execute"); - - assert_eq!( - import.state.candidate_availability.get("Turbo Diesel"), - Some(&1) - ); - } - - #[test] - fn overlays_real_world_flag_descriptor_into_executable_runtime_record() { - let base_state = state(); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 23, - live_record_count: 1, - live_entry_ids: vec![23], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 23, - payload_offset: Some(0x7200), - payload_len: Some(120), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_world_flag_row( - 110, - "Disable Stock Buying and Selling", - true, - )], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetWorldFlag { - key: "world.disable_stock_buying_and_selling".to_string(), - value: true, - }], - executable_import_ready: true, - notes: vec![ - "decoded from grounded real 0x4e9a row framing".to_string(), - "world-flag descriptor identity and keyed runtime mapping are checked in" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-world-flag-overlay", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("trigger service should execute"); - assert_eq!( - import - .state - .world_flags - .get("world.disable_stock_buying_and_selling"), - Some(&true) - ); - } - - #[test] - fn overlays_real_world_flag_false_variant_into_executable_runtime_record() { - let base_state = state(); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 24, - live_record_count: 1, - live_entry_ids: vec![24], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 24, - payload_offset: Some(0x7200), - payload_len: Some(120), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_world_flag_row( - 110, - "Disable Stock Buying and Selling", - false, - )], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetWorldFlag { - key: "world.disable_stock_buying_and_selling".to_string(), - value: false, - }], - executable_import_ready: true, - notes: vec![ - "decoded from grounded real 0x4e9a row framing".to_string(), - "world-flag descriptor identity and keyed runtime mapping are checked in" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-world-flag-false-overlay", - None, - ) - .expect("overlay import should project"); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("trigger service should execute"); - assert_eq!( - import - .state - .world_flags - .get("world.disable_stock_buying_and_selling"), - Some(&false) - ); - } - - #[test] - fn overlays_recovered_locomotive_policy_descriptors_into_executable_runtime_record() { - let base_state = state(); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 29, - live_record_count: 1, - live_entry_ids: vec![29], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 29, - payload_offset: Some(0x7200), - payload_len: Some(160), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![3, 0, 0, 0], - grouped_effect_rows: vec![ - real_world_flag_row(454, "All Steam Locos Avail.", true), - real_world_flag_row(455, "All Diesel Locos Avail.", false), - real_world_flag_row(456, "All Electric Locos Avail.", true), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![ - RuntimeEffect::SetWorldFlag { - key: "world.all_steam_locos_available".to_string(), - value: true, - }, - RuntimeEffect::SetWorldFlag { - key: "world.all_diesel_locos_available".to_string(), - value: false, - }, - RuntimeEffect::SetWorldFlag { - key: "world.all_electric_locos_available".to_string(), - value: true, - }, - ], - executable_import_ready: true, - notes: vec![ - "recovered locomotive policy descriptor band now imports as keyed world flags" - .to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-locomotive-policy-overlay", - None, - ) - .expect("overlay import should project"); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("trigger service should execute"); - assert_eq!( - import - .state - .world_flags - .get("world.all_steam_locos_available"), - Some(&true) - ); - assert_eq!( - import - .state - .world_flags - .get("world.all_diesel_locos_available"), - Some(&false) - ); - assert_eq!( - import - .state - .world_flags - .get("world.all_electric_locos_available"), - Some(&true) - ); - assert_eq!( - import - .state - .world_restore - .all_steam_locomotives_available_raw_u8, - Some(1) - ); - assert_eq!( - import - .state - .world_restore - .all_diesel_locomotives_available_raw_u8, - Some(0) - ); - assert_eq!( - import - .state - .world_restore - .all_electric_locomotives_available_raw_u8, - Some(1) - ); - } - - #[test] - fn overlays_real_world_flag_condition_into_executable_runtime_record() { - let mut base_state = state(); - base_state - .world_flags - .insert("world.disable_stock_buying_and_selling".to_string(), true); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 27, - live_record_count: 1, - live_entry_ids: vec![27], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 27, - payload_offset: Some(0x7200), - payload_len: Some(152), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 1, - standalone_condition_rows: vec![ - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: 2535, - subtype: 0, - flag_bytes: vec![ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, - ], - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some( - "World Flag: Disable Stock Buying and Selling".to_string(), - ), - semantic_family: Some("world_flag_equals".to_string()), - semantic_preview: Some( - "Test Disable Stock Buying and Selling == TRUE".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![ - "checked-in whole-game condition metadata sample".to_string(), - ], - }, - ], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { - group_index: 0, - row_index: 0, - descriptor_id: 109, - descriptor_label: Some("Turbo Diesel Availability".to_string()), - target_mask_bits: Some(0x08), - parameter_family: Some("candidate_availability_scalar".to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode: 3, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - row_shape: "scalar_assignment".to_string(), - semantic_family: Some("scalar_assignment".to_string()), - semantic_preview: Some("Set Turbo Diesel Availability to 1".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - recovered_cargo_label: None, - recovered_locomotive_id: None, - locomotive_name: None, - notes: vec!["checked-in whole-game grouped-effect sample".to_string()], - }], - decoded_conditions: vec![RuntimeCondition::WorldFlagEquals { - key: "world.disable_stock_buying_and_selling".to_string(), - value: true, - }], - decoded_actions: vec![RuntimeEffect::SetCandidateAvailability { - name: "Turbo Diesel".to_string(), - value: 1, - }], - executable_import_ready: true, - notes: vec!["world-flag condition gates a world-side effect".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-world-flag-condition-overlay", - None, - ) - .expect("overlay import should project"); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("trigger service should execute"); - assert_eq!( - import.state.candidate_availability.get("Turbo Diesel"), - Some(&1) - ); - } - - #[test] - fn imports_and_executes_world_scalar_conditions_through_runtime_state() { - let 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(), - 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: Some(save_cargo_catalog(&[ - (1, crate::RuntimeCargoClass::Factory), - (5, crate::RuntimeCargoClass::FarmMine), - (9, crate::RuntimeCargoClass::Other), - ])), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7800, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 45, - live_record_count: 2, - live_entry_ids: vec![41, 45], - decoded_record_count: 2, - imported_runtime_record_count: 2, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![ - crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 41, - payload_offset: Some(0x7200), - payload_len: Some(192), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![7, 0, 0, 0], - grouped_effect_rows: vec![ - real_locomotive_availability_row(250, 42), - real_locomotive_cost_row(352, 250000), - real_cargo_production_row(230, 125), - real_cargo_production_row(234, 75), - real_cargo_production_row(238, 30), - real_limited_track_building_amount_row(18), - real_territory_access_cost_row(750000), - ], - decoded_conditions: vec![], - decoded_actions: vec![ - RuntimeEffect::SetNamedLocomotiveAvailabilityValue { - name: "Big Boy".to_string(), - value: 42, - }, - RuntimeEffect::SetNamedLocomotiveCost { - name: "Locomotive 1".to_string(), - value: 250000, - }, - RuntimeEffect::SetCargoProductionSlot { - slot: 1, - value: 125, - }, - RuntimeEffect::SetCargoProductionSlot { slot: 5, value: 75 }, - RuntimeEffect::SetCargoProductionSlot { slot: 9, value: 30 }, - RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }, - RuntimeEffect::SetTerritoryAccessCost { value: 750000 }, - ], - executable_import_ready: true, - notes: vec!["world-scalar setup record".to_string()], - }, - crate::SmpLoadedPackedEventRecordSummary { - record_index: 1, - live_entry_id: 45, - payload_offset: Some(0x7300), - payload_len: Some(184), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 9, - standalone_condition_rows: vec![ - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: 2422, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&42_i32.to_le_bytes()); - bytes - }, - candidate_name: Some("Big Boy".to_string()), - comparator: Some("eq".to_string()), - metric: Some("Named Locomotive Availability: Big Boy".to_string()), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Named Locomotive Availability: Big Boy == 42".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 1, - raw_condition_id: 2423, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&250000_i32.to_le_bytes()); - bytes - }, - candidate_name: Some("Locomotive 1".to_string()), - comparator: Some("eq".to_string()), - metric: Some("Named Locomotive Cost: Locomotive 1".to_string()), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Named Locomotive Cost: Locomotive 1 == 250000" - .to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 2, - raw_condition_id: 200, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&125_i32.to_le_bytes()); - bytes - }, - candidate_name: Some("Cargo Production Slot 1".to_string()), - comparator: Some("eq".to_string()), - metric: Some( - "Cargo Production: Cargo Production Slot 1".to_string(), - ), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Cargo Production: Cargo Production Slot 1 == 125" - .to_string(), - ), - recovered_cargo_slot: Some(1), - recovered_cargo_class: Some("factory".to_string()), - requires_candidate_name_binding: false, - notes: vec![], - }, - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 3, - raw_condition_id: 2418, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&125_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Cargo Production Total".to_string()), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Cargo Production Total == 230".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 4, - raw_condition_id: 2419, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&125_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Factory Production Total".to_string()), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Factory Production Total == 125".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: Some("factory".to_string()), - requires_candidate_name_binding: false, - notes: vec![], - }, - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 5, - raw_condition_id: 2420, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&75_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Farm/Mine Production Total".to_string()), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Farm/Mine Production Total == 75".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: Some("farm_mine".to_string()), - requires_candidate_name_binding: false, - notes: vec![], - }, - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 6, - raw_condition_id: 2421, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&30_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Other Cargo Production Total".to_string()), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Other Cargo Production Total == 30".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: Some("other".to_string()), - requires_candidate_name_binding: false, - notes: vec![], - }, - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 7, - raw_condition_id: 2547, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&18_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Limited Track Building Amount".to_string()), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Limited Track Building Amount == 18".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 8, - raw_condition_id: 1516, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&750000_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Territory Access Cost".to_string()), - semantic_family: Some("world_scalar_threshold".to_string()), - semantic_preview: Some( - "Test Territory Access Cost == 750000".to_string(), - ), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - ], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_world_flag_row( - 110, - "Disable Stock Buying and Selling", - true, - )], - decoded_conditions: vec![ - RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name: "Big Boy".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 42, - }, - RuntimeCondition::NamedLocomotiveCostThreshold { - name: "Locomotive 1".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 250000, - }, - RuntimeCondition::CargoProductionSlotThreshold { - slot: 1, - label: "Cargo Production Slot 1".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 125, - }, - RuntimeCondition::CargoProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 230, - }, - RuntimeCondition::FactoryProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 125, - }, - RuntimeCondition::FarmMineProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 75, - }, - RuntimeCondition::OtherCargoProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 30, - }, - RuntimeCondition::LimitedTrackBuildingAmountThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 18, - }, - RuntimeCondition::TerritoryAccessCostThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 750000, - }, - ], - decoded_actions: vec![RuntimeEffect::SetWorldFlag { - key: "world.world_scalar_conditions_passed".to_string(), - value: true, - }], - executable_import_ready: true, - notes: vec!["world-scalar conditions gate a world-side effect".to_string()], - }, - ], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "world-scalar-condition-save-slice", - None, - ) - .expect("save-slice import should project"); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("setup trigger should execute"); - assert_eq!( - import.state.named_locomotive_availability.get("Big Boy"), - Some(&42) - ); - assert_eq!( - import.state.named_locomotive_cost.get("Locomotive 1"), - Some(&250000) - ); - assert_eq!(import.state.cargo_production_overrides.get(&1), Some(&125)); - assert_eq!(import.state.cargo_production_overrides.get(&5), Some(&75)); - assert_eq!(import.state.cargo_production_overrides.get(&9), Some(&30)); - assert_eq!(import.state.cargo_catalog.len(), 3); - assert_eq!( - import.state.cargo_catalog[0].cargo_class, - crate::RuntimeCargoClass::Factory - ); - assert_eq!( - import.state.cargo_catalog[1].cargo_class, - crate::RuntimeCargoClass::FarmMine - ); - assert_eq!( - import.state.cargo_catalog[2].cargo_class, - crate::RuntimeCargoClass::Other - ); - assert_eq!(import.state.event_runtime_records.len(), 2); - assert_eq!( - import.state.event_runtime_records[1].conditions, - vec![ - RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name: "Big Boy".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 42, - }, - RuntimeCondition::NamedLocomotiveCostThreshold { - name: "Locomotive 1".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 250000, - }, - RuntimeCondition::CargoProductionSlotThreshold { - slot: 1, - label: "Cargo Production Slot 1".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 125, - }, - RuntimeCondition::CargoProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 230, - }, - RuntimeCondition::FactoryProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 125, - }, - RuntimeCondition::FarmMineProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 75, - }, - RuntimeCondition::OtherCargoProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 30, - }, - RuntimeCondition::LimitedTrackBuildingAmountThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 18, - }, - RuntimeCondition::TerritoryAccessCostThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 750000, - }, - ] - ); - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("gated trigger should execute"); - - assert_eq!( - import - .state - .world_flags - .get("world.world_scalar_conditions_passed"), - Some(&true) - ); - } - - #[test] - fn overlays_selected_chairman_conditions_into_imported_runtime_records() { - let base_state = RuntimeState { - calendar: CalendarPoint { - year: 1840, - month_slot: 1, - phase_slot: 2, - tick_slot: 3, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: Some(1), - book_value_per_share: 2620, - investor_confidence: 37, - management_attitude: 58, - takeover_cooldown_year: Some(1839), - merger_cooldown_year: Some(1838), - }], - selected_company_id: Some(1), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![ - crate::RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 500, - linked_company_id: Some(1), - company_holdings: BTreeMap::new(), - holdings_value_total: 700, - net_worth_total: 1200, - purchasing_power_total: 1500, - }, - crate::RuntimeChairmanProfile { - profile_id: 2, - name: "Chairman Two".to_string(), - active: true, - current_cash: 200, - linked_company_id: None, - company_holdings: BTreeMap::new(), - holdings_value_total: 400, - net_worth_total: 600, - purchasing_power_total: 800, - }, - ], - selected_chairman_profile_id: Some(1), - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 71, - live_record_count: 1, - live_entry_ids: vec![71], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 71, - payload_offset: Some(0x7202), - payload_len: Some(136), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(6), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 1, - standalone_condition_rows: vec![ - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: 2218, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&500_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Player Cash".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Player Cash == 500".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - ], - negative_sentinel_scope: Some(selected_chairman_negative_sentinel_scope()), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_world_flag_row( - 110, - "Disable Stock Buying and Selling", - true, - )], - decoded_conditions: vec![RuntimeCondition::ChairmanNumericThreshold { - target: RuntimeChairmanTarget::SelectedChairman, - metric: crate::RuntimeChairmanMetric::CurrentCash, - comparator: RuntimeConditionComparator::Eq, - value: 500, - }], - decoded_actions: vec![RuntimeEffect::SetWorldFlag { - key: "world.chairman_condition_imported".to_string(), - value: true, - }], - executable_import_ready: true, - notes: vec!["chairman metric condition gates a world-side effect".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "chairman-condition-overlay", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import.state.event_runtime_records[0].conditions, - vec![RuntimeCondition::ChairmanNumericThreshold { - target: RuntimeChairmanTarget::SelectedChairman, - metric: crate::RuntimeChairmanMetric::CurrentCash, - comparator: RuntimeConditionComparator::Eq, - value: 500, - }] - ); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("chairman-gated trigger should execute"); - - assert_eq!( - import - .state - .world_flags - .get("world.chairman_condition_imported"), - Some(&true) - ); - } - - #[test] - fn overlays_book_value_conditions_into_imported_runtime_records() { - let base_state = RuntimeState { - calendar: CalendarPoint { - year: 1840, - month_slot: 1, - phase_slot: 2, - tick_slot: 3, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 2620, - investor_confidence: 37, - management_attitude: 58, - takeover_cooldown_year: Some(1839), - merger_cooldown_year: Some(1838), - }], - selected_company_id: Some(1), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 72, - live_record_count: 1, - live_entry_ids: vec![72], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 72, - payload_offset: Some(0x7202), - payload_len: Some(136), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 1, - standalone_condition_rows: vec![ - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: 2620, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Book Value Per Share".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Book Value Per Share == 2620".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - ], - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::SelectedCompanyOnly, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_world_flag_row( - 110, - "Disable Stock Buying and Selling", - true, - )], - decoded_conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - metric: crate::RuntimeCompanyMetric::BookValuePerShare, - comparator: RuntimeConditionComparator::Eq, - value: 2620, - }], - decoded_actions: vec![RuntimeEffect::SetWorldFlag { - key: "world.book_value_condition_imported".to_string(), - value: true, - }], - executable_import_ready: true, - notes: vec![ - "book value per share condition gates a world-side effect".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "company-book-value-condition-overlay", - None, - ) - .expect("overlay import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import.state.event_runtime_records[0].conditions, - vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::BookValuePerShare, - comparator: RuntimeConditionComparator::Eq, - value: 2620, - }] - ); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("company-governance trigger should execute"); - - assert_eq!( - import - .state - .world_flags - .get("world.book_value_condition_imported"), - Some(&true) - ); - } - - #[test] - fn imports_investor_confidence_condition_from_save_slice_company_context() { - let 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(), - 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: Some(save_company_roster()), - chairman_profile_table: Some(save_chairman_profile_table()), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 73, - live_record_count: 1, - live_entry_ids: vec![73], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 73, - payload_offset: Some(0x7202), - payload_len: Some(136), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 1, - standalone_condition_rows: vec![ - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: 2366, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&37_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Investor Confidence".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Investor Confidence == 37".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - ], - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::SelectedCompanyOnly, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_world_flag_row( - 110, - "Disable Stock Buying and Selling", - true, - )], - decoded_conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - metric: crate::RuntimeCompanyMetric::InvestorConfidence, - comparator: RuntimeConditionComparator::Eq, - value: 37, - }], - decoded_actions: vec![RuntimeEffect::SetWorldFlag { - key: "world.investor_confidence_condition_imported".to_string(), - value: true, - }], - executable_import_ready: true, - notes: vec![ - "investor confidence condition gates a world-side effect".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "company-investor-confidence-condition", - None, - ) - .expect("save-slice import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import.state.event_runtime_records[0].conditions, - vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::InvestorConfidence, - comparator: RuntimeConditionComparator::Eq, - value: 37, - }] - ); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("investor-confidence trigger should execute"); - - assert_eq!( - import - .state - .world_flags - .get("world.investor_confidence_condition_imported"), - Some(&true) - ); - } - - #[test] - fn imports_management_attitude_condition_from_save_slice_company_context() { - let 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(), - 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: Some(save_company_roster()), - chairman_profile_table: Some(save_chairman_profile_table()), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 74, - live_record_count: 1, - live_entry_ids: vec![74], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 74, - payload_offset: Some(0x7202), - payload_len: Some(136), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: vec![], - standalone_condition_row_count: 1, - standalone_condition_rows: vec![ - crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: 2369, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&58_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Management Attitude".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Management Attitude == 58".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }, - ], - negative_sentinel_scope: Some(company_negative_sentinel_scope( - RuntimeCompanyConditionTestScope::SelectedCompanyOnly, - )), - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_world_flag_row( - 110, - "Disable Stock Buying and Selling", - true, - )], - decoded_conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - metric: crate::RuntimeCompanyMetric::ManagementAttitude, - comparator: RuntimeConditionComparator::Eq, - value: 58, - }], - decoded_actions: vec![RuntimeEffect::SetWorldFlag { - key: "world.management_attitude_condition_imported".to_string(), - value: true, - }], - executable_import_ready: true, - notes: vec![ - "management attitude condition gates a world-side effect".to_string(), - ], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_to_runtime_state_import( - &save_slice, - "company-management-attitude-condition", - None, - ) - .expect("save-slice import should project"); - - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import.state.event_runtime_records[0].conditions, - vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::ManagementAttitude, - comparator: RuntimeConditionComparator::Eq, - value: 58, - }] - ); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("management-attitude trigger should execute"); - - assert_eq!( - import - .state - .world_flags - .get("world.management_attitude_condition_imported"), - Some(&true) - ); - } - - #[test] - fn overlays_recovered_world_toggle_batch_into_executable_runtime_record() { - let base_state = state(); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 25, - live_record_count: 1, - live_entry_ids: vec![25], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 25, - payload_offset: Some(0x7200), - payload_len: Some(168), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![3, 0, 0, 0], - grouped_effect_rows: vec![ - real_world_flag_row(111, "Disable Margin Buying/Short Selling Stock", true), - real_world_flag_row(120, "Disable All Track Building", true), - real_world_flag_row(131, "Disable Starting Any Companies", false), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![ - RuntimeEffect::SetWorldFlag { - key: "world.disable_margin_buying_short_selling_stock".to_string(), - value: true, - }, - RuntimeEffect::SetWorldFlag { - key: "world.disable_all_track_building".to_string(), - value: true, - }, - RuntimeEffect::SetWorldFlag { - key: "world.disable_starting_any_companies".to_string(), - value: false, - }, - ], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-world-toggle-batch-overlay", - None, - ) - .expect("overlay import should project"); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("trigger service should execute"); - assert_eq!( - import - .state - .world_flags - .get("world.disable_margin_buying_short_selling_stock"), - Some(&true) - ); - assert_eq!( - import - .state - .world_flags - .get("world.disable_all_track_building"), - Some(&true) - ); - assert_eq!( - import - .state - .world_flags - .get("world.disable_starting_any_companies"), - Some(&false) - ); - } - - #[test] - fn overlays_recovered_late_world_toggle_batch_into_executable_runtime_record() { - let base_state = state(); - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 26, - live_record_count: 1, - live_entry_ids: vec![26], - decoded_record_count: 1, - imported_runtime_record_count: 1, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 26, - payload_offset: Some(0x7200), - payload_len: Some(184), - decode_status: "executable".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![5, 0, 0, 0], - grouped_effect_rows: vec![ - real_world_flag_row(139, "Use Bio-Accelerator Cars", true), - real_world_flag_row(140, "Disable Cargo Economy", true), - real_world_flag_row(142, "Disable Train Crashes", false), - real_world_flag_row(143, "Disable Train Crashes AND Breakdowns", true), - real_world_flag_row(144, "AI Ignore Territories At Startup", true), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![ - RuntimeEffect::SetWorldFlag { - key: "world.use_bio_accelerator_cars".to_string(), - value: true, - }, - RuntimeEffect::SetWorldFlag { - key: "world.disable_cargo_economy".to_string(), - value: true, - }, - RuntimeEffect::SetWorldFlag { - key: "world.disable_train_crashes".to_string(), - value: false, - }, - RuntimeEffect::SetWorldFlag { - key: "world.disable_train_crashes_and_breakdowns".to_string(), - value: true, - }, - RuntimeEffect::SetWorldFlag { - key: "world.ai_ignore_territories_at_startup".to_string(), - value: true, - }, - ], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-late-world-toggle-batch-overlay", - None, - ) - .expect("overlay import should project"); - - crate::execute_step_command( - &mut import.state, - &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("trigger service should execute"); - assert_eq!( - import - .state - .world_flags - .get("world.use_bio_accelerator_cars"), - Some(&true) - ); - assert_eq!( - import.state.world_flags.get("world.disable_cargo_economy"), - Some(&true) - ); - assert_eq!( - import.state.world_flags.get("world.disable_train_crashes"), - Some(&false) - ); - assert_eq!( - import - .state - .world_flags - .get("world.disable_train_crashes_and_breakdowns"), - Some(&true) - ); - assert_eq!( - import - .state - .world_flags - .get("world.ai_ignore_territories_at_startup"), - Some(&true) - ); - } - - #[test] - fn overlays_real_confiscate_all_descriptor_into_executable_runtime_record() { - let base_state = RuntimeState { - companies: vec![ - crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - crate::RuntimeCompany { - company_id: 7, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 90, - debt: 10, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - selected_company_id: Some(42), - trains: vec![ - RuntimeTrain { - train_id: 1, - owner_company_id: 42, - territory_id: None, - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 2, - owner_company_id: 7, - territory_id: None, - locomotive_name: Some("Orca".to_string()), - active: true, - retired: false, - }, - ], - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 19, - live_record_count: 1, - live_entry_ids: vec![19], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 19, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_confiscate_all_row(true)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::ConfiscateCompanyAssets { - target: RuntimeCompanyTarget::SelectedCompany, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-confiscate-all-overlay", - None, - ) - .expect("overlay import should project"); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real confiscate-all descriptor should execute"); - - assert_eq!(import.state.companies[0].current_cash, 0); - assert_eq!(import.state.companies[0].debt, 0); - assert!(!import.state.companies[0].active); - assert_eq!(import.state.selected_company_id, None); - assert!(!import.state.trains[0].active); - assert!(import.state.trains[0].retired); - assert!(import.state.trains[1].active); - assert!(!import.state.trains[1].retired); - } - - #[test] - fn keeps_real_confiscate_all_false_row_parity_only() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 20, - live_record_count: 1, - live_entry_ids: vec![20], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 20, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(real_compact_control()), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_confiscate_all_row(false)], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "real-confiscate-all-false", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_confiscation_variant") - ); - } - - #[test] - fn blocks_confiscate_all_without_train_context() { - let base_state = RuntimeState { - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 24, - live_record_count: 1, - live_entry_ids: vec![24], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 24, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_confiscate_all_row(true)], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::ConfiscateCompanyAssets { - target: RuntimeCompanyTarget::SelectedCompany, - }], - executable_import_ready: true, - notes: vec![], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-confiscate-all-missing-trains", - None, - ) - .expect("overlay import should project"); - - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_train_context") - ); - assert!(import.state.event_runtime_records.is_empty()); - } - - #[test] - fn overlays_real_retire_train_descriptor_by_company_scope() { - let base_state = RuntimeState { - companies: vec![ - crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - crate::RuntimeCompany { - company_id: 7, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 90, - debt: 10, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - selected_company_id: Some(42), - trains: vec![ - RuntimeTrain { - train_id: 1, - owner_company_id: 42, - territory_id: Some(7), - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 2, - owner_company_id: 42, - territory_id: Some(8), - locomotive_name: Some("Orca".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 3, - owner_company_id: 7, - territory_id: Some(7), - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - ], - territories: vec![ - crate::RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - crate::RuntimeTerritory { - territory_id: 8, - name: Some("Great Plains".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - ], - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 21, - live_record_count: 1, - live_entry_ids: vec![21], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 21, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_retire_train_row(true, None, vec![])], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::RetireTrains { - company_target: Some(RuntimeCompanyTarget::SelectedCompany), - territory_target: None, - locomotive_name: None, - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-retire-train-company-overlay", - None, - ) - .expect("overlay import should project"); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("real retire-train descriptor should execute"); - - assert!(import.state.trains[0].retired); - assert!(import.state.trains[1].retired); - assert!(!import.state.trains[2].retired); - } - - #[test] - fn overlays_real_retire_train_descriptor_by_territory_and_locomotive_scope() { - let base_state = RuntimeState { - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - trains: vec![ - RuntimeTrain { - train_id: 1, - owner_company_id: 42, - territory_id: Some(7), - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 2, - owner_company_id: 42, - territory_id: Some(7), - locomotive_name: Some("Orca".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 3, - owner_company_id: 42, - territory_id: Some(8), - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - ], - territories: vec![ - crate::RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - crate::RuntimeTerritory { - territory_id: 8, - name: Some("Great Plains".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - ], - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 22, - live_record_count: 1, - live_entry_ids: vec![22], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 22, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![8, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_retire_train_row(true, Some("Mikado"), vec![])], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::RetireTrains { - company_target: None, - territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }), - locomotive_name: Some("Mikado".to_string()), - }], - executable_import_ready: true, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-retire-train-territory-overlay", - None, - ) - .expect("overlay import should project"); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("territory-scoped retire-train descriptor should execute"); - - assert!(import.state.trains[0].retired); - assert!(!import.state.trains[1].retired); - assert!(!import.state.trains[2].retired); - } - - #[test] - fn keeps_real_retire_train_missing_scope_parity_only() { - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 23, - live_record_count: 1, - live_entry_ids: vec![23], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 23, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![8, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_retire_train_row( - true, - Some("Mikado"), - vec!["retire train row is missing company and territory scope".to_string()], - )], - decoded_conditions: Vec::new(), - decoded_actions: vec![], - executable_import_ready: false, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_to_runtime_state_import( - &save_slice, - "real-retire-train-missing-scope", - None, - ) - .expect("save slice should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_retire_train_scope") - ); - } - - #[test] - fn blocks_retire_train_without_train_territory_context() { - let base_state = RuntimeState { - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - trains: vec![RuntimeTrain { - train_id: 1, - owner_company_id: 42, - territory_id: None, - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }], - territories: vec![crate::RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 25, - live_record_count: 1, - live_entry_ids: vec![25], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 25, - payload_offset: Some(0x7202), - payload_len: Some(120), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![8, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: vec![real_retire_train_row(true, Some("Mikado"), vec![])], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::RetireTrains { - company_target: None, - territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }), - locomotive_name: Some("Mikado".to_string()), - }], - executable_import_ready: true, - notes: vec![], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "real-retire-train-missing-train-territory", - None, - ) - .expect("overlay import should project"); - - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_missing_train_territory_context") - ); - assert!(import.state.event_runtime_records.is_empty()); - } - - #[test] - fn keeps_mixed_real_records_out_of_event_runtime_records() { - let base_state = RuntimeState { - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - ..state() - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 17, - live_record_count: 1, - live_entry_ids: vec![17], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 17, - payload_offset: Some(0x7202), - payload_len: Some(160), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: Some(false), - compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: vec![1, 1, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }), - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 1, 0, 0], - grouped_effect_rows: vec![ - real_track_capacity_row(18), - unsupported_real_grouped_row(), - ], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { - target: RuntimeCompanyTarget::SelectedCompany, - value: Some(18), - }], - executable_import_ready: false, - notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], - }], - }), - notes: vec![], - }; - - let import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "mixed-real-record-overlay", - None, - ) - .expect("overlay import should project"); - - assert!(import.state.event_runtime_records.is_empty()); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_confiscation_variant") - ); - } - - #[test] - fn overlays_save_slice_events_onto_base_company_context() { - let base_state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 2, - phase_slot: 1, - tick_slot: 3, - }, - world_flags: BTreeMap::from([("base.only".to_string(), true)]), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::from([("base.note".to_string(), "kept".to_string())]), - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 500, - debt: 20, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 1, - trigger_kind: 1, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![], - }], - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - periodic_boundary_calls: 9, - annual_finance_service_calls: 0, - periodic_route_preference_override_apply_count: 0, - periodic_route_preference_override_restore_count: 0, - trigger_dispatch_counts: BTreeMap::new(), - total_event_record_services: 4, - dirty_rerun_count: 2, - world_issue_opinion_base_terms_raw_i32: Vec::new(), - company_market_state: BTreeMap::new(), - company_periodic_side_latch_state: BTreeMap::new(), - active_periodic_route_preference_override: None, - last_periodic_route_preference_override: None, - annual_finance_last_actions: BTreeMap::new(), - annual_finance_action_counts: BTreeMap::new(), - annual_dividend_adjustment_commit_count: 0, - annual_bond_last_retired_principal_total: 0, - annual_bond_last_issued_principal_total: 0, - annual_stock_repurchase_last_share_count: 0, - annual_stock_issue_last_share_count: 0, - annual_finance_last_news_family_candidates: BTreeMap::new(), - annual_finance_last_news_events: Vec::new(), - chairman_issue_opinion_terms_raw_i32: BTreeMap::new(), - chairman_personality_raw_u8: BTreeMap::new(), - }, - }; - let 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 42, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(48), - decode_status: "parity_only".to_string(), - payload_family: "synthetic_harness".to_string(), - trigger_kind: Some(7), - active: Some(true), - marks_collection_dirty: Some(false), - one_shot: Some(false), - compact_control: None, - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: vec![], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { - target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, - delta: 50, - }], - executable_import_ready: false, - notes: vec!["needs company context".to_string()], - }], - }), - notes: vec![], - }; - - let mut import = project_save_slice_overlay_to_runtime_state_import( - &base_state, - &save_slice, - "overlay-smoke", - Some("overlay test".to_string()), - ) - .expect("overlay import should project"); - - assert_eq!(import.state.calendar, base_state.calendar); - assert_eq!(import.state.companies, base_state.companies); - assert_eq!(import.state.service_state, base_state.service_state); - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .map(|summary| summary.imported_runtime_record_count), - Some(1) - ); - assert_eq!( - import - .state - .packed_event_collection - .as_ref() - .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("imported") - ); - assert_eq!( - import - .state - .metadata - .get("save_slice.import_projection") - .map(String::as_str), - Some("overlay-runtime-restore-v1") - ); - assert_eq!( - import.state.metadata.get("base.note").map(String::as_str), - Some("kept") - ); - assert_eq!(import.state.world_flags.get("base.only"), Some(&true)); - - execute_step_command( - &mut import.state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("overlay-imported company-targeted record should run"); - - assert_eq!(import.state.companies[0].current_cash, 550); - } - - #[test] - fn loads_overlay_import_document_with_relative_paths() { - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system time should be after epoch") - .as_nanos(); - let fixture_dir = std::env::temp_dir().join(format!("rrt-overlay-import-{nonce}")); - std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created"); - - let snapshot_path = fixture_dir.join("base.json"); - let save_slice_path = fixture_dir.join("slice.json"); - let overlay_path = fixture_dir.join("overlay.json"); - - let snapshot = crate::RuntimeSnapshotDocument { - format_version: crate::SNAPSHOT_FORMAT_VERSION, - snapshot_id: "base".to_string(), - source: crate::RuntimeSnapshotSource { - source_fixture_id: None, - description: Some("base snapshot".to_string()), - }, - state: RuntimeState { - calendar: CalendarPoint { - year: 1835, - month_slot: 1, - phase_slot: 2, - tick_slot: 4, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 42, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(42), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }, - }; - crate::save_runtime_snapshot_document(&snapshot_path, &snapshot) - .expect("snapshot should save"); - - let save_slice_document = RuntimeSaveSliceDocument { - format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, - save_slice_id: "slice".to_string(), - source: RuntimeSaveSliceDocumentSource::default(), - 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(), - 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 1, - live_entry_ids: vec![7], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 0, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - control_lane_notes: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - records: vec![crate::SmpLoadedPackedEventRecordSummary { - record_index: 0, - live_entry_id: 7, - payload_offset: Some(0x7202), - payload_len: Some(48), - decode_status: "parity_only".to_string(), - payload_family: "synthetic_harness".to_string(), - trigger_kind: Some(7), - active: Some(true), - marks_collection_dirty: Some(false), - one_shot: Some(false), - compact_control: None, - text_bands: packed_text_bands(), - standalone_condition_row_count: 0, - standalone_condition_rows: vec![], - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: vec![], - decoded_conditions: Vec::new(), - decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { - target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, - delta: 50, - }], - executable_import_ready: false, - notes: vec!["needs company context".to_string()], - }], - }), - notes: vec![], - }, - }; - save_runtime_save_slice_document(&save_slice_path, &save_slice_document) - .expect("save slice should save"); - - let overlay = RuntimeOverlayImportDocument { - format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, - import_id: "overlay-relative".to_string(), - source: RuntimeOverlayImportDocumentSource { - description: Some("relative overlay".to_string()), - notes: vec![], - }, - base_snapshot_path: "base.json".to_string(), - save_slice_path: "slice.json".to_string(), - }; - save_runtime_overlay_import_document(&overlay_path, &overlay) - .expect("overlay document should save"); - - let import = - load_runtime_state_import(&overlay_path).expect("overlay runtime import should load"); - assert_eq!(import.import_id, "overlay-relative"); - assert_eq!(import.state.event_runtime_records.len(), 1); - assert_eq!(import.state.companies[0].company_id, 42); - - let _ = std::fs::remove_file(snapshot_path); - let _ = std::fs::remove_file(save_slice_path); - let _ = std::fs::remove_file(overlay_path); - let _ = std::fs::remove_dir(fixture_dir); - } -} diff --git a/crates/rrt-runtime/src/inspect.rs b/crates/rrt-runtime/src/inspect.rs new file mode 100644 index 0000000..af29c68 --- /dev/null +++ b/crates/rrt-runtime/src/inspect.rs @@ -0,0 +1,6 @@ +pub mod building; +pub mod campaign; +pub mod cargo; +pub mod pk4; +pub mod smp; +pub mod win; diff --git a/crates/rrt-runtime/src/inspect/building/bindings.rs b/crates/rrt-runtime/src/inspect/building/bindings.rs new file mode 100644 index 0000000..e86800b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/building/bindings.rs @@ -0,0 +1,51 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use serde::Deserialize; + +use super::scan::canonicalize_building_stem; +use super::types::{BuildingTypeNamedBindingComparison, BuildingTypeSourceEntry}; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +struct BuildingBindingArtifact { + bindings: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +struct BuildingBindingRow { + #[serde(default)] + candidate_name: Option, +} + +pub(super) fn load_named_binding_comparison( + bindings_path: &Path, + entries: &[BuildingTypeSourceEntry], +) -> Result> { + let artifact = + serde_json::from_str::(&fs::read_to_string(bindings_path)?)?; + let named_binding_stems = artifact + .bindings + .into_iter() + .filter_map(|binding| binding.candidate_name) + .map(|candidate_name| canonicalize_building_stem(&candidate_name)) + .collect::>(); + let source_stems = entries + .iter() + .map(|entry| entry.canonical_stem.clone()) + .collect::>(); + + Ok(BuildingTypeNamedBindingComparison { + bindings_path: bindings_path.display().to_string(), + named_binding_count: named_binding_stems.len(), + shared_canonical_stem_count: named_binding_stems.intersection(&source_stems).count(), + binding_only_canonical_stems: named_binding_stems + .difference(&source_stems) + .cloned() + .collect(), + source_only_canonical_stems: source_stems + .difference(&named_binding_stems) + .cloned() + .collect(), + }) +} diff --git a/crates/rrt-runtime/src/inspect/building/mod.rs b/crates/rrt-runtime/src/inspect/building/mod.rs new file mode 100644 index 0000000..f160c54 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/building/mod.rs @@ -0,0 +1,355 @@ +mod bindings; +mod probe; +mod recovered_tables; +mod scan; + +pub mod types; + +pub use scan::{inspect_building_types_dir, inspect_building_types_dir_with_bindings}; +pub use types::*; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn probes_bca_selector_bytes_from_fixed_offsets() { + let mut bytes = vec![0u8; 0xbc + 1]; + bytes[0xb8] = 0x12; + bytes[0xb9] = 0x34; + bytes[0xba] = 0x56; + bytes[0xbb] = 0x78; + let probe = probe::probe_bca_selector_bytes(&bytes); + assert_eq!(probe.byte_0xb8, 0x12); + assert_eq!(probe.byte_0xb9, 0x34); + assert_eq!(probe.byte_0xba, 0x56); + assert_eq!(probe.byte_0xbb, 0x78); + assert_eq!(probe.byte_0xb8_hex, "0x12"); + assert_eq!(probe.byte_0xbb_hex, "0x78"); + } + + #[test] + fn probes_bty_header_from_fixed_offsets() { + let mut bytes = vec![0u8; 0xc0]; + bytes[0x00..0x04].copy_from_slice(&0x03ebu32.to_le_bytes()); + bytes[0x04..0x04 + 5].copy_from_slice(b"Port\0"); + bytes[0x22..0x22 + 7].copy_from_slice(b"Cargo\0\0"); + bytes[0x40..0x40 + 6].copy_from_slice(b"Dock\0\0"); + bytes[0x5e..0x5e + 5].copy_from_slice(b"Sea\0\0"); + bytes[0x7c..0x7c + 6].copy_from_slice(b"Coast\0"); + bytes[0x9a..0x9a + 5].copy_from_slice(b"Port\0"); + bytes[0xb8] = 0x12; + bytes[0xb9] = 0x34; + bytes[0xba] = 0x56; + bytes[0xbb..0xbf].copy_from_slice(&0x89abcdefu32.to_le_bytes()); + + let probe = probe::probe_bty_header(&bytes); + assert_eq!(probe.type_id, 0x03eb); + assert_eq!(probe.type_id_hex, "0x000003eb"); + assert_eq!(probe.name_0x04, "Port"); + assert_eq!(probe.name_0x22, "Cargo"); + assert_eq!(probe.name_0x40, "Dock"); + assert_eq!(probe.name_0x5e, "Sea"); + assert_eq!(probe.name_0x7c, "Coast"); + assert_eq!(probe.name_0x9a, "Port"); + assert_eq!(probe.byte_0xb8_hex, "0x12"); + assert_eq!(probe.byte_0xb9_hex, "0x34"); + assert_eq!(probe.byte_0xba_hex, "0x56"); + assert_eq!(probe.dword_0xbb_hex, "0x89abcdef"); + } + + #[test] + fn summarizes_recovered_table_families_from_entries_and_files() { + let entries = vec![ + BuildingTypeSourceEntry { + canonical_stem: scan::canonicalize_building_stem("VictorianStationSml"), + raw_stems: vec!["VictorianStationSml".to_string()], + source_kinds: vec![BuildingTypeSourceKind::Bty], + file_names: vec!["VictorianStationSml.bty".to_string()], + }, + BuildingTypeSourceEntry { + canonical_stem: scan::canonicalize_building_stem("ClpBrdStationLrg"), + raw_stems: vec!["ClpbrdStationLrg".to_string()], + source_kinds: vec![BuildingTypeSourceKind::Bty], + file_names: vec!["ClpbrdStationLrg.bty".to_string()], + }, + BuildingTypeSourceEntry { + canonical_stem: scan::canonicalize_building_stem("Maintenance"), + raw_stems: vec!["Maintenance".to_string()], + source_kinds: vec![BuildingTypeSourceKind::Bty], + file_names: vec!["Maintenance.bty".to_string()], + }, + BuildingTypeSourceEntry { + canonical_stem: scan::canonicalize_building_stem("ServiceTower"), + raw_stems: vec!["ServiceTower".to_string()], + source_kinds: vec![BuildingTypeSourceKind::Bty], + file_names: vec!["ServiceTower.bty".to_string()], + }, + BuildingTypeSourceEntry { + canonical_stem: scan::canonicalize_building_stem("Port"), + raw_stems: vec!["Port".to_string()], + source_kinds: vec![BuildingTypeSourceKind::Bca, BuildingTypeSourceKind::Bty], + file_names: vec!["Port.bca".to_string(), "Port.bty".to_string()], + }, + ]; + let files = vec![ + BuildingTypeSourceFile { + file_name: "VictorianStationSml.bty".to_string(), + raw_stem: "VictorianStationSml".to_string(), + canonical_stem: scan::canonicalize_building_stem("VictorianStationSml"), + source_kind: BuildingTypeSourceKind::Bty, + byte_len: None, + bca_selector_probe: None, + bty_header_probe: Some(BuildingTypeBtyHeaderProbe { + type_id: 0x03ec, + type_id_hex: "0x000003ec".to_string(), + name_0x04: "VictorianStationSml".to_string(), + name_0x22: "VictorianStationSml".to_string(), + name_0x40: "VictorianStations".to_string(), + name_0x5e: "SmallTudorHouse".to_string(), + name_0x7c: "VictorianStations".to_string(), + name_0x9a: "VictorianStationSml".to_string(), + byte_0xb8: 0x06, + byte_0xb8_hex: "0x06".to_string(), + byte_0xb9: 0x06, + byte_0xb9_hex: "0x06".to_string(), + byte_0xba: 0x30, + byte_0xba_hex: "0x30".to_string(), + dword_0xbb: 0, + dword_0xbb_hex: "0x00000000".to_string(), + }), + }, + BuildingTypeSourceFile { + file_name: "ClpbrdStationLrg.bty".to_string(), + raw_stem: "ClpbrdStationLrg".to_string(), + canonical_stem: scan::canonicalize_building_stem("ClpbrdStationLrg"), + source_kind: BuildingTypeSourceKind::Bty, + byte_len: None, + bca_selector_probe: None, + bty_header_probe: Some(BuildingTypeBtyHeaderProbe { + type_id: 0x03ec, + type_id_hex: "0x000003ec".to_string(), + name_0x04: "ClpbrdStationLrg".to_string(), + name_0x22: "ClpbrdStationLrg".to_string(), + name_0x40: "ClpBrdStations".to_string(), + name_0x5e: "SmallTudorHouse".to_string(), + name_0x7c: "ClpBrdStations".to_string(), + name_0x9a: "ClpbrdStationLrg".to_string(), + byte_0xb8: 0x06, + byte_0xb8_hex: "0x06".to_string(), + byte_0xb9: 0x06, + byte_0xb9_hex: "0x06".to_string(), + byte_0xba: 0x30, + byte_0xba_hex: "0x30".to_string(), + dword_0xbb: 0, + dword_0xbb_hex: "0x00000000".to_string(), + }), + }, + BuildingTypeSourceFile { + file_name: "Maintenance.bty".to_string(), + raw_stem: "Maintenance".to_string(), + canonical_stem: scan::canonicalize_building_stem("Maintenance"), + source_kind: BuildingTypeSourceKind::Bty, + byte_len: None, + bca_selector_probe: None, + bty_header_probe: Some(BuildingTypeBtyHeaderProbe { + type_id: 0x03ec, + type_id_hex: "0x000003ec".to_string(), + name_0x04: "Maintenance".to_string(), + name_0x22: "Maintenance".to_string(), + name_0x40: "Maintenance Facility".to_string(), + name_0x5e: "200FtRulerCross".to_string(), + name_0x7c: "Maintenance Facility".to_string(), + name_0x9a: "Maintenance".to_string(), + byte_0xb8: 0x06, + byte_0xb8_hex: "0x06".to_string(), + byte_0xb9: 0x06, + byte_0xb9_hex: "0x06".to_string(), + byte_0xba: 0x30, + byte_0xba_hex: "0x30".to_string(), + dword_0xbb: 0, + dword_0xbb_hex: "0x00000000".to_string(), + }), + }, + BuildingTypeSourceFile { + file_name: "ServiceTower.bty".to_string(), + raw_stem: "ServiceTower".to_string(), + canonical_stem: scan::canonicalize_building_stem("ServiceTower"), + source_kind: BuildingTypeSourceKind::Bty, + byte_len: None, + bca_selector_probe: None, + bty_header_probe: Some(BuildingTypeBtyHeaderProbe { + type_id: 0x03ec, + type_id_hex: "0x000003ec".to_string(), + name_0x04: "ServiceTower".to_string(), + name_0x22: "ServiceTower".to_string(), + name_0x40: "Service Tower".to_string(), + name_0x5e: "200FtRulerCross".to_string(), + name_0x7c: "Service Tower".to_string(), + name_0x9a: "ServiceTower".to_string(), + byte_0xb8: 0x06, + byte_0xb8_hex: "0x06".to_string(), + byte_0xb9: 0x06, + byte_0xb9_hex: "0x06".to_string(), + byte_0xba: 0x30, + byte_0xba_hex: "0x30".to_string(), + dword_0xbb: 0, + dword_0xbb_hex: "0x00000000".to_string(), + }), + }, + BuildingTypeSourceFile { + file_name: "Port.bty".to_string(), + raw_stem: "Port".to_string(), + canonical_stem: scan::canonicalize_building_stem("Port"), + source_kind: BuildingTypeSourceKind::Bty, + byte_len: None, + bca_selector_probe: None, + bty_header_probe: Some(BuildingTypeBtyHeaderProbe { + type_id: 0x03ec, + type_id_hex: "0x000003ec".to_string(), + name_0x04: "Port".to_string(), + name_0x22: "Port".to_string(), + name_0x40: "Port".to_string(), + name_0x5e: "TextileMill".to_string(), + name_0x7c: "Port".to_string(), + name_0x9a: "Port".to_string(), + byte_0xb8: 0x06, + byte_0xb8_hex: "0x06".to_string(), + byte_0xb9: 0x06, + byte_0xb9_hex: "0x06".to_string(), + byte_0xba: 0x30, + byte_0xba_hex: "0x30".to_string(), + dword_0xbb: 0x01f4, + dword_0xbb_hex: "0x000001f4".to_string(), + }), + }, + BuildingTypeSourceFile { + file_name: "Port.bca".to_string(), + raw_stem: "Port".to_string(), + canonical_stem: scan::canonicalize_building_stem("Port"), + source_kind: BuildingTypeSourceKind::Bca, + byte_len: None, + bca_selector_probe: Some(BuildingTypeBcaSelectorProbe { + byte_0xb8: 0x00, + byte_0xb8_hex: "0x00".to_string(), + byte_0xb9: 0x00, + byte_0xb9_hex: "0x00".to_string(), + byte_0xba: 0x00, + byte_0xba_hex: "0x00".to_string(), + byte_0xbb: 0x00, + byte_0xbb_hex: "0x00".to_string(), + }), + bty_header_probe: None, + }, + ]; + + let summary = recovered_tables::summarize_recovered_table_families(&entries, &files); + assert!( + summary + .present_style_station_entries + .contains(&"VictorianStationSml".to_string()) + ); + assert!( + summary + .present_style_station_entries + .contains(&"ClpbrdStationLrg".to_string()) + ); + assert_eq!( + summary.present_standalone_entries, + vec!["Maintenance".to_string(), "ServiceTower".to_string()] + ); + assert_eq!(summary.recovered_source_family_summaries.len(), 4); + assert!(summary.recovered_source_family_summaries.iter().any(|row| { + row.canonical_stem == scan::canonicalize_building_stem("Maintenance") + && row.name_0x40 == "Maintenance Facility" + && row.dword_0xbb_hex == "0x00000000" + && row.byte_0xba_hex.is_none() + })); + assert!(summary.recovered_source_family_summaries.iter().any(|row| { + row.canonical_stem == scan::canonicalize_building_stem("VictorianStationSml") + && row.name_0x40 == "VictorianStations" + && row.dword_0xbb_hex == "0x00000000" + })); + assert_eq!( + summary.bare_port_warehouse_files, + vec!["Port.bca".to_string(), "Port.bty".to_string()] + ); + assert_eq!(summary.nonzero_bty_header_dword_summaries.len(), 1); + assert_eq!( + summary.nonzero_bty_header_dword_summaries[0].dword_0xbb_hex, + "0x000001f4" + ); + assert_eq!( + summary.nonzero_bty_header_dword_summaries[0].sample_file_names, + vec!["Port.bty".to_string()] + ); + assert_eq!( + summary.nonzero_bty_header_name_0x40_summaries, + vec![BuildingTypeBtyHeaderNameSummary { + header_offset_hex: "0x40".to_string(), + header_value: "Port".to_string(), + file_count: 1, + sample_file_names: vec!["Port.bty".to_string()], + }] + ); + assert_eq!( + summary.nonzero_bty_header_name_0x5e_summaries, + vec![BuildingTypeBtyHeaderNameSummary { + header_offset_hex: "0x5e".to_string(), + header_value: "TextileMill".to_string(), + file_count: 1, + sample_file_names: vec!["Port.bty".to_string()], + }] + ); + assert_eq!( + summary.nonzero_bty_header_name_0x7c_summaries, + vec![BuildingTypeBtyHeaderNameSummary { + header_offset_hex: "0x7c".to_string(), + header_value: "Port".to_string(), + file_count: 1, + sample_file_names: vec!["Port.bty".to_string()], + }] + ); + assert!( + summary + .bty_header_name_0x5e_dword_summaries + .iter() + .any(|row| { + row.header_offset_hex == "0x5e" + && row.header_value == "TextileMill" + && row.dword_0xbb == 0x01f4 + && row.file_count == 1 + && row.sample_file_names == vec!["Port.bty".to_string()] + }) + ); + assert_eq!( + summary.nonzero_bty_header_alias_selector_summaries, + vec![BuildingTypeBtyHeaderAliasSelectorSummary { + name_0x40: "Port".to_string(), + name_0x5e: "TextileMill".to_string(), + name_0x7c: "Port".to_string(), + dword_0xbb: 0x01f4, + dword_0xbb_hex: "0x000001f4".to_string(), + byte_0xb8_hex: "0x00".to_string(), + byte_0xb9_hex: "0x00".to_string(), + byte_0xba_hex: "0x00".to_string(), + byte_0xbb_hex: "0x00".to_string(), + file_count: 1, + sample_file_names: vec!["Port.bty".to_string()], + }] + ); + assert_eq!( + summary.bty_name_0x5e_bca_selector_summaries, + vec![BuildingTypeBtyNameBcaSelectorSummary { + header_offset_hex: "0x5e".to_string(), + header_value: "TextileMill".to_string(), + dword_0xbb: 0x01f4, + dword_0xbb_hex: "0x000001f4".to_string(), + byte_0xba_hex: "0x00".to_string(), + byte_0xbb_hex: "0x00".to_string(), + file_count: 1, + sample_file_names: vec!["Port.bty".to_string()], + }] + ); + } +} diff --git a/crates/rrt-runtime/src/inspect/building/probe.rs b/crates/rrt-runtime/src/inspect/building/probe.rs new file mode 100644 index 0000000..836c607 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/building/probe.rs @@ -0,0 +1,63 @@ +use super::types::{BuildingTypeBcaSelectorProbe, BuildingTypeBtyHeaderProbe}; + +pub(super) fn probe_bca_selector_bytes(bytes: &[u8]) -> BuildingTypeBcaSelectorProbe { + let byte_0xb8 = bytes.get(0xb8).copied().unwrap_or(0); + let byte_0xb9 = bytes.get(0xb9).copied().unwrap_or(0); + let byte_0xba = bytes.get(0xba).copied().unwrap_or(0); + let byte_0xbb = bytes.get(0xbb).copied().unwrap_or(0); + BuildingTypeBcaSelectorProbe { + byte_0xb8, + byte_0xb8_hex: format!("0x{byte_0xb8:02x}"), + byte_0xb9, + byte_0xb9_hex: format!("0x{byte_0xb9:02x}"), + byte_0xba, + byte_0xba_hex: format!("0x{byte_0xba:02x}"), + byte_0xbb, + byte_0xbb_hex: format!("0x{byte_0xbb:02x}"), + } +} + +pub(super) fn probe_bty_header(bytes: &[u8]) -> BuildingTypeBtyHeaderProbe { + let type_id = read_u32_le(bytes, 0x00); + let byte_0xb8 = bytes.get(0xb8).copied().unwrap_or(0); + let byte_0xb9 = bytes.get(0xb9).copied().unwrap_or(0); + let byte_0xba = bytes.get(0xba).copied().unwrap_or(0); + let dword_0xbb = read_u32_le(bytes, 0xbb); + BuildingTypeBtyHeaderProbe { + type_id, + type_id_hex: format!("0x{type_id:08x}"), + name_0x04: read_c_string(bytes, 0x04, 0x1e), + name_0x22: read_c_string(bytes, 0x22, 0x1e), + name_0x40: read_c_string(bytes, 0x40, 0x1e), + name_0x5e: read_c_string(bytes, 0x5e, 0x1e), + name_0x7c: read_c_string(bytes, 0x7c, 0x1e), + name_0x9a: read_c_string(bytes, 0x9a, 0x1e), + byte_0xb8, + byte_0xb8_hex: format!("0x{byte_0xb8:02x}"), + byte_0xb9, + byte_0xb9_hex: format!("0x{byte_0xb9:02x}"), + byte_0xba, + byte_0xba_hex: format!("0x{byte_0xba:02x}"), + dword_0xbb, + dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), + } +} + +fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { + bytes + .get(offset..offset + 4) + .and_then(|slice| <[u8; 4]>::try_from(slice).ok()) + .map(u32::from_le_bytes) + .unwrap_or(0) +} + +fn read_c_string(bytes: &[u8], offset: usize, max_len: usize) -> String { + let Some(slice) = bytes.get(offset..offset.saturating_add(max_len)) else { + return String::new(); + }; + let end = slice + .iter() + .position(|byte| *byte == 0) + .unwrap_or(slice.len()); + String::from_utf8_lossy(&slice[..end]).into_owned() +} diff --git a/crates/rrt-runtime/src/inspect/building/recovered_tables.rs b/crates/rrt-runtime/src/inspect/building/recovered_tables.rs new file mode 100644 index 0000000..6b2da7a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/building/recovered_tables.rs @@ -0,0 +1,475 @@ +use std::collections::BTreeMap; + +use super::scan::canonicalize_building_stem; +use super::types::{ + BuildingTypeBtyHeaderAliasSelectorSummary, BuildingTypeBtyHeaderDwordSummary, + BuildingTypeBtyHeaderNameDwordSummary, BuildingTypeBtyHeaderNameSummary, + BuildingTypeBtyNameBcaSelectorSummary, BuildingTypeRecoveredSourceFamilySummary, + BuildingTypeRecoveredTableSummary, BuildingTypeSourceEntry, BuildingTypeSourceFile, + BuildingTypeSourceKind, +}; + +pub(super) fn summarize_recovered_table_families( + entries: &[BuildingTypeSourceEntry], + files: &[BuildingTypeSourceFile], +) -> BuildingTypeRecoveredTableSummary { + const RECOVERED_STYLE_THEMES: [&str; 6] = + ["Victorian", "Tudor", "SoWest", "Persian", "Kyoto", "ClpBrd"]; + const RECOVERED_SOURCE_KINDS: [&str; 5] = [ + "StationSml", + "StationMed", + "StationLrg", + "ServiceTower", + "Maintenance", + ]; + + let entry_by_canonical = entries + .iter() + .map(|entry| (entry.canonical_stem.clone(), entry)) + .collect::>(); + + let mut present_style_station_entries = Vec::new(); + for style in RECOVERED_STYLE_THEMES { + for source_kind in ["StationSml", "StationMed", "StationLrg"] { + let canonical = canonicalize_building_stem(&format!("{style}{source_kind}")); + if let Some(entry) = entry_by_canonical.get(&canonical) { + if let Some(raw_stem) = entry.raw_stems.first() { + present_style_station_entries.push(raw_stem.clone()); + } + } + } + } + present_style_station_entries.sort(); + present_style_station_entries.dedup(); + + let mut present_standalone_entries = Vec::new(); + for raw_name in ["ServiceTower", "Maintenance"] { + let canonical = canonicalize_building_stem(raw_name); + if let Some(entry) = entry_by_canonical.get(&canonical) { + if let Some(raw_stem) = entry.raw_stems.first() { + present_standalone_entries.push(raw_stem.clone()); + } + } + } + present_standalone_entries.sort(); + present_standalone_entries.dedup(); + + let recovered_source_family_summaries = summarize_recovered_source_family_rows( + entries, + files, + &present_style_station_entries, + &present_standalone_entries, + ); + + let mut bare_port_warehouse_files = files + .iter() + .filter(|file| matches!(file.canonical_stem.as_str(), "port" | "warehouse")) + .map(|file| file.file_name.clone()) + .collect::>(); + bare_port_warehouse_files.sort(); + bare_port_warehouse_files.dedup(); + + let mut nonzero_bty_header_dword_groups = BTreeMap::>::new(); + for file in files { + let Some(probe) = &file.bty_header_probe else { + continue; + }; + if probe.dword_0xbb == 0 { + continue; + } + nonzero_bty_header_dword_groups + .entry(probe.dword_0xbb) + .or_default() + .push(file.file_name.clone()); + } + let nonzero_bty_header_dword_summaries = nonzero_bty_header_dword_groups + .into_iter() + .map(|(dword_0xbb, mut file_names)| { + file_names.sort(); + file_names.dedup(); + BuildingTypeBtyHeaderDwordSummary { + dword_0xbb, + dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), + file_count: file_names.len(), + sample_file_names: file_names.into_iter().take(24).collect(), + } + }) + .collect(); + + let nonzero_bty_header_name_0x40_summaries = + summarize_nonzero_bty_header_name_lane(files, 0x40, |probe| &probe.name_0x40); + let nonzero_bty_header_name_0x5e_summaries = + summarize_nonzero_bty_header_name_lane(files, 0x5e, |probe| &probe.name_0x5e); + let nonzero_bty_header_name_0x7c_summaries = + summarize_nonzero_bty_header_name_lane(files, 0x7c, |probe| &probe.name_0x7c); + let nonzero_bty_header_alias_selector_summaries = + summarize_nonzero_bty_header_alias_selector_patterns(entries, files); + let bty_header_name_0x5e_dword_summaries = + summarize_bty_header_name_lane_by_dword(files, 0x5e, |probe| &probe.name_0x5e); + let bty_name_0x5e_bca_selector_summaries = + summarize_bty_name_0x5e_bca_selector_patterns(entries, files); + + BuildingTypeRecoveredTableSummary { + recovered_style_themes: RECOVERED_STYLE_THEMES + .into_iter() + .map(str::to_string) + .collect(), + recovered_source_kinds: RECOVERED_SOURCE_KINDS + .into_iter() + .map(str::to_string) + .collect(), + present_style_station_entries, + present_standalone_entries, + recovered_source_family_summaries, + bare_port_warehouse_files, + nonzero_bty_header_dword_summaries, + nonzero_bty_header_name_0x40_summaries, + nonzero_bty_header_name_0x5e_summaries, + nonzero_bty_header_name_0x7c_summaries, + nonzero_bty_header_alias_selector_summaries, + bty_header_name_0x5e_dword_summaries, + bty_name_0x5e_bca_selector_summaries, + } +} + +fn summarize_nonzero_bty_header_name_lane( + files: &[BuildingTypeSourceFile], + offset: u32, + selector: impl Fn(&super::types::BuildingTypeBtyHeaderProbe) -> &String, +) -> Vec { + let mut groups = BTreeMap::>::new(); + for file in files { + let Some(probe) = &file.bty_header_probe else { + continue; + }; + if probe.dword_0xbb == 0 { + continue; + } + let header_value = selector(probe).trim(); + if header_value.is_empty() { + continue; + } + groups + .entry(header_value.to_string()) + .or_default() + .push(file.file_name.clone()); + } + + let mut summaries = groups + .into_iter() + .map(|(header_value, mut file_names)| { + file_names.sort(); + file_names.dedup(); + BuildingTypeBtyHeaderNameSummary { + header_offset_hex: format!("0x{offset:02x}"), + header_value, + file_count: file_names.len(), + sample_file_names: file_names.into_iter().take(24).collect(), + } + }) + .collect::>(); + summaries.sort_by(|left, right| { + right + .file_count + .cmp(&left.file_count) + .then_with(|| left.header_offset_hex.cmp(&right.header_offset_hex)) + .then_with(|| left.header_value.cmp(&right.header_value)) + }); + summaries +} + +fn summarize_recovered_source_family_rows( + entries: &[BuildingTypeSourceEntry], + files: &[BuildingTypeSourceFile], + present_style_station_entries: &[String], + present_standalone_entries: &[String], +) -> Vec { + let file_by_name = files + .iter() + .map(|file| (file.file_name.as_str(), file)) + .collect::>(); + + let mut canonical_stems = present_style_station_entries + .iter() + .chain(present_standalone_entries.iter()) + .map(|raw_stem| canonicalize_building_stem(raw_stem)) + .collect::>(); + canonical_stems.sort(); + canonical_stems.dedup(); + + let mut summaries = Vec::new(); + for canonical_stem in canonical_stems { + let Some(entry) = entries + .iter() + .find(|entry| entry.canonical_stem == canonical_stem) + else { + continue; + }; + let bty_file = entry + .file_names + .iter() + .filter_map(|name| file_by_name.get(name.as_str())) + .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bty)); + let Some(bty_file) = bty_file else { + continue; + }; + let Some(bty_probe) = &bty_file.bty_header_probe else { + continue; + }; + let bca_file = entry + .file_names + .iter() + .filter_map(|name| file_by_name.get(name.as_str())) + .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bca)); + let bca_probe = bca_file.and_then(|file| file.bca_selector_probe.as_ref()); + summaries.push(BuildingTypeRecoveredSourceFamilySummary { + canonical_stem: canonical_stem.clone(), + sample_raw_stem: entry + .raw_stems + .first() + .cloned() + .unwrap_or_else(|| canonical_stem.clone()), + has_bca_pair: bca_file.is_some(), + type_id_hex: bty_probe.type_id_hex.clone(), + name_0x40: bty_probe.name_0x40.clone(), + name_0x5e: bty_probe.name_0x5e.clone(), + name_0x7c: bty_probe.name_0x7c.clone(), + dword_0xbb_hex: bty_probe.dword_0xbb_hex.clone(), + byte_0xba_hex: bca_probe.map(|probe| probe.byte_0xba_hex.clone()), + byte_0xbb_hex: bca_probe.map(|probe| probe.byte_0xbb_hex.clone()), + }); + } + summaries.sort_by(|left, right| { + left.canonical_stem + .cmp(&right.canonical_stem) + .then_with(|| left.sample_raw_stem.cmp(&right.sample_raw_stem)) + }); + summaries +} + +fn summarize_bty_header_name_lane_by_dword( + files: &[BuildingTypeSourceFile], + offset: u32, + selector: impl Fn(&super::types::BuildingTypeBtyHeaderProbe) -> &String, +) -> Vec { + let mut groups = BTreeMap::<(String, u32), Vec>::new(); + for file in files { + let Some(probe) = &file.bty_header_probe else { + continue; + }; + let header_value = selector(probe).trim(); + if header_value.is_empty() { + continue; + } + groups + .entry((header_value.to_string(), probe.dword_0xbb)) + .or_default() + .push(file.file_name.clone()); + } + + let mut summaries = groups + .into_iter() + .map(|((header_value, dword_0xbb), mut file_names)| { + file_names.sort(); + file_names.dedup(); + BuildingTypeBtyHeaderNameDwordSummary { + header_offset_hex: format!("0x{offset:02x}"), + header_value, + dword_0xbb, + dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), + file_count: file_names.len(), + sample_file_names: file_names.into_iter().take(24).collect(), + } + }) + .collect::>(); + summaries.sort_by(|left, right| { + right + .file_count + .cmp(&left.file_count) + .then_with(|| left.dword_0xbb.cmp(&right.dword_0xbb)) + .then_with(|| left.header_offset_hex.cmp(&right.header_offset_hex)) + .then_with(|| left.header_value.cmp(&right.header_value)) + }); + summaries +} + +fn summarize_bty_name_0x5e_bca_selector_patterns( + entries: &[BuildingTypeSourceEntry], + files: &[BuildingTypeSourceFile], +) -> Vec { + let file_by_name = files + .iter() + .map(|file| (file.file_name.as_str(), file)) + .collect::>(); + let mut groups = BTreeMap::<(String, u32, String, String), Vec>::new(); + + for entry in entries { + let bty_file = entry + .file_names + .iter() + .filter_map(|name| file_by_name.get(name.as_str())) + .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bty)); + let bca_file = entry + .file_names + .iter() + .filter_map(|name| file_by_name.get(name.as_str())) + .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bca)); + let (Some(bty_file), Some(bca_file)) = (bty_file, bca_file) else { + continue; + }; + let (Some(bty_probe), Some(bca_probe)) = + (&bty_file.bty_header_probe, &bca_file.bca_selector_probe) + else { + continue; + }; + let header_value = bty_probe.name_0x5e.trim(); + if header_value.is_empty() { + continue; + } + groups + .entry(( + header_value.to_string(), + bty_probe.dword_0xbb, + bca_probe.byte_0xba_hex.clone(), + bca_probe.byte_0xbb_hex.clone(), + )) + .or_default() + .push(bty_file.file_name.clone()); + } + + let mut summaries = groups + .into_iter() + .map( + |((header_value, dword_0xbb, byte_0xba_hex, byte_0xbb_hex), mut file_names)| { + file_names.sort(); + file_names.dedup(); + BuildingTypeBtyNameBcaSelectorSummary { + header_offset_hex: "0x5e".to_string(), + header_value, + dword_0xbb, + dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), + byte_0xba_hex, + byte_0xbb_hex, + file_count: file_names.len(), + sample_file_names: file_names.into_iter().take(24).collect(), + } + }, + ) + .collect::>(); + summaries.sort_by(|left, right| { + right + .file_count + .cmp(&left.file_count) + .then_with(|| left.dword_0xbb.cmp(&right.dword_0xbb)) + .then_with(|| left.header_value.cmp(&right.header_value)) + .then_with(|| left.byte_0xba_hex.cmp(&right.byte_0xba_hex)) + .then_with(|| left.byte_0xbb_hex.cmp(&right.byte_0xbb_hex)) + }); + summaries +} + +fn summarize_nonzero_bty_header_alias_selector_patterns( + entries: &[BuildingTypeSourceEntry], + files: &[BuildingTypeSourceFile], +) -> Vec { + let file_by_name = files + .iter() + .map(|file| (file.file_name.as_str(), file)) + .collect::>(); + let mut groups = BTreeMap::< + (String, String, String, u32, String, String, String, String), + Vec, + >::new(); + + for entry in entries { + let bty_file = entry + .file_names + .iter() + .filter_map(|name| file_by_name.get(name.as_str())) + .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bty)); + let bca_file = entry + .file_names + .iter() + .filter_map(|name| file_by_name.get(name.as_str())) + .find(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bca)); + let (Some(bty_file), Some(bca_file)) = (bty_file, bca_file) else { + continue; + }; + let (Some(bty_probe), Some(bca_probe)) = + (&bty_file.bty_header_probe, &bca_file.bca_selector_probe) + else { + continue; + }; + if bty_probe.dword_0xbb == 0 { + continue; + } + let name_0x40 = bty_probe.name_0x40.trim(); + let name_0x5e = bty_probe.name_0x5e.trim(); + let name_0x7c = bty_probe.name_0x7c.trim(); + if name_0x40.is_empty() || name_0x5e.is_empty() || name_0x7c.is_empty() { + continue; + } + groups + .entry(( + name_0x40.to_string(), + name_0x5e.to_string(), + name_0x7c.to_string(), + bty_probe.dword_0xbb, + bca_probe.byte_0xb8_hex.clone(), + bca_probe.byte_0xb9_hex.clone(), + bca_probe.byte_0xba_hex.clone(), + bca_probe.byte_0xbb_hex.clone(), + )) + .or_default() + .push(bty_file.file_name.clone()); + } + + let mut summaries = groups + .into_iter() + .map( + |( + ( + name_0x40, + name_0x5e, + name_0x7c, + dword_0xbb, + byte_0xb8_hex, + byte_0xb9_hex, + byte_0xba_hex, + byte_0xbb_hex, + ), + mut file_names, + )| { + file_names.sort(); + file_names.dedup(); + BuildingTypeBtyHeaderAliasSelectorSummary { + name_0x40, + name_0x5e, + name_0x7c, + dword_0xbb, + dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), + byte_0xb8_hex, + byte_0xb9_hex, + byte_0xba_hex, + byte_0xbb_hex, + file_count: file_names.len(), + sample_file_names: file_names.into_iter().take(24).collect(), + } + }, + ) + .collect::>(); + summaries.sort_by(|left, right| { + right + .file_count + .cmp(&left.file_count) + .then_with(|| left.dword_0xbb.cmp(&right.dword_0xbb)) + .then_with(|| left.name_0x40.cmp(&right.name_0x40)) + .then_with(|| left.name_0x5e.cmp(&right.name_0x5e)) + .then_with(|| left.name_0x7c.cmp(&right.name_0x7c)) + .then_with(|| left.byte_0xb8_hex.cmp(&right.byte_0xb8_hex)) + .then_with(|| left.byte_0xb9_hex.cmp(&right.byte_0xb9_hex)) + .then_with(|| left.byte_0xba_hex.cmp(&right.byte_0xba_hex)) + .then_with(|| left.byte_0xbb_hex.cmp(&right.byte_0xbb_hex)) + }); + summaries +} diff --git a/crates/rrt-runtime/src/inspect/building/scan.rs b/crates/rrt-runtime/src/inspect/building/scan.rs new file mode 100644 index 0000000..375a721 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/building/scan.rs @@ -0,0 +1,184 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; + +use super::bindings::load_named_binding_comparison; +use super::probe::{probe_bca_selector_bytes, probe_bty_header}; +use super::recovered_tables::summarize_recovered_table_families; +use super::types::{ + BuildingTypeBcaSelectorPatternSummary, BuildingTypeSourceEntry, BuildingTypeSourceFile, + BuildingTypeSourceKind, BuildingTypeSourceReport, +}; + +pub fn inspect_building_types_dir( + path: &Path, +) -> Result> { + inspect_building_types_dir_with_bindings(path, None) +} + +pub fn inspect_building_types_dir_with_bindings( + path: &Path, + bindings_path: Option<&Path>, +) -> Result> { + let mut files = Vec::new(); + for entry in fs::read_dir(path)? { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + let file_name = entry.file_name().to_string_lossy().into_owned(); + let Some(extension) = Path::new(&file_name) + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.to_ascii_lowercase()) + else { + continue; + }; + let source_kind = match extension.as_str() { + "bca" => BuildingTypeSourceKind::Bca, + "bty" => BuildingTypeSourceKind::Bty, + _ => continue, + }; + let bytes = fs::read(entry.path())?; + let raw_stem = Path::new(&file_name) + .file_stem() + .and_then(|stem| stem.to_str()) + .unwrap_or("") + .to_string(); + if raw_stem.is_empty() { + continue; + } + files.push(BuildingTypeSourceFile { + file_name, + canonical_stem: canonicalize_building_stem(&raw_stem), + raw_stem, + source_kind: source_kind.clone(), + byte_len: Some(bytes.len()), + bca_selector_probe: match source_kind { + BuildingTypeSourceKind::Bca => Some(probe_bca_selector_bytes(&bytes)), + BuildingTypeSourceKind::Bty => None, + }, + bty_header_probe: match source_kind { + BuildingTypeSourceKind::Bca => None, + BuildingTypeSourceKind::Bty => Some(probe_bty_header(&bytes)), + }, + }); + } + + files.sort_by(|left, right| { + left.canonical_stem + .cmp(&right.canonical_stem) + .then_with(|| left.file_name.cmp(&right.file_name)) + }); + + let mut grouped = BTreeMap::>::new(); + for file in &files { + grouped + .entry(file.canonical_stem.clone()) + .or_default() + .push(file); + } + + let entries = grouped + .into_iter() + .map(|(canonical_stem, group)| BuildingTypeSourceEntry { + canonical_stem, + raw_stems: group + .iter() + .map(|file| file.raw_stem.clone()) + .collect::>() + .into_iter() + .collect(), + source_kinds: group + .iter() + .map(|file| file.source_kind.clone()) + .collect::>() + .into_iter() + .collect(), + file_names: group + .iter() + .map(|file| file.file_name.clone()) + .collect::>() + .into_iter() + .collect(), + }) + .collect::>(); + + let bca_file_count = files + .iter() + .filter(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bca)) + .count(); + let bty_file_count = files + .iter() + .filter(|file| matches!(file.source_kind, BuildingTypeSourceKind::Bty)) + .count(); + let mut grouped_selector_patterns = + BTreeMap::<(usize, String, String, String, String), Vec>::new(); + for file in &files { + let Some(probe) = &file.bca_selector_probe else { + continue; + }; + grouped_selector_patterns + .entry(( + file.byte_len.unwrap_or_default(), + probe.byte_0xb8_hex.clone(), + probe.byte_0xb9_hex.clone(), + probe.byte_0xba_hex.clone(), + probe.byte_0xbb_hex.clone(), + )) + .or_default() + .push(file.file_name.clone()); + } + let bca_selector_patterns = grouped_selector_patterns + .into_iter() + .map( + |( + (byte_len, byte_0xb8_hex, byte_0xb9_hex, byte_0xba_hex, byte_0xbb_hex), + file_names, + )| BuildingTypeBcaSelectorPatternSummary { + byte_len, + byte_0xb8_hex, + byte_0xb9_hex, + byte_0xba_hex, + byte_0xbb_hex, + file_count: file_names.len(), + sample_file_names: file_names.into_iter().take(12).collect(), + }, + ) + .collect::>(); + + let notes = vec![ + "BuildingTypes sources are grouped by a canonical stem that lowercases and strips spaces, underscores, and hyphens so paired .bca/.bty variants collapse onto one asset token.".to_string(), + "This report is an offline asset-pool view only; it does not by itself assign live candidate ids or prove scenario candidate-table availability.".to_string(), + "For .bca files, the report also exposes the narrow selector-byte window at offsets 0xb8..0xbb used by the grounded aux-candidate and live-candidate stream decoders.".to_string(), + "The recovered stock table above the Tier-2 building seam combines one style/theme subset with one source-kind table; this report now surfaces the matching on-disk filename families directly.".to_string(), + ]; + + let named_binding_comparison = if let Some(bindings_path) = bindings_path { + Some(load_named_binding_comparison(bindings_path, &entries)?) + } else { + None + }; + let recovered_table_summary = summarize_recovered_table_families(&entries, &files); + + Ok(BuildingTypeSourceReport { + directory_path: path.display().to_string(), + bca_file_count, + bty_file_count, + unique_canonical_stem_count: entries.len(), + bca_selector_pattern_count: bca_selector_patterns.len(), + named_binding_comparison, + recovered_table_summary, + notes, + bca_selector_patterns, + files, + entries, + }) +} + +pub(super) fn canonicalize_building_stem(stem: &str) -> String { + stem.chars() + .filter(|ch| !matches!(ch, ' ' | '_' | '-')) + .flat_map(|ch| ch.to_lowercase()) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/building/types.rs b/crates/rrt-runtime/src/inspect/building/types.rs new file mode 100644 index 0000000..86d452f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/building/types.rs @@ -0,0 +1,184 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BuildingTypeSourceKind { + Bca, + Bty, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeSourceFile { + pub file_name: String, + pub raw_stem: String, + pub canonical_stem: String, + pub source_kind: BuildingTypeSourceKind, + #[serde(default)] + pub byte_len: Option, + #[serde(default)] + pub bca_selector_probe: Option, + #[serde(default)] + pub bty_header_probe: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeSourceEntry { + pub canonical_stem: String, + pub raw_stems: Vec, + pub source_kinds: Vec, + pub file_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBcaSelectorProbe { + pub byte_0xb8: u8, + pub byte_0xb8_hex: String, + pub byte_0xb9: u8, + pub byte_0xb9_hex: String, + pub byte_0xba: u8, + pub byte_0xba_hex: String, + pub byte_0xbb: u8, + pub byte_0xbb_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBtyHeaderProbe { + pub type_id: u32, + pub type_id_hex: String, + pub name_0x04: String, + pub name_0x22: String, + pub name_0x40: String, + pub name_0x5e: String, + pub name_0x7c: String, + pub name_0x9a: String, + pub byte_0xb8: u8, + pub byte_0xb8_hex: String, + pub byte_0xb9: u8, + pub byte_0xb9_hex: String, + pub byte_0xba: u8, + pub byte_0xba_hex: String, + pub dword_0xbb: u32, + pub dword_0xbb_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBcaSelectorPatternSummary { + pub byte_len: usize, + pub byte_0xb8_hex: String, + pub byte_0xb9_hex: String, + pub byte_0xba_hex: String, + pub byte_0xbb_hex: String, + pub file_count: usize, + pub sample_file_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeNamedBindingComparison { + pub bindings_path: String, + pub named_binding_count: usize, + pub shared_canonical_stem_count: usize, + pub binding_only_canonical_stems: Vec, + pub source_only_canonical_stems: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeRecoveredTableSummary { + pub recovered_style_themes: Vec, + pub recovered_source_kinds: Vec, + pub present_style_station_entries: Vec, + pub present_standalone_entries: Vec, + pub recovered_source_family_summaries: Vec, + pub bare_port_warehouse_files: Vec, + pub nonzero_bty_header_dword_summaries: Vec, + pub nonzero_bty_header_name_0x40_summaries: Vec, + pub nonzero_bty_header_name_0x5e_summaries: Vec, + pub nonzero_bty_header_name_0x7c_summaries: Vec, + pub nonzero_bty_header_alias_selector_summaries: Vec, + pub bty_header_name_0x5e_dword_summaries: Vec, + pub bty_name_0x5e_bca_selector_summaries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBtyHeaderDwordSummary { + pub dword_0xbb: u32, + pub dword_0xbb_hex: String, + pub file_count: usize, + pub sample_file_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeRecoveredSourceFamilySummary { + pub canonical_stem: String, + pub sample_raw_stem: String, + pub has_bca_pair: bool, + pub type_id_hex: String, + pub name_0x40: String, + pub name_0x5e: String, + pub name_0x7c: String, + pub dword_0xbb_hex: String, + #[serde(default)] + pub byte_0xba_hex: Option, + #[serde(default)] + pub byte_0xbb_hex: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBtyHeaderNameSummary { + pub header_offset_hex: String, + pub header_value: String, + pub file_count: usize, + pub sample_file_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBtyHeaderNameDwordSummary { + pub header_offset_hex: String, + pub header_value: String, + pub dword_0xbb: u32, + pub dword_0xbb_hex: String, + pub file_count: usize, + pub sample_file_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBtyHeaderAliasSelectorSummary { + pub name_0x40: String, + pub name_0x5e: String, + pub name_0x7c: String, + pub dword_0xbb: u32, + pub dword_0xbb_hex: String, + pub byte_0xb8_hex: String, + pub byte_0xb9_hex: String, + pub byte_0xba_hex: String, + pub byte_0xbb_hex: String, + pub file_count: usize, + pub sample_file_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBtyNameBcaSelectorSummary { + pub header_offset_hex: String, + pub header_value: String, + pub dword_0xbb: u32, + pub dword_0xbb_hex: String, + pub byte_0xba_hex: String, + pub byte_0xbb_hex: String, + pub file_count: usize, + pub sample_file_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeSourceReport { + pub directory_path: String, + pub bca_file_count: usize, + pub bty_file_count: usize, + pub unique_canonical_stem_count: usize, + pub bca_selector_pattern_count: usize, + #[serde(default)] + pub named_binding_comparison: Option, + pub recovered_table_summary: BuildingTypeRecoveredTableSummary, + pub notes: Vec, + pub bca_selector_patterns: Vec, + pub files: Vec, + pub entries: Vec, +} diff --git a/crates/rrt-runtime/src/campaign_exe.rs b/crates/rrt-runtime/src/inspect/campaign.rs similarity index 100% rename from crates/rrt-runtime/src/campaign_exe.rs rename to crates/rrt-runtime/src/inspect/campaign.rs diff --git a/crates/rrt-runtime/src/inspect/cargo/bindings.rs b/crates/rrt-runtime/src/inspect/cargo/bindings.rs new file mode 100644 index 0000000..6e3ee0a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/cargo/bindings.rs @@ -0,0 +1,30 @@ +use std::fs; +use std::path::Path; + +use serde::Deserialize; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +struct CargoBindingArtifact { + bindings: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub(super) struct CargoBindingRow { + pub(super) descriptor_id: u32, + pub(super) band: String, + pub(super) cargo_name: String, + pub(super) binding_index: usize, +} + +pub(super) fn load_cargo_bindings( + path: Option<&Path>, +) -> Result>, Box> { + let Some(path) = path else { + return Ok(None); + }; + if !path.exists() { + return Ok(None); + } + let artifact: CargoBindingArtifact = serde_json::from_str(&fs::read_to_string(path)?)?; + Ok(Some(artifact.bindings)) +} diff --git a/crates/rrt-runtime/src/inspect/cargo/cargo_skin.rs b/crates/rrt-runtime/src/inspect/cargo/cargo_skin.rs new file mode 100644 index 0000000..4a88a2a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/cargo/cargo_skin.rs @@ -0,0 +1,72 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use crate::inspect::pk4::inspect_pk4_bytes; + +use super::types::{CargoSkinDescriptorEntry, CargoSkinInspectionReport, parse_cargo_name_token}; + +pub fn inspect_cargo_skin_pk4( + path: &Path, +) -> Result> { + let bytes = fs::read(path)?; + let inspection = inspect_pk4_bytes(&bytes)?; + let mut entries = Vec::new(); + for entry in &inspection.entries { + if entry.extension.as_deref() != Some("dsc") { + continue; + } + let payload = bytes + .get(entry.payload_absolute_offset..entry.payload_end_offset) + .ok_or_else(|| format!("pk4 payload range out of bounds for {}", entry.name))?; + let parsed = parse_cargo_skin_descriptor_entry(&entry.name, payload)?; + entries.push(parsed); + } + entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name)); + + let mut notes = Vec::new(); + notes.push( + "cargoSkin descriptors are parsed from .dsc payloads whose first non-empty line names the descriptor kind and whose second non-empty line carries the cargo token." + .to_string(), + ); + notes.push( + "A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix." + .to_string(), + ); + + let unique_visible_name_count = entries + .iter() + .map(|entry| entry.name.visible_name.as_str()) + .collect::>() + .len(); + + Ok(CargoSkinInspectionReport { + pk4_path: path.display().to_string(), + entry_count: entries.len(), + unique_visible_name_count, + notes, + entries, + }) +} + +pub(super) fn parse_cargo_skin_descriptor_entry( + entry_name: &str, + bytes: &[u8], +) -> Result> { + let text = std::str::from_utf8(bytes)?; + let mut lines = text.lines().map(str::trim).filter(|line| !line.is_empty()); + let descriptor_kind = lines + .next() + .ok_or_else(|| format!("cargo skin descriptor {entry_name} is missing the kind line"))?; + let raw_name = lines + .next() + .ok_or_else(|| format!("cargo skin descriptor {entry_name} is missing the name line"))?; + + Ok(CargoSkinDescriptorEntry { + pk4_entry_name: entry_name.to_string(), + payload_len: bytes.len(), + payload_len_hex: format!("0x{:x}", bytes.len()), + descriptor_kind: descriptor_kind.to_string(), + name: parse_cargo_name_token(raw_name), + }) +} diff --git a/crates/rrt-runtime/src/inspect/cargo/cargo_types.rs b/crates/rrt-runtime/src/inspect/cargo/cargo_types.rs new file mode 100644 index 0000000..aa1820d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/cargo/cargo_types.rs @@ -0,0 +1,81 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use super::types::{CargoTypeEntry, CargoTypeInspectionReport, parse_cargo_name_token}; + +pub fn inspect_cargo_types_dir( + path: &Path, +) -> Result> { + let mut entries = Vec::new(); + for entry in fs::read_dir(path)? { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + let file_name = entry.file_name(); + let file_name = file_name.to_string_lossy().into_owned(); + if Path::new(&file_name) + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.eq_ignore_ascii_case("cty")) + != Some(true) + { + continue; + } + let bytes = fs::read(entry.path())?; + entries.push(parse_cargo_type_entry(&file_name, &bytes)?); + } + entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name)); + + let mut notes = Vec::new(); + notes.push( + "CargoTypes entries carry a 0x03ea header and an inline NUL-terminated cargo token string." + .to_string(), + ); + notes.push( + "A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix." + .to_string(), + ); + + let unique_visible_name_count = entries + .iter() + .map(|entry| entry.name.visible_name.as_str()) + .collect::>() + .len(); + + Ok(CargoTypeInspectionReport { + directory_path: path.display().to_string(), + entry_count: entries.len(), + unique_visible_name_count, + notes, + entries, + }) +} + +pub(super) fn parse_cargo_type_entry( + file_name: &str, + bytes: &[u8], +) -> Result> { + if bytes.len() < 5 { + return Err(format!("cargo type entry {file_name} is too short").into()); + } + let header_magic = u32::from_le_bytes(bytes[0..4].try_into().expect("length checked")); + let raw_name = parse_nul_terminated_utf8(bytes, 4) + .ok_or_else(|| format!("cargo type entry {file_name} is missing a NUL-terminated name"))?; + + Ok(CargoTypeEntry { + file_name: file_name.to_string(), + file_size: bytes.len(), + file_size_hex: format!("0x{:x}", bytes.len()), + header_magic, + header_magic_hex: format!("0x{header_magic:08x}"), + name: parse_cargo_name_token(&raw_name), + }) +} + +fn parse_nul_terminated_utf8(bytes: &[u8], offset: usize) -> Option { + let tail = bytes.get(offset..)?; + let end = tail.iter().position(|byte| *byte == 0)?; + String::from_utf8(tail[..end].to_vec()).ok() +} diff --git a/crates/rrt-runtime/src/inspect/cargo/economy.rs b/crates/rrt-runtime/src/inspect/cargo/economy.rs new file mode 100644 index 0000000..58c32f6 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/cargo/economy.rs @@ -0,0 +1,159 @@ +use std::collections::BTreeSet; +use std::path::Path; + +use crate::inspect::cargo::{ + NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT, +}; + +use super::bindings::{CargoBindingRow, load_cargo_bindings}; +use super::cargo_skin::inspect_cargo_skin_pk4; +use super::cargo_types::inspect_cargo_types_dir; +use super::registry::build_live_registry_entries; +use super::selector::{build_price_selector_candidate_registry, build_selector_from_bindings}; +use super::types::{ + CargoEconomySourceReport, CargoSkinInspectionReport, CargoTypeInspectionReport, +}; + +pub fn inspect_cargo_economy_sources( + cargo_types_dir: &Path, + cargo_skin_pk4_path: &Path, +) -> Result> { + inspect_cargo_economy_sources_with_bindings(cargo_types_dir, cargo_skin_pk4_path, None) +} + +pub fn inspect_cargo_economy_sources_with_bindings( + cargo_types_dir: &Path, + cargo_skin_pk4_path: &Path, + cargo_bindings_path: Option<&Path>, +) -> Result> { + let cargo_types = inspect_cargo_types_dir(cargo_types_dir)?; + let cargo_skins = inspect_cargo_skin_pk4(cargo_skin_pk4_path)?; + let cargo_bindings = load_cargo_bindings(cargo_bindings_path)?; + Ok(build_cargo_economy_source_report( + cargo_types, + cargo_skins, + cargo_bindings.as_deref(), + )) +} + +pub(super) fn build_cargo_economy_source_report( + cargo_types: CargoTypeInspectionReport, + cargo_skins: CargoSkinInspectionReport, + cargo_bindings: Option<&[CargoBindingRow]>, +) -> CargoEconomySourceReport { + let cargo_type_visible_names = cargo_types + .entries + .iter() + .map(|entry| entry.name.visible_name.clone()) + .collect::>(); + let cargo_skin_visible_names = cargo_skins + .entries + .iter() + .map(|entry| entry.name.visible_name.clone()) + .collect::>(); + + let shared_visible_name_count = cargo_type_visible_names + .intersection(&cargo_skin_visible_names) + .count(); + let visible_name_union_count = cargo_type_visible_names + .union(&cargo_skin_visible_names) + .count(); + let cargo_type_only_visible_names = cargo_type_visible_names + .difference(&cargo_skin_visible_names) + .cloned() + .collect::>(); + let cargo_skin_only_visible_names = cargo_skin_visible_names + .difference(&cargo_type_visible_names) + .cloned() + .collect::>(); + let live_registry_entries = + build_live_registry_entries(&cargo_types.entries, &cargo_skins.entries); + let production_selector = cargo_bindings.and_then(|bindings| { + build_selector_from_bindings( + bindings, + &live_registry_entries, + "cargo_production_named", + "named_cargo_production", + NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT, + "This selector is grounded from the checked-in named cargo production bindings artifact.", + "The current grounded order matches the 50-row named cargo-production descriptor strip.", + ) + }); + let price_selector = cargo_bindings + .and_then(|bindings| { + build_selector_from_bindings( + bindings, + &live_registry_entries, + "cargo_price_named", + "named_cargo_price", + NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, + "This selector is grounded from the checked-in named cargo price bindings artifact.", + "The current grounded order matches the 71-row named cargo-price descriptor strip.", + ) + }) + .unwrap_or_else(|| build_price_selector_candidate_registry(&live_registry_entries)); + let price_selector_candidate_only_visible_names = price_selector + .entries + .iter() + .map(|entry| entry.visible_name.as_str()) + .collect::>(); + let price_selector_candidate_only_visible_names = live_registry_entries + .iter() + .filter(|entry| { + !price_selector_candidate_only_visible_names.contains(entry.visible_name.as_str()) + }) + .map(|entry| entry.visible_name.clone()) + .collect::>(); + let price_selector_candidate_excess_count = live_registry_entries + .len() + .saturating_sub(NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT); + + let mut notes = Vec::new(); + notes.push(format!( + "Named cargo-price descriptors 106..176 span {} rows, while named cargo-production descriptors 180..229 span {} rows.", + NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT + )); + notes.push(format!( + "The inspected CargoTypes corpus exposes {} visible names, the inspected cargoSkin corpus exposes {} visible names, and their union exposes {} visible names.", + cargo_types.unique_visible_name_count, cargo_skins.unique_visible_name_count, visible_name_union_count + )); + if !price_selector.exact_resolution { + notes.push( + "That visible-name union still does not match the 71-row named cargo-price strip, so this offline source reconstruction is groundwork rather than a complete price-selector binding." + .to_string(), + ); + } else { + notes.push( + "The checked-in bindings now close the 71-row named cargo-price strip on top of the merged live cargo registry." + .to_string(), + ); + } + if cargo_types.unique_visible_name_count == NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT { + notes.push( + "The CargoTypes corpus still matches the 50-row named cargo-production strip cardinality that grounds the current production bindings." + .to_string(), + ); + } + + CargoEconomySourceReport { + cargo_types_dir: cargo_types.directory_path, + cargo_skin_pk4_path: cargo_skins.pk4_path, + named_cargo_price_row_count: NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, + named_cargo_production_row_count: NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT, + cargo_type_count: cargo_types.entry_count, + cargo_skin_count: cargo_skins.entry_count, + shared_visible_name_count, + visible_name_union_count, + cargo_type_only_visible_names, + cargo_skin_only_visible_names, + live_registry_count: live_registry_entries.len(), + live_registry_entries, + price_selector_candidate_excess_count, + price_selector_candidate_only_visible_names, + production_selector, + price_selector, + notes, + cargo_type_entries: cargo_types.entries, + cargo_skin_entries: cargo_skins.entries, + } +} diff --git a/crates/rrt-runtime/src/inspect/cargo/mod.rs b/crates/rrt-runtime/src/inspect/cargo/mod.rs new file mode 100644 index 0000000..25be34c --- /dev/null +++ b/crates/rrt-runtime/src/inspect/cargo/mod.rs @@ -0,0 +1,127 @@ +mod bindings; +mod cargo_skin; +mod cargo_types; +mod economy; +mod registry; +mod selector; + +pub mod types; + +pub const NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT: usize = 71; +pub const NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT: usize = 50; +pub const CARGO_TYPE_MAGIC: u32 = 0x0000_03ea; + +pub use cargo_skin::inspect_cargo_skin_pk4; +pub use cargo_types::inspect_cargo_types_dir; +pub use economy::{inspect_cargo_economy_sources, inspect_cargo_economy_sources_with_bindings}; +pub use types::*; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_plain_cargo_type_entry() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&CARGO_TYPE_MAGIC.to_le_bytes()); + bytes.extend_from_slice(b"Alcohol\0"); + let entry = + cargo_types::parse_cargo_type_entry("Alcohol.cty", &bytes).expect("entry should parse"); + assert_eq!(entry.header_magic, CARGO_TYPE_MAGIC); + assert_eq!(entry.name.raw_name, "Alcohol"); + assert_eq!(entry.name.visible_name, "Alcohol"); + assert_eq!(entry.name.localized_string_id, None); + } + + #[test] + fn parses_localized_cargo_type_entry() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&CARGO_TYPE_MAGIC.to_le_bytes()); + bytes.extend_from_slice(b"~4465Ceramics\0"); + let entry = cargo_types::parse_cargo_type_entry("~4465Ceramics.cty", &bytes) + .expect("entry should parse"); + assert_eq!(entry.name.raw_name, "~4465Ceramics"); + assert_eq!(entry.name.visible_name, "Ceramics"); + assert_eq!(entry.name.localized_string_id, Some(4465)); + } + + #[test] + fn parses_cargo_skin_descriptor_entry() { + let entry = cargo_skin::parse_cargo_skin_descriptor_entry( + "Alcohol.dsc", + b"cargoSkin\r\nAlcohol\r\n", + ) + .expect("descriptor should parse"); + assert_eq!(entry.descriptor_kind, "cargoSkin"); + assert_eq!(entry.name.visible_name, "Alcohol"); + } + + #[test] + fn builds_cargo_source_report_union_counts() { + let cargo_types = CargoTypeInspectionReport { + directory_path: "CargoTypes".to_string(), + entry_count: 2, + unique_visible_name_count: 2, + notes: Vec::new(), + entries: vec![ + CargoTypeEntry { + file_name: "Alcohol.cty".to_string(), + file_size: 16, + file_size_hex: "0x10".to_string(), + header_magic: CARGO_TYPE_MAGIC, + header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), + name: types::parse_cargo_name_token("Alcohol"), + }, + CargoTypeEntry { + file_name: "Coal.cty".to_string(), + file_size: 16, + file_size_hex: "0x10".to_string(), + header_magic: CARGO_TYPE_MAGIC, + header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), + name: types::parse_cargo_name_token("Coal"), + }, + ], + }; + let cargo_skins = CargoSkinInspectionReport { + pk4_path: "Cargo106.PK4".to_string(), + entry_count: 2, + unique_visible_name_count: 2, + notes: Vec::new(), + entries: vec![ + CargoSkinDescriptorEntry { + pk4_entry_name: "Alcohol.dsc".to_string(), + payload_len: 20, + payload_len_hex: "0x14".to_string(), + descriptor_kind: "cargoSkin".to_string(), + name: types::parse_cargo_name_token("Alcohol"), + }, + CargoSkinDescriptorEntry { + pk4_entry_name: "Beer.dsc".to_string(), + payload_len: 16, + payload_len_hex: "0x10".to_string(), + descriptor_kind: "cargoSkin".to_string(), + name: types::parse_cargo_name_token("Beer"), + }, + ], + }; + + let report = economy::build_cargo_economy_source_report(cargo_types, cargo_skins, None); + assert_eq!(report.shared_visible_name_count, 1); + assert_eq!(report.visible_name_union_count, 3); + assert_eq!(report.live_registry_count, 3); + assert_eq!( + report.cargo_type_only_visible_names, + vec!["Coal".to_string()] + ); + assert_eq!( + report.cargo_skin_only_visible_names, + vec!["Beer".to_string()] + ); + assert_eq!( + report.price_selector.selector_kind, + "named_cargo_price_candidate_registry" + ); + assert_eq!(report.price_selector.entries.len(), 3); + assert!(!report.price_selector.exact_resolution); + } +} diff --git a/crates/rrt-runtime/src/inspect/cargo/registry.rs b/crates/rrt-runtime/src/inspect/cargo/registry.rs new file mode 100644 index 0000000..e03a2d1 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/cargo/registry.rs @@ -0,0 +1,78 @@ +use std::collections::BTreeSet; + +use super::types::{ + CargoLiveRegistryEntry, CargoRegistrySourceKind, CargoSkinDescriptorEntry, CargoTypeEntry, +}; + +pub(super) fn build_live_registry_entries( + cargo_type_entries: &[CargoTypeEntry], + cargo_skin_entries: &[CargoSkinDescriptorEntry], +) -> Vec { + let mut visible_names = cargo_type_entries + .iter() + .map(|entry| entry.name.visible_name.clone()) + .chain( + cargo_skin_entries + .iter() + .map(|entry| entry.name.visible_name.clone()), + ) + .collect::>() + .into_iter() + .collect::>(); + visible_names.sort(); + + visible_names + .into_iter() + .map(|visible_name| { + let mut raw_names = cargo_type_entries + .iter() + .filter(|entry| entry.name.visible_name == visible_name) + .map(|entry| entry.name.raw_name.clone()) + .chain( + cargo_skin_entries + .iter() + .filter(|entry| entry.name.visible_name == visible_name) + .map(|entry| entry.name.raw_name.clone()), + ) + .collect::>() + .into_iter() + .collect::>(); + raw_names.sort(); + + let localized_string_ids = cargo_type_entries + .iter() + .filter(|entry| entry.name.visible_name == visible_name) + .filter_map(|entry| entry.name.localized_string_id) + .chain( + cargo_skin_entries + .iter() + .filter(|entry| entry.name.visible_name == visible_name) + .filter_map(|entry| entry.name.localized_string_id), + ) + .collect::>() + .into_iter() + .collect::>(); + + let mut source_kinds = Vec::new(); + if cargo_type_entries + .iter() + .any(|entry| entry.name.visible_name == visible_name) + { + source_kinds.push(CargoRegistrySourceKind::CargoTypes); + } + if cargo_skin_entries + .iter() + .any(|entry| entry.name.visible_name == visible_name) + { + source_kinds.push(CargoRegistrySourceKind::CargoSkin); + } + + CargoLiveRegistryEntry { + visible_name, + raw_names, + localized_string_ids, + source_kinds, + } + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/cargo/selector.rs b/crates/rrt-runtime/src/inspect/cargo/selector.rs new file mode 100644 index 0000000..86f1f3a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/cargo/selector.rs @@ -0,0 +1,94 @@ +use crate::inspect::cargo::NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT; + +use super::bindings::CargoBindingRow; +use super::types::{CargoLiveRegistryEntry, CargoSelectorEntry, CargoSelectorReport}; + +pub(super) fn build_selector_from_bindings( + bindings: &[CargoBindingRow], + live_registry_entries: &[CargoLiveRegistryEntry], + band: &str, + selector_kind: &str, + selector_row_count: usize, + grounding_note: &str, + order_note: &str, +) -> Option { + let mut rows = bindings + .iter() + .filter(|binding| binding.band == band) + .collect::>(); + rows.sort_by_key(|binding| binding.binding_index); + if rows.is_empty() { + return None; + } + + let entries = rows + .into_iter() + .map(|binding| CargoSelectorEntry { + selector_index: binding.binding_index, + descriptor_id: Some(binding.descriptor_id), + visible_name: binding.cargo_name.clone(), + source_kinds: live_registry_entries + .iter() + .find(|entry| entry.visible_name == binding.cargo_name) + .map(|entry| entry.source_kinds.clone()) + .unwrap_or_default(), + }) + .collect::>(); + + Some(CargoSelectorReport { + selector_kind: selector_kind.to_string(), + exact_resolution: entries.len() == selector_row_count, + selector_row_count, + candidate_registry_count: live_registry_entries.len(), + notes: vec![grounding_note.to_string(), order_note.to_string()], + entries, + }) +} + +pub(super) fn build_price_selector_candidate_registry( + live_registry_entries: &[CargoLiveRegistryEntry], +) -> CargoSelectorReport { + let entries = live_registry_entries + .iter() + .enumerate() + .map(|(index, entry)| CargoSelectorEntry { + selector_index: index + 1, + descriptor_id: None, + visible_name: entry.visible_name.clone(), + source_kinds: entry.source_kinds.clone(), + }) + .collect::>(); + let candidate_registry_count = entries.len(); + let exact_resolution = candidate_registry_count == NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT; + let mut notes = Vec::new(); + notes.push( + "This is the current merged visible-name registry, sorted lexicographically, not a claimed reproduction of the original price selector." + .to_string(), + ); + if exact_resolution { + notes.push( + "The merged visible-name registry cardinality matches the 71-row named cargo-price descriptor strip." + .to_string(), + ); + } else { + notes.push(format!( + "The merged visible-name registry has {} entries, so the exact 71-row price-selector binding remains unresolved by static source recovery alone.", + candidate_registry_count + )); + let excess = + candidate_registry_count.saturating_sub(NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT); + notes.push(format!( + "Current unresolved gap is {} excess candidate names relative to the descriptor strip.", + excess + )); + } + + CargoSelectorReport { + selector_kind: "named_cargo_price_candidate_registry".to_string(), + exact_resolution, + selector_row_count: NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, + candidate_registry_count, + notes, + entries, + } +} diff --git a/crates/rrt-runtime/src/inspect/cargo/types.rs b/crates/rrt-runtime/src/inspect/cargo/types.rs new file mode 100644 index 0000000..3b37f30 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/cargo/types.rs @@ -0,0 +1,124 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoNameToken { + pub raw_name: String, + pub visible_name: String, + #[serde(default)] + pub localized_string_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoTypeEntry { + pub file_name: String, + pub file_size: usize, + pub file_size_hex: String, + pub header_magic: u32, + pub header_magic_hex: String, + pub name: CargoNameToken, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoTypeInspectionReport { + pub directory_path: String, + pub entry_count: usize, + pub unique_visible_name_count: usize, + pub notes: Vec, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoSkinDescriptorEntry { + pub pk4_entry_name: String, + pub payload_len: usize, + pub payload_len_hex: String, + pub descriptor_kind: String, + pub name: CargoNameToken, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoSkinInspectionReport { + pub pk4_path: String, + pub entry_count: usize, + pub unique_visible_name_count: usize, + pub notes: Vec, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoEconomySourceReport { + pub cargo_types_dir: String, + pub cargo_skin_pk4_path: String, + pub named_cargo_price_row_count: usize, + pub named_cargo_production_row_count: usize, + pub cargo_type_count: usize, + pub cargo_skin_count: usize, + pub shared_visible_name_count: usize, + pub visible_name_union_count: usize, + pub cargo_type_only_visible_names: Vec, + pub cargo_skin_only_visible_names: Vec, + pub live_registry_count: usize, + pub live_registry_entries: Vec, + pub price_selector_candidate_excess_count: usize, + pub price_selector_candidate_only_visible_names: Vec, + pub production_selector: Option, + pub price_selector: CargoSelectorReport, + pub notes: Vec, + pub cargo_type_entries: Vec, + pub cargo_skin_entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CargoRegistrySourceKind { + CargoTypes, + CargoSkin, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoLiveRegistryEntry { + pub visible_name: String, + pub raw_names: Vec, + pub localized_string_ids: Vec, + pub source_kinds: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoSelectorEntry { + pub selector_index: usize, + #[serde(default)] + pub descriptor_id: Option, + pub visible_name: String, + pub source_kinds: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoSelectorReport { + pub selector_kind: String, + pub exact_resolution: bool, + pub selector_row_count: usize, + pub candidate_registry_count: usize, + pub notes: Vec, + pub entries: Vec, +} + +pub(super) fn parse_cargo_name_token(raw_name: &str) -> CargoNameToken { + let mut visible_name = raw_name.to_string(); + let mut localized_string_id = None; + if let Some(rest) = raw_name.strip_prefix('~') { + let digits = rest + .chars() + .take_while(|character| character.is_ascii_digit()) + .collect::(); + if !digits.is_empty() { + localized_string_id = digits.parse::().ok(); + visible_name = rest[digits.len()..].to_string(); + } + } + + CargoNameToken { + raw_name: raw_name.to_string(), + visible_name, + localized_string_id, + } +} diff --git a/crates/rrt-runtime/src/pk4.rs b/crates/rrt-runtime/src/inspect/pk4.rs similarity index 100% rename from crates/rrt-runtime/src/pk4.rs rename to crates/rrt-runtime/src/inspect/pk4.rs diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/anchor.rs b/crates/rrt-runtime/src/inspect/smp/bundle/anchor.rs new file mode 100644 index 0000000..1049b4a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/anchor.rs @@ -0,0 +1,185 @@ +use crate::inspect::smp::bundle::{ + SmpContainerProfile, SmpRuntimeAnchorCycleBlock, SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock, + SmpSecondaryVariantProbe, +}; +use crate::inspect::smp::common::{read_u32_at, read_u32_window}; + +pub(in crate::inspect::smp) fn parse_save_bootstrap_block( + container_profile: Option<&SmpContainerProfile>, + secondary_variant_probe: Option<&SmpSecondaryVariantProbe>, +) -> Option { + let profile = container_profile?; + let secondary = secondary_variant_probe?; + let words = &secondary.words; + if words.len() < 8 { + return None; + } + + let supported = matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ); + if !supported { + return None; + } + + Some(SmpSaveBootstrapBlock { + profile_family: profile.profile_family.clone(), + aligned_window_offset: secondary.aligned_window_offset, + leading_word: words[0], + leading_word_hex: format!("0x{:08x}", words[0]), + anchor_word: words[1], + anchor_word_hex: format!("0x{:08x}", words[1]), + descriptor_word_2: words[2], + descriptor_word_2_hex: format!("0x{:08x}", words[2]), + descriptor_word_3: words[3], + descriptor_word_3_hex: format!("0x{:08x}", words[3]), + descriptor_word_4: words[4], + descriptor_word_4_hex: format!("0x{:08x}", words[4]), + descriptor_word_5: words[5], + descriptor_word_5_hex: format!("0x{:08x}", words[5]), + descriptor_word_6: words[6], + descriptor_word_6_hex: format!("0x{:08x}", words[6]), + descriptor_word_7: words[7], + descriptor_word_7_hex: format!("0x{:08x}", words[7]), + }) +} + +pub(in crate::inspect::smp) fn parse_runtime_anchor_cycle_block( + bytes: &[u8], + container_profile: Option<&SmpContainerProfile>, + secondary_variant_probe: Option<&SmpSecondaryVariantProbe>, +) -> Option { + let profile = container_profile?; + let secondary = secondary_variant_probe?; + let supported = matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-classic-sandbox-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + | "rt3-105-sandbox-container-v1" + ); + if !supported { + return None; + } + + let cycle_start_offset = secondary.aligned_window_offset + 0x1c; + let cycle_words = read_u32_window(bytes, cycle_start_offset, 9); + if cycle_words.len() < 9 { + return None; + } + + let mut full_cycle_count = 0usize; + let mut cursor = cycle_start_offset; + while read_u32_window(bytes, cursor, cycle_words.len()) == cycle_words { + full_cycle_count += 1; + cursor += cycle_words.len() * 4; + } + + if full_cycle_count == 0 { + return None; + } + + let mut partial_cycle_word_count = 0usize; + while partial_cycle_word_count < cycle_words.len() { + let offset = cursor + partial_cycle_word_count * 4; + if read_u32_at(bytes, offset) == Some(cycle_words[partial_cycle_word_count]) { + partial_cycle_word_count += 1; + } else { + break; + } + } + + let trailer_offset = cursor + partial_cycle_word_count * 4; + let trailer_words = read_u32_window(bytes, trailer_offset, 16); + + Some(SmpRuntimeAnchorCycleBlock { + profile_family: profile.profile_family.clone(), + cycle_start_offset, + cycle_hex_words: cycle_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + cycle_words, + full_cycle_count, + partial_cycle_word_count, + trailer_offset, + trailer_hex_words: trailer_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + trailer_words, + }) +} + +pub(in crate::inspect::smp) fn parse_save_anchor_run_block( + bytes: &[u8], + container_profile: Option<&SmpContainerProfile>, + save_bootstrap_block: Option<&SmpSaveBootstrapBlock>, +) -> Option { + let profile = container_profile?; + let bootstrap = save_bootstrap_block?; + let supported = matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ); + if !supported { + return None; + } + + let cycle_start_offset = bootstrap.aligned_window_offset + 0x1c; + let cycle_words = read_u32_window(bytes, cycle_start_offset, 9); + if cycle_words.len() < 9 { + return None; + } + + let mut full_cycle_count = 0usize; + let mut cursor = cycle_start_offset; + while read_u32_window(bytes, cursor, cycle_words.len()) == cycle_words { + full_cycle_count += 1; + cursor += cycle_words.len() * 4; + } + + if full_cycle_count == 0 { + return None; + } + + let mut partial_cycle_word_count = 0usize; + while partial_cycle_word_count < cycle_words.len() { + let offset = cursor + partial_cycle_word_count * 4; + if read_u32_at(bytes, offset) == Some(cycle_words[partial_cycle_word_count]) { + partial_cycle_word_count += 1; + } else { + break; + } + } + + let trailer_offset = cursor + partial_cycle_word_count * 4; + let trailer_words = read_u32_window(bytes, trailer_offset, 12); + + Some(SmpSaveAnchorRunBlock { + profile_family: profile.profile_family.clone(), + cycle_start_offset, + cycle_hex_words: cycle_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + cycle_words, + full_cycle_count, + partial_cycle_word_count, + trailer_offset, + trailer_hex_words: trailer_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + trailer_words, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/ascii.rs b/crates/rrt-runtime/src/inspect/smp/bundle/ascii.rs new file mode 100644 index 0000000..73a33cf --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/ascii.rs @@ -0,0 +1,88 @@ +use crate::inspect::smp::bundle::SmpAsciiPreview; +use crate::inspect::smp::map_title::{build_ascii_preview, is_ascii_preview_byte}; + +pub(in crate::inspect::smp) fn collect_ascii_previews_in_range( + bytes: &[u8], + start: usize, + end: usize, + min_len: usize, +) -> Vec { + let mut previews = Vec::new(); + let mut run_start = None; + let end = end.min(bytes.len()); + + for index in start..end { + let byte = bytes[index]; + if is_ascii_preview_byte(byte) { + run_start.get_or_insert(index); + continue; + } + + if let Some(current_start) = run_start.take() { + if index - current_start >= min_len { + previews.push(build_ascii_preview(bytes, current_start, index)); + } + } + } + + if let Some(current_start) = run_start { + if end - current_start >= min_len { + previews.push(build_ascii_preview(bytes, current_start, end)); + } + } + + previews +} + +pub(in crate::inspect::smp) fn find_c_string_with_suffix_in_range( + bytes: &[u8], + start: usize, + end: usize, + suffix: &str, +) -> Option { + let end = end.min(bytes.len()); + let suffix = suffix.as_bytes(); + let mut offset = start.min(end); + + while offset < end { + if !is_ascii_preview_byte(bytes[offset]) { + offset += 1; + continue; + } + + let run_start = offset; + while offset < end && is_ascii_preview_byte(bytes[offset]) { + offset += 1; + } + + let run = &bytes[run_start..offset]; + if run.ends_with(suffix) { + return Some(run_start); + } + } + + None +} + +pub(in crate::inspect::smp) fn read_c_string_in_range( + bytes: &[u8], + start: usize, + end: usize, +) -> Option { + if start >= end || start >= bytes.len() { + return None; + } + + let end = end.min(bytes.len()); + let mut cursor = start; + while cursor < end && bytes[cursor] != 0 { + cursor += 1; + } + if cursor == start { + return None; + } + + std::str::from_utf8(&bytes[start..cursor]) + .ok() + .map(ToString::to_string) +} diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/entrypoints.rs b/crates/rrt-runtime/src/inspect/smp/bundle/entrypoints.rs new file mode 100644 index 0000000..05a9496 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/entrypoints.rs @@ -0,0 +1,520 @@ +use std::fs; +use std::path::Path; + +use crate::inspect::smp::bundle::{ + SmpInspectionReport, SmpKnownTagHit, classify_container_profile, classify_header_variant_probe, + classify_secondary_variant_probe, parse_preamble, parse_runtime_anchor_cycle_block, + parse_runtime_post_span_probe, parse_runtime_trailer_block, parse_save_anchor_run_block, + parse_save_bootstrap_block, parse_shared_header, probe_early_content_layout, +}; +use crate::inspect::smp::catalog::offsets::bundle::TAG_OFFSET_SAMPLE_LIMIT; +use crate::inspect::smp::catalog::tags::KNOWN_TAG_DEFINITIONS; +use crate::inspect::smp::common::{ + SmpSaveUnclassifiedTaggedCollectionHeaderProbe, + filter_unclassified_tagged_collection_header_probes_outside_known_spans, find_first_ascii_run, + find_u16_le_offsets, scan_save_unclassified_tagged_collection_header_probes, +}; +use crate::inspect::smp::events::parse_event_runtime_collection_summary; +use crate::inspect::smp::map_title::parse_map_title_hint_probe; +use crate::inspect::smp::profiles::*; +use crate::inspect::smp::regions::*; +use crate::inspect::smp::save_load::build_save_load_summary; +use crate::inspect::smp::special_conditions::*; +use crate::inspect::smp::structures::*; +use crate::inspect::smp::world::*; +use crate::inspect::smp::*; + +pub fn inspect_smp_file(path: &Path) -> Result> { + let bytes = fs::read(path)?; + Ok(inspect_bundle_bytes( + &bytes, + path.extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.to_ascii_lowercase()), + )) +} + +pub fn inspect_smp_bytes(bytes: &[u8]) -> SmpInspectionReport { + inspect_bundle_bytes(bytes, None) +} + +pub fn inspect_unclassified_save_collection_headers_file( + path: &Path, +) -> Result, Box> { + let bytes = fs::read(path)?; + let file_extension_hint = path + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.to_ascii_lowercase()); + let shared_header = parse_shared_header(&bytes); + let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); + let first_ascii_run = find_first_ascii_run(&bytes); + let early_content_probe = first_ascii_run + .as_ref() + .and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run)); + let secondary_variant_probe = early_content_probe + .as_ref() + .and_then(classify_secondary_variant_probe); + let container_profile = classify_container_profile( + file_extension_hint.as_deref(), + header_variant_probe.as_ref(), + secondary_variant_probe.as_ref(), + ); + let save_company_collection_header_probe = parse_save_company_collection_header_probe( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_chairman_profile_collection_header_probe = + parse_save_chairman_profile_collection_header_probe( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_train_collection_header_probe = parse_save_train_collection_header_probe( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_region_collection_header_probe = parse_save_region_collection_header_probe( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_placed_structure_collection_header_probe = + parse_save_placed_structure_collection_header_probe( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let known_header_probes = [ + save_company_collection_header_probe.as_ref(), + save_chairman_profile_collection_header_probe.as_ref(), + save_train_collection_header_probe.as_ref(), + save_region_collection_header_probe.as_ref(), + save_placed_structure_collection_header_probe.as_ref(), + ]; + let probes = scan_save_unclassified_tagged_collection_header_probes( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + Ok( + filter_unclassified_tagged_collection_header_probes_outside_known_spans( + probes, + &known_header_probes, + ), + ) +} + +pub(in crate::inspect::smp) fn inspect_bundle_bytes( + bytes: &[u8], + file_extension_hint: Option, +) -> SmpInspectionReport { + let known_tag_hits = KNOWN_TAG_DEFINITIONS + .iter() + .filter_map(|definition| { + let offsets = find_u16_le_offsets(bytes, definition.tag_id); + if offsets.is_empty() { + return None; + } + + Some(SmpKnownTagHit { + tag_id: definition.tag_id, + tag_hex: format!("0x{:04x}", definition.tag_id), + label: definition.label.to_string(), + grounded_meaning: definition.grounded_meaning.to_string(), + hit_count: offsets.len(), + sample_offsets: offsets + .iter() + .copied() + .take(TAG_OFFSET_SAMPLE_LIMIT) + .collect(), + last_offset: offsets.last().copied(), + }) + }) + .collect::>(); + + let shared_header = parse_shared_header(bytes); + let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); + let first_ascii_run = find_first_ascii_run(bytes); + let early_content_probe = first_ascii_run + .as_ref() + .and_then(|ascii_run| probe_early_content_layout(bytes, ascii_run)); + let secondary_variant_probe = early_content_probe + .as_ref() + .and_then(classify_secondary_variant_probe); + let container_profile = classify_container_profile( + file_extension_hint.as_deref(), + header_variant_probe.as_ref(), + secondary_variant_probe.as_ref(), + ); + let runtime_anchor_cycle_block = parse_runtime_anchor_cycle_block( + bytes, + container_profile.as_ref(), + secondary_variant_probe.as_ref(), + ); + let save_bootstrap_block = + parse_save_bootstrap_block(container_profile.as_ref(), secondary_variant_probe.as_ref()); + let save_anchor_run_block = parse_save_anchor_run_block( + bytes, + container_profile.as_ref(), + save_bootstrap_block.as_ref(), + ); + let runtime_trailer_block = parse_runtime_trailer_block( + container_profile.as_ref(), + runtime_anchor_cycle_block.as_ref(), + ); + let runtime_post_span_probe = + parse_runtime_post_span_probe(bytes, runtime_trailer_block.as_ref()); + let rt3_105_packed_profile_probe = parse_rt3_105_packed_profile_probe( + bytes, + file_extension_hint.as_deref(), + header_variant_probe.as_ref(), + container_profile.as_ref(), + ); + let rt3_105_post_span_bridge_probe = parse_rt3_105_post_span_bridge_probe( + runtime_trailer_block.as_ref(), + runtime_post_span_probe.as_ref(), + rt3_105_packed_profile_probe.as_ref(), + ); + let rt3_105_save_bridge_payload_probe = + parse_rt3_105_save_bridge_payload_probe(bytes, rt3_105_post_span_bridge_probe.as_ref()); + let save_world_selection_context_probe = parse_save_world_selection_context_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_world_issue_37_probe = parse_save_world_issue_37_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_world_economic_tuning_probe = parse_save_world_economic_tuning_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_world_finance_neighborhood_probe = parse_save_world_finance_neighborhood_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_company_collection_header_probe = parse_save_company_collection_header_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_chairman_profile_collection_header_probe = + parse_save_chairman_profile_collection_header_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_train_collection_header_probe = parse_save_train_collection_header_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_train_collection_directory_probe = parse_save_train_collection_directory_probe( + bytes, + save_train_collection_header_probe.as_ref(), + ); + let save_region_collection_header_probe = parse_save_region_collection_header_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_region_record_triplet_probe = + parse_save_region_record_triplet_probe(bytes, save_region_collection_header_probe.as_ref()); + let save_region_queued_notice_record_probe = parse_save_region_queued_notice_record_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + save_region_collection_header_probe.as_ref(), + ); + let save_region_fixed_row_run_candidate_probe = parse_save_region_fixed_row_run_candidate_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + save_region_collection_header_probe.as_ref(), + ); + let save_placed_structure_collection_header_probe = + parse_save_placed_structure_collection_header_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let save_placed_structure_record_triplet_probe = + parse_save_placed_structure_record_triplet_probe( + bytes, + save_placed_structure_collection_header_probe.as_ref(), + ); + let save_placed_structure_dynamic_side_buffer_probe = + parse_save_placed_structure_dynamic_side_buffer_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let known_header_probes = [ + save_company_collection_header_probe.as_ref(), + save_chairman_profile_collection_header_probe.as_ref(), + save_train_collection_header_probe.as_ref(), + save_region_collection_header_probe.as_ref(), + save_placed_structure_collection_header_probe.as_ref(), + ]; + let save_unclassified_tagged_collection_header_probes = + filter_unclassified_tagged_collection_header_probes_outside_known_spans( + scan_save_unclassified_tagged_collection_header_probes( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ), + &known_header_probes, + ); + let save_company_roster_probe = parse_save_company_roster_probe( + bytes, + save_company_collection_header_probe.as_ref(), + save_world_selection_context_probe.as_ref(), + ); + let save_chairman_profile_table_probe = parse_save_chairman_profile_table_probe( + bytes, + save_chairman_profile_collection_header_probe.as_ref(), + save_world_selection_context_probe.as_ref(), + save_company_collection_header_probe.as_ref(), + ); + let map_title_hint_probe = parse_map_title_hint_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let rt3_105_save_name_table_probe = parse_rt3_105_save_name_table_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + rt3_105_save_bridge_payload_probe.as_ref(), + ); + let rt3_105_save_named_locomotive_availability_probe = + parse_rt3_105_save_named_locomotive_availability_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + rt3_105_packed_profile_probe.as_ref(), + ); + let special_conditions_probe = parse_special_conditions_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + let smp_aligned_runtime_rule_band_probe = parse_smp_aligned_runtime_rule_band_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + special_conditions_probe.as_ref(), + ); + let post_special_conditions_scalar_probe = parse_post_special_conditions_scalar_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + special_conditions_probe.as_ref(), + ); + let post_text_field_neighborhood_probe = parse_post_text_field_neighborhood_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + special_conditions_probe.as_ref(), + ); + let locomotive_policy_neighborhood_probe = parse_locomotive_policy_neighborhood_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + special_conditions_probe.as_ref(), + ); + let pre_recipe_scalar_plateau_probe = parse_pre_recipe_scalar_plateau_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + special_conditions_probe.as_ref(), + ); + let recipe_book_summary_probe = parse_recipe_book_summary_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + special_conditions_probe.as_ref(), + ); + let classic_rehydrate_profile_probe = + parse_classic_rehydrate_profile_probe(bytes, runtime_post_span_probe.as_ref()); + let save_load_summary = build_save_load_summary( + file_extension_hint.as_deref(), + container_profile.as_ref(), + runtime_trailer_block.as_ref(), + rt3_105_post_span_bridge_probe.as_ref(), + classic_rehydrate_profile_probe.as_ref(), + rt3_105_packed_profile_probe.as_ref(), + rt3_105_save_name_table_probe.as_ref(), + ); + let event_runtime_collection_summary = parse_event_runtime_collection_summary( + bytes, + container_profile.as_ref(), + save_load_summary.as_ref(), + ); + let mut warnings = Vec::new(); + if bytes.is_empty() { + warnings + .push("File is empty, so no `.smp` container structure could be observed.".to_string()); + } + + if known_tag_hits.is_empty() { + warnings.push( + "No grounded runtime bundle tags were found in little-endian form. This does not prove the file is invalid." + .to_string(), + ); + } + if shared_header.is_none() && !bytes.is_empty() { + warnings.push( + "File is shorter than the observed 64-byte common RT3 bundle preamble.".to_string(), + ); + } + if let Some(shared_header) = &shared_header { + let header_family_is_known = header_variant_probe + .as_ref() + .map(|probe| probe.is_known_family) + .unwrap_or(false); + if !shared_header.matches_grounded_common_signature && !header_family_is_known { + warnings.push( + "The first 64-byte preamble does not match the currently observed shared RT3 bundle signature." + .to_string(), + ); + } + } + if first_ascii_run.is_some() && early_content_probe.is_none() { + warnings.push( + "Found early text content but could not resolve the next stable nonzero region after its zero padding." + .to_string(), + ); + } + if container_profile + .as_ref() + .is_some_and(|profile| !profile.is_known_profile) + { + warnings.push( + "The current probes did not match any known composite container profile.".to_string(), + ); + } + if known_tag_hits + .iter() + .any(|hit| hit.hit_count > hit.sample_offsets.len()) + { + warnings.push( + "Known-tag offsets are sampled in this report. Large hit counts usually mean byte-pattern noise, not validated chunk boundaries." + .to_string(), + ); + } + warnings.push( + "Inspection scans raw bytes for a small grounded tag set only. It does not validate bundle layout or decode payloads." + .to_string(), + ); + + SmpInspectionReport { + inspection_mode: "grounded-tag-scan-plus-preamble".to_string(), + file_extension_hint, + file_size: bytes.len(), + sha256: sha256_hex(bytes), + preamble: parse_preamble(bytes), + shared_header, + header_variant_probe, + first_ascii_run, + early_content_probe, + secondary_variant_probe, + container_profile, + save_bootstrap_block, + save_anchor_run_block, + runtime_anchor_cycle_block, + runtime_trailer_block, + runtime_post_span_probe, + rt3_105_post_span_bridge_probe, + rt3_105_save_bridge_payload_probe, + save_world_selection_context_probe, + save_world_issue_37_probe, + save_world_economic_tuning_probe, + save_world_finance_neighborhood_probe, + save_company_collection_header_probe, + save_chairman_profile_collection_header_probe, + save_train_collection_header_probe, + save_train_collection_directory_probe, + save_region_collection_header_probe, + save_region_record_triplet_probe, + save_region_queued_notice_record_probe, + save_region_fixed_row_run_candidate_probe, + save_placed_structure_collection_header_probe, + save_placed_structure_record_triplet_probe, + save_placed_structure_dynamic_side_buffer_probe, + save_unclassified_tagged_collection_header_probes, + save_company_roster_probe, + save_chairman_profile_table_probe, + map_title_hint_probe, + rt3_105_save_name_table_probe, + rt3_105_save_named_locomotive_availability_probe, + special_conditions_probe, + smp_aligned_runtime_rule_band_probe, + post_special_conditions_scalar_probe, + post_text_field_neighborhood_probe, + locomotive_policy_neighborhood_probe, + pre_recipe_scalar_plateau_probe, + recipe_book_summary_probe, + classic_rehydrate_profile_probe, + rt3_105_packed_profile_probe, + save_load_summary, + event_runtime_collection_summary, + contains_grounded_runtime_tags: !known_tag_hits.is_empty(), + known_tag_hits, + notes: vec![ + "Grounded `.smp` runtime tags currently include mask-plane payload ids 0x2cee and 0x2d51.".to_string(), + "Grounded sidecar-byte-plane bundle family currently spans 0x9471..0x9472.".to_string(), + "The shared-header parse is intentionally conservative: it only names common preamble lanes and checks the observed RT3 bundle-family signature.".to_string(), + "The header-variant probe classifies the preamble into one of the currently observed install-era families when possible." + .to_string(), + "The early-content probe resolves the first stable nonzero block after the padded scenario text and then captures the next aligned word window." + .to_string(), + "The secondary-variant probe classifies that aligned word window into one of the currently observed file-family patterns." + .to_string(), + "The recipe-book summary probe reports per-book structural signatures at the grounded recipe-book root [world+0x0fe7] without attempting a full cargo-line decode." + .to_string(), + "Where a recipe cargo-token word looks like two printable letters in its high 16 bits, the probe exposes that as one probable ASCII stem while still treating the wider token semantics as inferred." + .to_string(), + "The container-profile layer combines extension hint, header family, and second-window family into one observed container classification." + .to_string(), + "The save-bootstrap reader currently parses one conservative 8-word descriptor only for known save-container profiles." + .to_string(), + "The save-anchor-run reader follows that descriptor tail into the observed repeated 9-word anchor cycle and captures the first trailer words after the cycle diverges." + .to_string(), + "The runtime-anchor-cycle reader applies the same cycle/trailer scan across the currently known save and sandbox runtime container profiles." + .to_string(), + "The runtime-trailer reader classifies the first 16 words after the cycle divergence into the currently observed runtime trailer families." + .to_string(), + "The runtime post-span probe follows the trailer's high-16 span lane into the later file region and records the next nonzero bytes, the first aligned high-16-dense candidate window, and any grounded progress-id hits found nearby." + .to_string(), + "The RT3 1.05 post-span bridge probe correlates the trailer selector/descriptor lanes with the next candidate region and the later packed-profile block for the currently observed 1.05 save families." + .to_string(), + "The RT3 1.05 common-save bridge payload probe captures the two stable bridge-stage blocks currently observed under the base 1.05 save branch." + .to_string(), + "The RT3 1.05 candidate-availability table probe decodes the fixed-width trailing name table from either the common-save bridge payload or the fixed 0x6a70..0x73c0 source range when that header validates." + .to_string(), + "The RT3 1.05 save-side named locomotive availability probe scans the post-profile save region for the grounded fixed-width locomotive-name-plus-dword row family when that run is present." + .to_string(), + "The post-special-conditions scalar probe captures the fixed 0x0df4..0x0f30 dword window immediately after the hidden sentinel slot, splits it into the aligned-band overlap prefix and the later tail, and records the live-object offset alignment of that tail without claiming a byte-for-byte mirror." + .to_string(), + "The classic rehydrate-profile probe recognizes the grounded 0x32dc -> 0x3714 -> 0x3715 progress-id sequence and captures the exact 0x108-byte block between the latter two ids when that pattern appears." + .to_string(), + "The classic packed-profile block reader exposes the stable map-path, display-name, atlas-tracked latch bytes, and the small set of nonzero word lanes observed inside that 0x108-byte block." + .to_string(), + "The RT3 1.05 packed-profile probe recognizes the later string-bearing save block rooted at the first post-header .gmp path and exposes the observed map-path, display-name, atlas-tracked byte lanes, and stable nonzero words." + .to_string(), + format!( + "Restore-side loading of the four sidecar byte planes is only grounded for bundle versions >= 0x{:04x}.", + SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION + ), + ], + warnings, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/mod.rs b/crates/rrt-runtime/src/inspect/smp/bundle/mod.rs new file mode 100644 index 0000000..4af78f0 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/mod.rs @@ -0,0 +1,31 @@ +mod anchor; +mod ascii; +mod entrypoints; +mod model; +mod post_span; +mod preamble; +mod report; +mod trailer; +mod variants; + +pub use super::common::SmpAsciiPreview; +pub use entrypoints::{ + inspect_smp_bytes, inspect_smp_file, inspect_unclassified_save_collection_headers_file, +}; +pub use model::*; +pub use report::*; + +pub(super) use anchor::{ + parse_runtime_anchor_cycle_block, parse_save_anchor_run_block, parse_save_bootstrap_block, +}; +pub(super) use ascii::{ + collect_ascii_previews_in_range, find_c_string_with_suffix_in_range, read_c_string_in_range, +}; +pub(super) use entrypoints::inspect_bundle_bytes; +pub(super) use post_span::{parse_grounded_progress_hit_offset, parse_runtime_post_span_probe}; +pub(super) use preamble::{parse_preamble, parse_shared_header}; +pub(super) use trailer::parse_runtime_trailer_block; +pub(super) use variants::{ + classify_container_profile, classify_header_variant_probe, classify_secondary_variant_probe, + probe_early_content_layout, +}; diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/model.rs b/crates/rrt-runtime/src/inspect/smp/bundle/model.rs new file mode 100644 index 0000000..e793127 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/model.rs @@ -0,0 +1,160 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpContainerProfile { + pub profile_family: String, + pub profile_evidence: Vec, + pub is_known_profile: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveBootstrapBlock { + pub profile_family: String, + pub aligned_window_offset: usize, + pub leading_word: u32, + pub leading_word_hex: String, + pub anchor_word: u32, + pub anchor_word_hex: String, + pub descriptor_word_2: u32, + pub descriptor_word_2_hex: String, + pub descriptor_word_3: u32, + pub descriptor_word_3_hex: String, + pub descriptor_word_4: u32, + pub descriptor_word_4_hex: String, + pub descriptor_word_5: u32, + pub descriptor_word_5_hex: String, + pub descriptor_word_6: u32, + pub descriptor_word_6_hex: String, + pub descriptor_word_7: u32, + pub descriptor_word_7_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveAnchorRunBlock { + pub profile_family: String, + pub cycle_start_offset: usize, + pub cycle_words: Vec, + pub cycle_hex_words: Vec, + pub full_cycle_count: usize, + pub partial_cycle_word_count: usize, + pub trailer_offset: usize, + pub trailer_words: Vec, + pub trailer_hex_words: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRuntimeAnchorCycleBlock { + pub profile_family: String, + pub cycle_start_offset: usize, + pub cycle_words: Vec, + pub cycle_hex_words: Vec, + pub full_cycle_count: usize, + pub partial_cycle_word_count: usize, + pub trailer_offset: usize, + pub trailer_words: Vec, + pub trailer_hex_words: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRuntimeTrailerBlock { + pub profile_family: String, + pub trailer_family: String, + pub trailer_evidence: Vec, + pub trailer_offset: usize, + pub prefix_words_0_to_5: Vec, + pub prefix_hex_words_0_to_5: Vec, + pub tag_word_6: u32, + pub tag_word_6_hex: String, + pub tag_chunk_id_u16: u16, + pub tag_chunk_id_hex: String, + pub tag_chunk_id_grounded_alignment: Option, + pub length_word_7: u32, + pub length_word_7_hex: String, + pub length_high_u16: u16, + pub length_high_hex: String, + pub selector_word_8: u32, + pub selector_word_8_hex: String, + pub selector_high_u16: u16, + pub selector_high_hex: String, + pub layout_word_9: u32, + pub layout_word_9_hex: String, + pub descriptor_word_10: u32, + pub descriptor_word_10_hex: String, + pub descriptor_high_u16: u16, + pub descriptor_high_hex: String, + pub descriptor_word_11: u32, + pub descriptor_word_11_hex: String, + pub counter_word_12: u32, + pub counter_word_12_hex: String, + pub offset_word_13: u32, + pub offset_word_13_hex: String, + pub span_word_14: u32, + pub span_word_14_hex: String, + pub mode_word_15: u32, + pub mode_word_15_hex: String, + pub words: Vec, + pub hex_words: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRuntimePostSpanProbe { + pub profile_family: String, + pub span_target_offset: usize, + pub next_nonzero_offset: Option, + pub next_aligned_candidate_offset: Option, + pub next_aligned_candidate_words: Vec, + pub next_aligned_candidate_hex_words: Vec, + pub header_candidates: Vec, + pub grounded_progress_hits: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRuntimePostSpanHeaderCandidate { + pub offset: usize, + pub words: Vec, + pub hex_words: Vec, + pub dense_word_count: usize, + pub high_u16_words: Vec, + pub high_hex_words: Vec, + pub grounded_alignments: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRt3105PostSpanBridgeProbe { + pub profile_family: String, + pub bridge_family: String, + pub bridge_evidence: Vec, + pub span_target_offset: usize, + pub next_candidate_offset: Option, + pub next_candidate_delta_from_span_target: Option, + pub packed_profile_offset: usize, + pub packed_profile_delta_from_span_target: usize, + pub next_candidate_delta_from_packed_profile: Option, + pub selector_high_u16: u16, + pub selector_high_hex: String, + pub descriptor_high_u16: u16, + pub descriptor_high_hex: String, + pub next_candidate_high_u16_words: Vec, + pub next_candidate_high_hex_words: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRt3105SaveBridgePayloadProbe { + pub profile_family: String, + pub bridge_family: String, + pub primary_block_offset: usize, + pub primary_block_len: usize, + pub primary_block_len_hex: String, + pub primary_words: Vec, + pub primary_hex_words: Vec, + pub secondary_block_offset: usize, + pub secondary_block_delta_from_primary: usize, + pub secondary_block_delta_from_primary_hex: String, + pub secondary_block_end_offset: usize, + pub secondary_block_len: usize, + pub secondary_block_len_hex: String, + pub secondary_preview_word_count: usize, + pub secondary_words: Vec, + pub secondary_hex_words: Vec, + pub evidence: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/post_span.rs b/crates/rrt-runtime/src/inspect/smp/bundle/post_span.rs new file mode 100644 index 0000000..d41f835 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/post_span.rs @@ -0,0 +1,158 @@ +use crate::inspect::smp::bundle::{ + SmpRuntimePostSpanHeaderCandidate, SmpRuntimePostSpanProbe, SmpRuntimeTrailerBlock, +}; +use crate::inspect::smp::common::{find_next_nonzero_offset, read_u32_at, read_u32_window}; + +pub(in crate::inspect::smp) fn parse_runtime_post_span_probe( + bytes: &[u8], + runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>, +) -> Option { + let trailer = runtime_trailer_block?; + let span_target_offset = trailer.trailer_offset + trailer.length_high_u16 as usize; + let next_nonzero_offset = find_next_nonzero_offset(bytes, span_target_offset); + let header_candidates = + collect_runtime_post_span_header_candidates(bytes, span_target_offset, 0x8000); + let next_aligned_candidate_offset = header_candidates.first().map(|candidate| candidate.offset); + let next_aligned_candidate_words = header_candidates + .first() + .map(|candidate| candidate.words.clone()) + .unwrap_or_default(); + let grounded_progress_hits = + find_grounded_progress_high16_hits(bytes, span_target_offset, 0x8000); + + Some(SmpRuntimePostSpanProbe { + profile_family: trailer.profile_family.clone(), + span_target_offset, + next_nonzero_offset, + next_aligned_candidate_offset, + next_aligned_candidate_hex_words: next_aligned_candidate_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + next_aligned_candidate_words, + header_candidates, + grounded_progress_hits, + }) +} + +pub(in crate::inspect::smp) fn collect_runtime_post_span_header_candidates( + bytes: &[u8], + start: usize, + search_len: usize, +) -> Vec { + let end = bytes.len().min(start + search_len); + let mut offset = start & !0x3; + let mut candidates = Vec::new(); + + while offset + 16 <= end && candidates.len() < 8 { + if let Some(candidate) = build_runtime_post_span_header_candidate(bytes, offset) { + let mut cluster_end = offset + 4; + while cluster_end + 16 <= end + && build_runtime_post_span_header_candidate(bytes, cluster_end).is_some() + { + cluster_end += 4; + } + candidates.push(candidate); + offset = cluster_end; + } else { + offset += 4; + } + } + + candidates +} + +pub(in crate::inspect::smp) fn build_runtime_post_span_header_candidate( + bytes: &[u8], + offset: usize, +) -> Option { + let words = read_u32_window(bytes, offset, 4); + if words.len() < 4 { + return None; + } + + let dense_words = words + .iter() + .copied() + .filter(|word| (word & 0xffff) == 0 && (word >> 16) != 0) + .collect::>(); + if dense_words.len() < 3 { + return None; + } + + let high_u16_words = words + .iter() + .map(|word| (word >> 16) as u16) + .collect::>(); + let mut grounded_alignments = Vec::new(); + for high in &high_u16_words { + if let Some(alignment) = classify_runtime_post_span_high16_grounded_alignment(*high) { + if !grounded_alignments + .iter() + .any(|existing| existing == alignment) + { + grounded_alignments.push(alignment.to_string()); + } + } + } + + Some(SmpRuntimePostSpanHeaderCandidate { + offset, + hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(), + dense_word_count: dense_words.len(), + high_hex_words: high_u16_words + .iter() + .map(|word| format!("0x{word:04x}")) + .collect(), + high_u16_words, + grounded_alignments, + words, + }) +} + +pub(in crate::inspect::smp) fn classify_runtime_post_span_high16_grounded_alignment( + high_u16: u16, +) -> Option<&'static str> { + match high_u16 { + 0x32dc => Some( + "High-16 value 0x32dc matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.", + ), + 0x3714 => Some( + "High-16 value 0x3714 matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.", + ), + 0x3715 => Some( + "High-16 value 0x3715 matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.", + ), + _ => None, + } +} + +pub(in crate::inspect::smp) fn find_grounded_progress_high16_hits( + bytes: &[u8], + start: usize, + search_len: usize, +) -> Vec { + let end = bytes.len().min(start + search_len); + let mut hits = Vec::new(); + let mut offset = start & !0x3; + while offset + 4 <= end { + if let Some(word) = read_u32_at(bytes, offset) { + let high = (word >> 16) as u16; + if matches!(high, 0x32dc | 0x3714 | 0x3715) { + hits.push(format!("0x{high:04x}@0x{offset:08x}")); + } + } + offset += 4; + } + hits +} + +pub(in crate::inspect::smp) fn parse_grounded_progress_hit_offset( + hits: &[String], + high_u16: u16, +) -> Option { + let needle = format!("0x{high_u16:04x}@0x"); + let hit = hits.iter().find(|hit| hit.starts_with(&needle))?; + let offset_hex = hit.split("@0x").nth(1)?; + usize::from_str_radix(offset_hex, 16).ok() +} diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/preamble.rs b/crates/rrt-runtime/src/inspect/smp/bundle/preamble.rs new file mode 100644 index 0000000..7986bac --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/preamble.rs @@ -0,0 +1,76 @@ +use crate::inspect::smp::bundle::{SmpPreamble, SmpPreambleWord, SmpSharedHeader}; +use crate::inspect::smp::catalog::offsets::bundle::{ + PREAMBLE_U32_WORD_COUNT, SHARED_SIGNATURE_WORDS_1_TO_7, +}; + +pub(in crate::inspect::smp) fn parse_preamble(bytes: &[u8]) -> SmpPreamble { + let byte_len = bytes.len().min(PREAMBLE_U32_WORD_COUNT * 4); + let words = bytes[..byte_len] + .chunks_exact(4) + .enumerate() + .map(|(index, chunk)| { + let value_le = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + SmpPreambleWord { + index, + offset: index * 4, + value_le, + value_hex: format!("0x{value_le:08x}"), + } + }) + .collect::>(); + + SmpPreamble { + byte_len, + word_count: words.len(), + words, + } +} + +pub(in crate::inspect::smp) fn parse_shared_header(bytes: &[u8]) -> Option { + let words = read_preamble_words(bytes)?; + let shared_signature_words_1_to_7 = words[1..=7].to_vec(); + let payload_window_words_8_to_9 = words[8..=9].to_vec(); + let reserved_words_10_to_14 = words[10..=14].to_vec(); + let final_flag_word = words[15]; + + Some(SmpSharedHeader { + byte_len: PREAMBLE_U32_WORD_COUNT * 4, + root_kind_word: words[0], + root_kind_word_hex: format!("0x{:08x}", words[0]), + primary_family_tag: words[1], + primary_family_tag_hex: format!("0x{:08x}", words[1]), + shared_signature_hex_words_1_to_7: shared_signature_words_1_to_7 + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + matches_grounded_common_signature: shared_signature_words_1_to_7 + == SHARED_SIGNATURE_WORDS_1_TO_7, + shared_signature_words_1_to_7, + payload_window_hex_words_8_to_9: payload_window_words_8_to_9 + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + payload_window_words_8_to_9, + reserved_words_10_to_14_all_zero: reserved_words_10_to_14.iter().all(|word| *word == 0), + reserved_words_10_to_14, + final_flag_word, + final_flag_word_hex: format!("0x{final_flag_word:08x}"), + }) +} + +pub(in crate::inspect::smp) fn read_preamble_words( + bytes: &[u8], +) -> Option<[u32; PREAMBLE_U32_WORD_COUNT]> { + if bytes.len() < PREAMBLE_U32_WORD_COUNT * 4 { + return None; + } + + let mut words = [0u32; PREAMBLE_U32_WORD_COUNT]; + for (index, chunk) in bytes[..PREAMBLE_U32_WORD_COUNT * 4] + .chunks_exact(4) + .enumerate() + { + words[index] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + } + Some(words) +} diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/report.rs b/crates/rrt-runtime/src/inspect/smp/bundle/report.rs new file mode 100644 index 0000000..f47a28f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/report.rs @@ -0,0 +1,179 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::bundle::{ + SmpContainerProfile, SmpRt3105PostSpanBridgeProbe, SmpRt3105SaveBridgePayloadProbe, + SmpRuntimeAnchorCycleBlock, SmpRuntimePostSpanProbe, SmpRuntimeTrailerBlock, + SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock, +}; +use crate::inspect::smp::common::{ + SmpAsciiPreview, SmpSaveTaggedCollectionHeaderProbe, SmpSaveTrainCollectionDirectoryProbe, + SmpSaveUnclassifiedTaggedCollectionHeaderProbe, +}; +use crate::inspect::smp::events::SmpLoadedEventRuntimeCollectionSummary; +use crate::inspect::smp::map_title::SmpMapTitleHintProbe; +use crate::inspect::smp::profiles::{ + SmpClassicRehydrateProfileProbe, SmpRt3105PackedProfileProbe, SmpRt3105SaveNameTableProbe, + SmpRt3105SaveNamedLocomotiveAvailabilityProbe, +}; +use crate::inspect::smp::regions::{ + SmpSaveRegionFixedRowRunCandidateProbe, SmpSaveRegionQueuedNoticeRecordProbe, + SmpSaveRegionRecordTripletProbe, +}; +use crate::inspect::smp::save_load::SmpSaveLoadSummary; +use crate::inspect::smp::special_conditions::{ + SmpAlignedRuntimeRuleBandProbe, SmpLocomotivePolicyNeighborhoodProbe, + SmpPostSpecialConditionsScalarProbe, SmpPostTextFieldNeighborhoodProbe, + SmpPreRecipeScalarPlateauProbe, SmpRecipeBookSummaryProbe, SmpSpecialConditionsProbe, +}; +use crate::inspect::smp::structures::{ + SmpSavePlacedStructureDynamicSideBufferProbe, SmpSavePlacedStructureRecordTripletProbe, +}; +use crate::inspect::smp::world::{ + SmpLoadedChairmanProfileTable, SmpLoadedCompanyRoster, SmpSaveWorldEconomicTuningProbe, + SmpSaveWorldFinanceNeighborhoodProbe, SmpSaveWorldIssue37Probe, + SmpSaveWorldSelectionContextProbe, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpKnownTagHit { + pub tag_id: u16, + pub tag_hex: String, + pub label: String, + pub grounded_meaning: String, + pub hit_count: usize, + pub sample_offsets: Vec, + pub last_offset: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPreambleWord { + pub index: usize, + pub offset: usize, + pub value_le: u32, + pub value_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPreamble { + pub byte_len: usize, + pub word_count: usize, + pub words: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + +pub struct SmpSharedHeader { + pub byte_len: usize, + pub root_kind_word: u32, + pub root_kind_word_hex: String, + pub primary_family_tag: u32, + pub primary_family_tag_hex: String, + pub shared_signature_words_1_to_7: Vec, + pub shared_signature_hex_words_1_to_7: Vec, + pub matches_grounded_common_signature: bool, + pub payload_window_words_8_to_9: Vec, + pub payload_window_hex_words_8_to_9: Vec, + pub reserved_words_10_to_14: Vec, + pub reserved_words_10_to_14_all_zero: bool, + pub final_flag_word: u32, + pub final_flag_word_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpHeaderVariantProbe { + pub variant_family: String, + pub variant_evidence: Vec, + pub is_known_family: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpEarlyContentProbe { + pub first_post_text_nonzero_offset: usize, + pub zero_pad_after_text_len: usize, + pub first_post_text_block_len: usize, + pub first_post_text_block_hex: String, + pub trailing_zero_pad_after_first_block_len: usize, + pub secondary_nonzero_offset: Option, + pub secondary_aligned_word_window_offset: Option, + pub secondary_aligned_word_window_words: Vec, + pub secondary_aligned_word_window_hex_words: Vec, + pub secondary_preview_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSecondaryVariantProbe { + pub aligned_window_offset: usize, + pub words: Vec, + pub hex_words: Vec, + pub variant_family: String, + pub variant_evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpInspectionReport { + pub inspection_mode: String, + pub file_extension_hint: Option, + pub file_size: usize, + pub sha256: String, + pub preamble: SmpPreamble, + pub shared_header: Option, + pub header_variant_probe: Option, + pub first_ascii_run: Option, + pub early_content_probe: Option, + pub secondary_variant_probe: Option, + pub container_profile: Option, + pub save_bootstrap_block: Option, + pub save_anchor_run_block: Option, + pub runtime_anchor_cycle_block: Option, + pub runtime_trailer_block: Option, + pub runtime_post_span_probe: Option, + pub rt3_105_post_span_bridge_probe: Option, + pub rt3_105_save_bridge_payload_probe: Option, + pub save_world_selection_context_probe: Option, + pub save_world_issue_37_probe: Option, + pub save_world_economic_tuning_probe: Option, + pub save_world_finance_neighborhood_probe: Option, + pub save_company_collection_header_probe: Option, + pub save_chairman_profile_collection_header_probe: Option, + pub save_train_collection_header_probe: Option, + pub save_train_collection_directory_probe: Option, + pub save_region_collection_header_probe: Option, + pub save_region_record_triplet_probe: Option, + #[serde(default)] + pub save_region_queued_notice_record_probe: Option, + #[serde(default)] + pub save_region_fixed_row_run_candidate_probe: Option, + pub save_placed_structure_collection_header_probe: Option, + pub save_placed_structure_record_triplet_probe: + Option, + #[serde(default)] + pub save_placed_structure_dynamic_side_buffer_probe: + Option, + #[serde(default)] + pub save_unclassified_tagged_collection_header_probes: + Vec, + #[serde(default)] + pub save_company_roster_probe: Option, + #[serde(default)] + pub save_chairman_profile_table_probe: Option, + #[serde(default)] + pub map_title_hint_probe: Option, + pub rt3_105_save_name_table_probe: Option, + pub rt3_105_save_named_locomotive_availability_probe: + Option, + pub special_conditions_probe: Option, + pub smp_aligned_runtime_rule_band_probe: Option, + pub post_special_conditions_scalar_probe: Option, + pub post_text_field_neighborhood_probe: Option, + pub locomotive_policy_neighborhood_probe: Option, + pub pre_recipe_scalar_plateau_probe: Option, + pub recipe_book_summary_probe: Option, + pub classic_rehydrate_profile_probe: Option, + pub rt3_105_packed_profile_probe: Option, + pub save_load_summary: Option, + pub event_runtime_collection_summary: Option, + pub contains_grounded_runtime_tags: bool, + pub known_tag_hits: Vec, + pub notes: Vec, + pub warnings: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/trailer.rs b/crates/rrt-runtime/src/inspect/smp/bundle/trailer.rs new file mode 100644 index 0000000..f8b2faf --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/trailer.rs @@ -0,0 +1,134 @@ +use crate::inspect::smp::bundle::{ + SmpContainerProfile, SmpRuntimeAnchorCycleBlock, SmpRuntimeTrailerBlock, +}; + +pub(in crate::inspect::smp) fn parse_runtime_trailer_block( + container_profile: Option<&SmpContainerProfile>, + runtime_anchor_cycle_block: Option<&SmpRuntimeAnchorCycleBlock>, +) -> Option { + let profile = container_profile?; + let anchor = runtime_anchor_cycle_block?; + let words = &anchor.trailer_words; + if words.len() < 16 { + return None; + } + + let trailer_family = match profile.profile_family.as_str() { + "rt3-classic-save-container-v1" + if words[..6] + == [ + 0x00020000, 0x00030000, 0x00010000, 0x00010000, 0x00010000, 0x00020000, + ] => + { + "rt3-classic-save-trailer-v1" + } + "rt3-classic-sandbox-container-v1" + if words[..6] + == [ + 0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, + ] => + { + "rt3-classic-sandbox-trailer-v1" + } + "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + if words[..6] + == [ + 0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, + ] => + { + "rt3-105-save-trailer-v1" + } + "rt3-105-sandbox-container-v1" + if words[..6] + == [ + 0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, + ] => + { + "rt3-105-sandbox-trailer-v1" + } + _ => "unknown", + } + .to_string(); + + let tag_chunk_id_u16 = (words[6] >> 16) as u16; + let length_high_u16 = (words[7] >> 16) as u16; + let selector_high_u16 = (words[8] >> 16) as u16; + let descriptor_high_u16 = (words[10] >> 16) as u16; + let tag_chunk_id_grounded_alignment = + classify_runtime_trailer_chunk_id_grounded_alignment(tag_chunk_id_u16).map(str::to_string); + + let mut trailer_evidence = vec![ + format!("container profile {}", profile.profile_family), + format!( + "prefix words {}", + words[..6] + .iter() + .map(|word| format!("0x{word:08x}")) + .collect::>() + .join(", ") + ), + format!("high-16 chunk id 0x{tag_chunk_id_u16:04x} from trailer word 6"), + format!("high-16 span 0x{length_high_u16:04x} from trailer word 7"), + format!("high-16 selector 0x{selector_high_u16:04x} from trailer word 8"), + format!("high-16 descriptor 0x{descriptor_high_u16:04x} from trailer word 10"), + ]; + if let Some(alignment) = &tag_chunk_id_grounded_alignment { + trailer_evidence.push(alignment.clone()); + } + + Some(SmpRuntimeTrailerBlock { + profile_family: profile.profile_family.clone(), + trailer_family, + trailer_evidence, + trailer_offset: anchor.trailer_offset, + prefix_words_0_to_5: words[..6].to_vec(), + prefix_hex_words_0_to_5: words[..6] + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + tag_word_6: words[6], + tag_word_6_hex: format!("0x{:08x}", words[6]), + tag_chunk_id_u16, + tag_chunk_id_hex: format!("0x{tag_chunk_id_u16:04x}"), + tag_chunk_id_grounded_alignment, + length_word_7: words[7], + length_word_7_hex: format!("0x{:08x}", words[7]), + length_high_u16, + length_high_hex: format!("0x{length_high_u16:04x}"), + selector_word_8: words[8], + selector_word_8_hex: format!("0x{:08x}", words[8]), + selector_high_u16, + selector_high_hex: format!("0x{selector_high_u16:04x}"), + layout_word_9: words[9], + layout_word_9_hex: format!("0x{:08x}", words[9]), + descriptor_word_10: words[10], + descriptor_word_10_hex: format!("0x{:08x}", words[10]), + descriptor_high_u16, + descriptor_high_hex: format!("0x{descriptor_high_u16:04x}"), + descriptor_word_11: words[11], + descriptor_word_11_hex: format!("0x{:08x}", words[11]), + counter_word_12: words[12], + counter_word_12_hex: format!("0x{:08x}", words[12]), + offset_word_13: words[13], + offset_word_13_hex: format!("0x{:08x}", words[13]), + span_word_14: words[14], + span_word_14_hex: format!("0x{:08x}", words[14]), + mode_word_15: words[15], + mode_word_15_hex: format!("0x{:08x}", words[15]), + words: words.to_vec(), + hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(), + }) +} + +pub(in crate::inspect::smp) fn classify_runtime_trailer_chunk_id_grounded_alignment( + tag_chunk_id_u16: u16, +) -> Option<&'static str> { + match tag_chunk_id_u16 { + 0x2ee1 => Some( + "High-16 chunk id 0x2ee1 matches the disassembly-grounded map-style bundle family already read by shell_setup_load_selected_profile_bundle_into_payload_record.", + ), + _ => None, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/bundle/variants.rs b/crates/rrt-runtime/src/inspect/smp/bundle/variants.rs new file mode 100644 index 0000000..cce8ca9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/bundle/variants.rs @@ -0,0 +1,431 @@ +use crate::inspect::smp::bundle::{ + SmpContainerProfile, SmpEarlyContentProbe, SmpHeaderVariantProbe, SmpSecondaryVariantProbe, + SmpSharedHeader, +}; +use crate::inspect::smp::catalog::offsets::bundle::{ + EARLY_ALIGNED_WORD_WINDOW_COUNT, EARLY_PREVIEW_BYTE_LIMIT, EARLY_ZERO_RUN_THRESHOLD, +}; +use crate::inspect::smp::common::{ + SmpAsciiPreview, find_next_nonzero_offset, find_zero_run, hex_encode, read_u32_window, +}; + +pub(in crate::inspect::smp) fn classify_header_variant_probe( + shared_header: &SmpSharedHeader, +) -> SmpHeaderVariantProbe { + let words = &shared_header.shared_signature_words_1_to_7; + let root = shared_header.root_kind_word; + let final_flag = shared_header.final_flag_word; + + let (variant_family, evidence, is_known_family) = match (root, words.as_slice(), final_flag) { + ( + 0x00002649, + [ + 0x00002ee0, + 0x00040001, + 0x00028000, + 0x00010000, + 0x00000771, + 0x00000771, + 0x00000771, + ], + 0x00000001, + ) => ( + "rt3-105-gmx-header-v1".to_string(), + vec![ + "root kind word 0x00002649".to_string(), + "1.05 common signature words 1..7".to_string(), + "final flag 0x00000001".to_string(), + ], + true, + ), + ( + 0x000025e5, + [ + 0x00002ee0, + 0x00040001, + 0x00028000, + 0x00010000, + 0x00000771, + 0x00000771, + 0x00000771, + ], + 0x00000000, + ) => ( + "rt3-105-common-header-v1".to_string(), + vec![ + "root kind word 0x000025e5".to_string(), + "1.05 common signature words 1..7".to_string(), + "final flag 0x00000000".to_string(), + ], + true, + ), + ( + 0x000025e5, + [ + 0x00002ee0, + 0x00040001, + 0x00018000, + 0x00010000, + 0x00000746, + 0x00000746, + 0x00000746, + ], + 0x00000000, + ) => ( + "rt3-105-scenario-save-header-v1".to_string(), + vec![ + "root kind word 0x000025e5".to_string(), + "1.05 scenario-save signature words 1..7".to_string(), + "final flag 0x00000000".to_string(), + ], + true, + ), + ( + 0x000025e5, + [ + 0x00002ee0, + 0x0001c001, + 0x00018000, + 0x00010000, + 0x00000754, + 0x00000754, + 0x00000754, + ], + 0x00000000, + ) => ( + "rt3-105-alt-save-header-v1".to_string(), + vec![ + "root kind word 0x000025e5".to_string(), + "1.05 alternate-save signature words 1..7".to_string(), + "final flag 0x00000000".to_string(), + ], + true, + ), + ( + 0x000026ad, + [ + 0x00002ee0, + 0x00014001, + 0x00020000, + 0x00010000, + 0x00000725, + 0x00000725, + 0x00000725, + ], + 0x00000100, + ) => ( + "rt3-classic-gms-header-v1".to_string(), + vec![ + "root kind word 0x000026ad".to_string(), + "classic save signature words 1..7".to_string(), + "final flag 0x00000100".to_string(), + ], + true, + ), + ( + 0x000026ad, + [ + 0x00002ee0, + 0x0001c001, + 0x00018000, + 0x00010000, + 0x00000765, + 0x00000765, + 0x00000765, + ], + 0x00000001, + ) => ( + "rt3-classic-gmx-header-v1".to_string(), + vec![ + "root kind word 0x000026ad".to_string(), + "classic sandbox signature words 1..7".to_string(), + "final flag 0x00000001".to_string(), + ], + true, + ), + (0x000025e5, [0x00002ee0, _, _, 0x00010000, _, _, _], 0x00000000 | 0x00000100) => ( + "rt3-map-header-family".to_string(), + vec![ + "root kind word 0x000025e5".to_string(), + "map-family anchor 0x00002ee0".to_string(), + "word4 0x00010000".to_string(), + ], + true, + ), + _ => ( + "unknown".to_string(), + vec![format!( + "root=0x{root:08x}, words1..7={}, final=0x{final_flag:08x}", + words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect::>() + .join(", ") + )], + false, + ), + }; + + SmpHeaderVariantProbe { + variant_family, + variant_evidence: evidence, + is_known_family, + } +} + +pub(in crate::inspect::smp) fn probe_early_content_layout( + bytes: &[u8], + ascii_run: &SmpAsciiPreview, +) -> Option { + let search_start = ascii_run.offset + ascii_run.byte_len; + let first_post_text_nonzero_offset = find_next_nonzero_offset(bytes, search_start)?; + let zero_pad_after_text_len = first_post_text_nonzero_offset.saturating_sub(search_start); + let first_zero_run_after_block = find_zero_run( + bytes, + first_post_text_nonzero_offset, + EARLY_ZERO_RUN_THRESHOLD, + ) + .unwrap_or(bytes.len()); + let first_post_text_block = &bytes[first_post_text_nonzero_offset..first_zero_run_after_block]; + let secondary_nonzero_offset = find_next_nonzero_offset(bytes, first_zero_run_after_block); + let trailing_zero_pad_after_first_block_len = secondary_nonzero_offset + .map(|offset| offset.saturating_sub(first_zero_run_after_block)) + .unwrap_or_else(|| bytes.len().saturating_sub(first_zero_run_after_block)); + let secondary_aligned_word_window_offset = secondary_nonzero_offset.map(|offset| offset & !0x3); + let secondary_aligned_word_window_words = secondary_aligned_word_window_offset + .map(|offset| read_u32_window(bytes, offset, EARLY_ALIGNED_WORD_WINDOW_COUNT)) + .unwrap_or_default(); + let secondary_preview_hex = secondary_nonzero_offset + .map(|offset| { + hex_encode(&bytes[offset..bytes.len().min(offset + EARLY_PREVIEW_BYTE_LIMIT)]) + }) + .unwrap_or_default(); + + Some(SmpEarlyContentProbe { + first_post_text_nonzero_offset, + zero_pad_after_text_len, + first_post_text_block_len: first_post_text_block.len(), + first_post_text_block_hex: hex_encode(first_post_text_block), + trailing_zero_pad_after_first_block_len, + secondary_nonzero_offset, + secondary_aligned_word_window_offset, + secondary_aligned_word_window_hex_words: secondary_aligned_word_window_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + secondary_aligned_word_window_words, + secondary_preview_hex, + }) +} + +pub(in crate::inspect::smp) fn classify_secondary_variant_probe( + probe: &SmpEarlyContentProbe, +) -> Option { + let aligned_window_offset = probe.secondary_aligned_word_window_offset?; + let words = probe.secondary_aligned_word_window_words.clone(); + if words.is_empty() { + return None; + } + + let mut evidence = Vec::new(); + let variant_family = match words.as_slice() { + [0x001e0000, 0x86a00100, 0x03000001, 0xf0000100, ..] => { + evidence.push("leading word 0x001e0000".to_string()); + evidence.push("anchor word 0x86a00100".to_string()); + evidence.push("third/fourth words 0x03000001 and 0xf0000100".to_string()); + "rt3-gms-family-v1".to_string() + } + [0x000a0000, 0x49f00100, 0x00000002, 0xa0000000, ..] => { + evidence.push("leading word 0x000a0000".to_string()); + evidence.push("anchor word 0x49f00100".to_string()); + evidence.push("third/fourth words 0x00000002 and 0xa0000000".to_string()); + "rt3-gmx-family-v1".to_string() + } + [0x001c0000, 0x86a00100, 0x00000001, 0xa0000000, ..] => { + evidence.push("leading word 0x001c0000".to_string()); + evidence.push("anchor word 0x86a00100".to_string()); + evidence.push("third/fourth words 0x00000001 and 0xa0000000".to_string()); + "rt3-105-gms-family-v1".to_string() + } + [0x00190000, 0x86a00100, 0x00000001, 0xa0000000, ..] => { + evidence.push("leading word 0x00190000".to_string()); + evidence.push("anchor word 0x86a00100".to_string()); + evidence.push("third/fourth words 0x00000001 and 0xa0000000".to_string()); + "rt3-105-gmx-family-v1".to_string() + } + [0x00130000, 0x86a00100, 0x21000001, 0xa0000100, ..] => { + evidence.push("leading word 0x00130000".to_string()); + evidence.push("anchor word 0x86a00100".to_string()); + evidence.push("third/fourth words 0x21000001 and 0xa0000100".to_string()); + "rt3-105-gms-scenario-family-v1".to_string() + } + [0x00140000, 0x93e00100, 0x00000004, 0xa0000000, ..] => { + evidence.push("leading word 0x00140000".to_string()); + evidence.push("anchor word 0x93e00100".to_string()); + evidence.push("third/fourth words 0x00000004 and 0xa0000000".to_string()); + "rt3-map-secondary-family-v1".to_string() + } + [0x00010000, 0x49f00100, 0x00000002, 0xa0000000, ..] => { + evidence.push("leading word 0x00010000".to_string()); + evidence.push("anchor word 0x49f00100".to_string()); + evidence.push("third/fourth words 0x00000002 and 0xa0000000".to_string()); + "rt3-105-gms-alt-family-v1".to_string() + } + [0x86a00100, 0x00000001, 0xa0000000, 0x00000186, ..] => { + evidence.push("window starts directly on 0x86a00100".to_string()); + evidence.push("likely same family with missing leading unaligned word".to_string()); + "rt3-family-unaligned-anchor".to_string() + } + _ => { + evidence.push(format!( + "unrecognized leading words: {}", + words + .iter() + .take(4) + .map(|word| format!("0x{word:08x}")) + .collect::>() + .join(", ") + )); + "unknown".to_string() + } + }; + + Some(SmpSecondaryVariantProbe { + aligned_window_offset, + hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(), + words, + variant_family, + variant_evidence: evidence, + }) +} + +pub(in crate::inspect::smp) fn classify_container_profile( + file_extension_hint: Option<&str>, + header_variant_probe: Option<&SmpHeaderVariantProbe>, + secondary_variant_probe: Option<&SmpSecondaryVariantProbe>, +) -> Option { + let header_family = header_variant_probe.map(|probe| probe.variant_family.as_str())?; + let secondary_family = secondary_variant_probe.map(|probe| probe.variant_family.as_str())?; + let extension = file_extension_hint.unwrap_or(""); + + let (profile_family, profile_evidence, is_known_profile) = + match (extension, header_family, secondary_family) { + ("gms", "rt3-classic-gms-header-v1", "rt3-gms-family-v1") => ( + "rt3-classic-save-container-v1".to_string(), + vec![ + "extension .gms".to_string(), + "classic save header family".to_string(), + "classic save secondary window family".to_string(), + ], + true, + ), + ("gmx", "rt3-classic-gmx-header-v1", "rt3-gmx-family-v1") => ( + "rt3-classic-sandbox-container-v1".to_string(), + vec![ + "extension .gmx".to_string(), + "classic sandbox header family".to_string(), + "classic sandbox secondary window family".to_string(), + ], + true, + ), + ("gms", "rt3-105-common-header-v1", "rt3-105-gms-family-v1") => ( + "rt3-105-save-container-v1".to_string(), + vec![ + "extension .gms".to_string(), + "1.05 common header family".to_string(), + "1.05 save secondary window family".to_string(), + ], + true, + ), + ("gms", "rt3-105-scenario-save-header-v1", "rt3-105-gms-scenario-family-v1") => ( + "rt3-105-scenario-save-container-v1".to_string(), + vec![ + "extension .gms".to_string(), + "1.05 scenario-save header family".to_string(), + "1.05 scenario-save secondary window family".to_string(), + ], + true, + ), + ("gms", "rt3-105-alt-save-header-v1", "rt3-105-gms-alt-family-v1") => ( + "rt3-105-alt-save-container-v1".to_string(), + vec![ + "extension .gms".to_string(), + "1.05 alternate-save header family".to_string(), + "1.05 alternate-save secondary window family".to_string(), + ], + true, + ), + ("gmx", "rt3-105-gmx-header-v1", "rt3-105-gmx-family-v1") => ( + "rt3-105-sandbox-container-v1".to_string(), + vec![ + "extension .gmx".to_string(), + "1.05 sandbox header family".to_string(), + "1.05 sandbox secondary window family".to_string(), + ], + true, + ), + ("gmp", "rt3-105-common-header-v1", "rt3-family-unaligned-anchor") => ( + "rt3-105-map-container-v1".to_string(), + vec![ + "extension .gmp".to_string(), + "1.05 common header family".to_string(), + "map-style secondary unaligned anchor".to_string(), + ], + true, + ), + ("gmp", "rt3-105-scenario-save-header-v1", "unknown") => ( + "rt3-105-scenario-map-container-v1".to_string(), + vec![ + "extension .gmp".to_string(), + "1.05 scenario-map header family".to_string(), + "fixed candidate-availability table range present despite unknown early secondary window".to_string(), + ], + true, + ), + ("gmp", "rt3-105-alt-save-header-v1", "unknown") => ( + "rt3-105-alt-map-container-v1".to_string(), + vec![ + "extension .gmp".to_string(), + "1.05 alternate-map header family".to_string(), + "fixed candidate-availability table range present despite unknown early secondary window".to_string(), + ], + true, + ), + ("gmp", "rt3-map-header-family", "rt3-family-unaligned-anchor") => ( + "rt3-map-container-family".to_string(), + vec![ + "extension .gmp".to_string(), + "map header family".to_string(), + "map-style secondary unaligned anchor".to_string(), + ], + true, + ), + ("gmp", "rt3-map-header-family", "rt3-map-secondary-family-v1") => ( + "rt3-map-container-family".to_string(), + vec![ + "extension .gmp".to_string(), + "map header family".to_string(), + "observed map secondary window family".to_string(), + ], + true, + ), + (_, header_family, secondary_family) => ( + "unknown".to_string(), + vec![ + format!( + "extension {}", + if extension.is_empty() { "" } else { extension } + ), + format!("header family {header_family}"), + format!("secondary family {secondary_family}"), + ], + false, + ), + }; + + Some(SmpContainerProfile { + profile_family, + profile_evidence, + is_known_profile, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/conditions/cargo.rs b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/cargo.rs new file mode 100644 index 0000000..698dcd3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/cargo.rs @@ -0,0 +1,142 @@ +use crate::inspect::smp::catalog::conditions::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) struct KnownCargoSlotDefinition { + pub(in crate::inspect::smp) slot_id: u32, + pub(in crate::inspect::smp) label: &'static str, + pub(in crate::inspect::smp) cargo_class: RuntimeCargoClass, + pub(in crate::inspect::smp) descriptor_id: u32, +} + +pub(in crate::inspect::smp) const KNOWN_CARGO_SLOT_DEFINITIONS: [KnownCargoSlotDefinition; 11] = [ + KnownCargoSlotDefinition { + slot_id: 1, + label: "Cargo Production Slot 1", + cargo_class: RuntimeCargoClass::Factory, + descriptor_id: 230, + }, + KnownCargoSlotDefinition { + slot_id: 2, + label: "Cargo Production Slot 2", + cargo_class: RuntimeCargoClass::Factory, + descriptor_id: 231, + }, + KnownCargoSlotDefinition { + slot_id: 3, + label: "Cargo Production Slot 3", + cargo_class: RuntimeCargoClass::Factory, + descriptor_id: 232, + }, + KnownCargoSlotDefinition { + slot_id: 4, + label: "Cargo Production Slot 4", + cargo_class: RuntimeCargoClass::Factory, + descriptor_id: 233, + }, + KnownCargoSlotDefinition { + slot_id: 5, + label: "Cargo Production Slot 5", + cargo_class: RuntimeCargoClass::FarmMine, + descriptor_id: 234, + }, + KnownCargoSlotDefinition { + slot_id: 6, + label: "Cargo Production Slot 6", + cargo_class: RuntimeCargoClass::FarmMine, + descriptor_id: 235, + }, + KnownCargoSlotDefinition { + slot_id: 7, + label: "Cargo Production Slot 7", + cargo_class: RuntimeCargoClass::FarmMine, + descriptor_id: 236, + }, + KnownCargoSlotDefinition { + slot_id: 8, + label: "Cargo Production Slot 8", + cargo_class: RuntimeCargoClass::FarmMine, + descriptor_id: 237, + }, + KnownCargoSlotDefinition { + slot_id: 9, + label: "Cargo Production Slot 9", + cargo_class: RuntimeCargoClass::Other, + descriptor_id: 238, + }, + KnownCargoSlotDefinition { + slot_id: 10, + label: "Cargo Production Slot 10", + cargo_class: RuntimeCargoClass::Other, + descriptor_id: 239, + }, + KnownCargoSlotDefinition { + slot_id: 11, + label: "Cargo Production Slot 11", + cargo_class: RuntimeCargoClass::Other, + descriptor_id: 240, + }, +]; + +pub(in crate::inspect::smp) const GROUNDED_LOCOMOTIVE_PREFIX: [&str; 61] = [ + "2-D-2", + "E-88", + "Adler 2-2-2", + "USA 103", + "American 4-4-0", + "Atlantic 4-4-2", + "Baldwin 0-6-0", + "Be 5/7", + "Beuth 2-2-2", + "Big Boy 4-8-8-4", + "C55 Deltic", + "Camelback 0-6-0", + "Challenger 4-6-6-4", + "Class 01 4-6-2", + "Class 103", + "Class 132", + "Class 500 4-6-0", + "Class 9100", + "Class EF 66", + "Class 6E", + "Consolidation 2-8-0", + "Crampton 4-2-0", + "DD 080-X", + "DD40AX", + "Duke Class 4-4-0", + "E18", + "E428", + "Brenner E412", + "E60CP", + "Eight Wheeler 4-4-0", + "EP-2 Bipolar", + "ET22", + "F3", + "Fairlie 0-6-6-0", + "Firefly 2-2-2", + "FP45", + "Ge 6/6 Crocodile", + "GG1", + "GP7", + "H10 2-8-2", + "HST 125", + "Kriegslok 2-10-0", + "Mallard 4-6-2", + "Norris 4-2-0", + "Northern 4-8-4", + "Orca NX462", + "Pacific 4-6-2", + "Planet 2-2-0", + "Re 6/6", + "Red Devil 4-8-4", + "S3 4-4-0", + "NA-90D", + "Shay (2-Truck)", + "Shinkansen Series 0", + "Stirling 4-2-2", + "Trans-Euro", + "V200", + "VL80T", + "GP 35", + "U1", + "Zephyr", +]; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/conditions/ids.rs b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/ids.rs new file mode 100644 index 0000000..736ea2a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/ids.rs @@ -0,0 +1,35 @@ +pub(in crate::inspect::smp) const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; +pub(in crate::inspect::smp) const REAL_WORLD_VARIABLE_1_CONDITION_ID: i32 = 2241; +pub(in crate::inspect::smp) const REAL_WORLD_VARIABLE_2_CONDITION_ID: i32 = 2242; +pub(in crate::inspect::smp) const REAL_WORLD_VARIABLE_3_CONDITION_ID: i32 = 2243; +pub(in crate::inspect::smp) const REAL_WORLD_VARIABLE_4_CONDITION_ID: i32 = 2244; +pub(in crate::inspect::smp) const REAL_COMPANY_VARIABLE_1_CONDITION_ID: i32 = 2245; +pub(in crate::inspect::smp) const REAL_COMPANY_VARIABLE_2_CONDITION_ID: i32 = 2246; +pub(in crate::inspect::smp) const REAL_COMPANY_VARIABLE_3_CONDITION_ID: i32 = 2247; +pub(in crate::inspect::smp) const REAL_COMPANY_VARIABLE_4_CONDITION_ID: i32 = 2248; +pub(in crate::inspect::smp) const REAL_PLAYER_VARIABLE_1_CONDITION_ID: i32 = 2249; +pub(in crate::inspect::smp) const REAL_PLAYER_VARIABLE_2_CONDITION_ID: i32 = 2250; +pub(in crate::inspect::smp) const REAL_PLAYER_VARIABLE_3_CONDITION_ID: i32 = 2251; +pub(in crate::inspect::smp) const REAL_PLAYER_VARIABLE_4_CONDITION_ID: i32 = 2252; +pub(in crate::inspect::smp) const REAL_TERRITORY_VARIABLE_1_CONDITION_ID: i32 = 2253; +pub(in crate::inspect::smp) const REAL_TERRITORY_VARIABLE_2_CONDITION_ID: i32 = 2254; +pub(in crate::inspect::smp) const REAL_TERRITORY_VARIABLE_3_CONDITION_ID: i32 = 2255; +pub(in crate::inspect::smp) const REAL_TERRITORY_VARIABLE_4_CONDITION_ID: i32 = 2256; +pub(in crate::inspect::smp) const REAL_CHAIRMAN_CASH_CONDITION_ID: i32 = 2218; +pub(in crate::inspect::smp) const REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID: i32 = 2239; +pub(in crate::inspect::smp) const REAL_CHAIRMAN_NET_WORTH_CONDITION_ID: i32 = 2240; +pub(in crate::inspect::smp) const REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID: i32 = 1247; +pub(in crate::inspect::smp) const REAL_INVESTOR_CONFIDENCE_CONDITION_ID: i32 = 2366; +pub(in crate::inspect::smp) const REAL_CREDIT_RATING_CONDITION_ID: i32 = 2367; +pub(in crate::inspect::smp) const REAL_PRIME_RATE_CONDITION_ID: i32 = 2368; +pub(in crate::inspect::smp) const REAL_MANAGEMENT_ATTITUDE_CONDITION_ID: i32 = 2369; +pub(in crate::inspect::smp) const REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID: i32 = 2620; +pub(in crate::inspect::smp) const REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID: i32 = 200; +pub(in crate::inspect::smp) const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422; +pub(in crate::inspect::smp) const REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID: i32 = 2423; +pub(in crate::inspect::smp) const REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2418; +pub(in crate::inspect::smp) const REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2419; +pub(in crate::inspect::smp) const REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2420; +pub(in crate::inspect::smp) const REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2421; +pub(in crate::inspect::smp) const REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547; +pub(in crate::inspect::smp) const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/conditions/kinds.rs b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/kinds.rs new file mode 100644 index 0000000..ede1a53 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/kinds.rs @@ -0,0 +1,43 @@ +use crate::inspect::smp::catalog::conditions::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) enum RealOrdinaryConditionMetric { + WorldVariable(u32), + Company(RuntimeCompanyMetric), + CompanyVariable(u32), + PlayerVariable(u32), + Chairman(RuntimeChairmanMetric), + Territory(RuntimeTerritoryMetric), + TerritoryVariable(u32), + CompanyTerritory(RuntimeTrackMetric), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) enum RealWorldConditionKind { + SpecialCondition { label: &'static str }, + CandidateAvailability, + NamedLocomotiveAvailability, + NamedLocomotiveCost, + CargoProductionSlot, + CargoProductionTotal, + FactoryProductionTotal, + FarmMineProductionTotal, + OtherCargoProductionTotal, + LimitedTrackBuildingAmount, + TerritoryAccessCost, + EconomicStatus, + WorldFlag { key: &'static str }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) enum RealOrdinaryConditionKind { + Numeric(RealOrdinaryConditionMetric), + WorldState(RealWorldConditionKind), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) struct RealOrdinaryConditionMetadata { + pub(in crate::inspect::smp) raw_condition_id: i32, + pub(in crate::inspect::smp) label: &'static str, + pub(in crate::inspect::smp) kind: RealOrdinaryConditionKind, +} diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/conditions/lookup.rs b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/lookup.rs new file mode 100644 index 0000000..991dbea --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/lookup.rs @@ -0,0 +1,38 @@ +use crate::inspect::smp::catalog::conditions::*; +use crate::inspect::smp::catalog::{ + known_special_condition_definition_for_label_id, real_grouped_effect_descriptor_metadata, +}; + +pub(in crate::inspect::smp) fn real_ordinary_condition_metadata( + raw_condition_id: i32, +) -> Option { + REAL_ORDINARY_CONDITION_METADATA + .iter() + .copied() + .find(|metadata| metadata.raw_condition_id == raw_condition_id) + .or_else(|| { + known_special_condition_definition_for_label_id(raw_condition_id as u32).map( + |definition| { + let kind = if let Some(world_toggle) = + real_grouped_effect_descriptor_metadata(110 + definition.slot_index as u32) + .filter(|metadata| metadata.parameter_family == "world_flag_toggle") + { + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { + key: world_toggle.runtime_key.unwrap_or(world_toggle.label), + }) + } else { + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::SpecialCondition { + label: definition.label, + }, + ) + }; + RealOrdinaryConditionMetadata { + raw_condition_id, + label: definition.label, + kind, + } + }, + ) + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/conditions/mod.rs b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/mod.rs new file mode 100644 index 0000000..b721695 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/mod.rs @@ -0,0 +1,13 @@ +use crate::inspect::smp::catalog::*; + +mod cargo; +mod ids; +mod kinds; +mod lookup; +mod table; + +pub(in crate::inspect::smp) use cargo::*; +pub(in crate::inspect::smp) use ids::*; +pub(in crate::inspect::smp) use kinds::*; +pub(in crate::inspect::smp) use lookup::*; +pub(in crate::inspect::smp) use table::*; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/conditions/table.rs b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/table.rs new file mode 100644 index 0000000..f132e99 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/conditions/table.rs @@ -0,0 +1,351 @@ +use crate::inspect::smp::catalog::conditions::*; + +pub(in crate::inspect::smp) const REAL_ORDINARY_CONDITION_METADATA: + [RealOrdinaryConditionMetadata; 56] = [ + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_WORLD_VARIABLE_1_CONDITION_ID, + label: "Game Variable 1", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(1)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_WORLD_VARIABLE_2_CONDITION_ID, + label: "Game Variable 2", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(2)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_WORLD_VARIABLE_3_CONDITION_ID, + label: "Game Variable 3", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(3)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_WORLD_VARIABLE_4_CONDITION_ID, + label: "Game Variable 4", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(4)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_COMPANY_VARIABLE_1_CONDITION_ID, + label: "Company Variable 1", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(1)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_COMPANY_VARIABLE_2_CONDITION_ID, + label: "Company Variable 2", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(2)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_COMPANY_VARIABLE_3_CONDITION_ID, + label: "Company Variable 3", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(3)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_COMPANY_VARIABLE_4_CONDITION_ID, + label: "Company Variable 4", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(4)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PLAYER_VARIABLE_1_CONDITION_ID, + label: "Player Variable 1", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(1)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PLAYER_VARIABLE_2_CONDITION_ID, + label: "Player Variable 2", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(2)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PLAYER_VARIABLE_3_CONDITION_ID, + label: "Player Variable 3", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(3)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PLAYER_VARIABLE_4_CONDITION_ID, + label: "Player Variable 4", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(4)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_VARIABLE_1_CONDITION_ID, + label: "Territory Variable 1", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(1)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_VARIABLE_2_CONDITION_ID, + label: "Territory Variable 2", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(2)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_VARIABLE_3_CONDITION_ID, + label: "Territory Variable 3", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(3)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_VARIABLE_4_CONDITION_ID, + label: "Territory Variable 4", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(4)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 1802, + label: "Current Cash", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::CurrentCash, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID, + label: "Player Cash", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( + RuntimeChairmanMetric::CurrentCash, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID, + label: "Player Stock Value", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( + RuntimeChairmanMetric::HoldingsValueTotal, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CHAIRMAN_NET_WORTH_CONDITION_ID, + label: "Player Net Worth", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( + RuntimeChairmanMetric::NetWorthTotal, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID, + label: "Purchasing Power", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( + RuntimeChairmanMetric::PurchasingPowerTotal, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 951, + label: "Total Debt", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TotalDebt, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_INVESTOR_CONFIDENCE_CONDITION_ID, + label: "Investor Confidence", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::InvestorConfidence, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CREDIT_RATING_CONDITION_ID, + label: "Credit Rating", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::CreditRating, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PRIME_RATE_CONDITION_ID, + label: "Prime Rate", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::PrimeRate, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_MANAGEMENT_ATTITUDE_CONDITION_ID, + label: "Management Attitude", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::ManagementAttitude, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID, + label: "Book Value Per Share", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::BookValuePerShare, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2293, + label: "Company Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesTotal, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2294, + label: "Company Single Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesSingle, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2295, + label: "Company Double Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesDouble, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2296, + label: "Company Transition Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesTransition, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2297, + label: "Company Electric Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesElectric, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2298, + label: "Company Non-Electric Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesNonElectric, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2313, + label: "Territory Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesTotal, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2314, + label: "Territory Single Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesSingle, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2315, + label: "Territory Double Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesDouble, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2316, + label: "Territory Transition Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesTransition, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2317, + label: "Territory Electric Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesElectric, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2318, + label: "Territory Non-Electric Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesNonElectric, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2323, + label: "Company-Territory Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Total, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2324, + label: "Company-Territory Single Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Single, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2325, + label: "Company-Territory Double Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Double, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2326, + label: "Company-Territory Transition Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Transition, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2327, + label: "Company-Territory Electric Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Electric, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2328, + label: "Company-Territory Non-Electric Track Pieces", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::NonElectric, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, + label: "%1 Avail.", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID, + label: "%1 Production", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID, + label: "Unknown Loco Available", + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability, + ), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID, + label: "Unknown Loco Cost", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID, + label: "All Cargo Production", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID, + label: "All Factory Production", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID, + label: "All Farm/Mine Production", + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FarmMineProductionTotal, + ), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID, + label: "Unknown Cargo Production", + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::OtherCargoProductionTotal, + ), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID, + label: "Limited Track Building Amount", + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::LimitedTrackBuildingAmount, + ), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_ACCESS_COST_CONDITION_ID, + label: "Access Rights Cost:", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2350, + label: "Economic Status", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus), + }, +]; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/descriptors.rs b/crates/rrt-runtime/src/inspect/smp/catalog/descriptors.rs new file mode 100644 index 0000000..e9102e1 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/descriptors.rs @@ -0,0 +1,217 @@ +use crate::inspect::smp::catalog::*; +use serde::Deserialize; +use std::collections::BTreeMap; +use std::sync::OnceLock; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) struct RealGroupedEffectDescriptorMetadata { + pub(in crate::inspect::smp) descriptor_id: u32, + pub(in crate::inspect::smp) label: &'static str, + pub(in crate::inspect::smp) target_mask_bits: u8, + pub(in crate::inspect::smp) parameter_family: &'static str, + pub(in crate::inspect::smp) runtime_key: Option<&'static str>, + pub(in crate::inspect::smp) runtime_status: RealGroupedEffectRuntimeStatus, + pub(in crate::inspect::smp) executable_in_runtime: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) enum RealGroupedEffectRuntimeStatus { + Executable, + ShellOwned, + EvidenceBlocked, + VariantOrScopeBlocked, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub(in crate::inspect::smp) struct CheckedInEventEffectsSemanticCatalogArtifact { + pub(in crate::inspect::smp) descriptors: Vec, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub(in crate::inspect::smp) struct CheckedInEventEffectSemanticRow { + pub(in crate::inspect::smp) descriptor_id: u32, + pub(in crate::inspect::smp) label: String, + pub(in crate::inspect::smp) target_mask_bits: u8, + pub(in crate::inspect::smp) parameter_family: String, + pub(in crate::inspect::smp) runtime_key: Option, + pub(in crate::inspect::smp) runtime_status: String, + pub(in crate::inspect::smp) executable_in_runtime: bool, +} + +pub(in crate::inspect::smp) const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: + [RealGroupedEffectDescriptorMetadata; 12] = [ + RealGroupedEffectDescriptorMetadata { + descriptor_id: 1, + label: "Player Cash", + target_mask_bits: 0x02, + parameter_family: "player_finance_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 2, + label: "Company Cash", + target_mask_bits: 0x01, + parameter_family: "company_finance_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 3, + label: "Territory - Allow All", + target_mask_bits: 0x05, + parameter_family: "territory_access_toggle", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 8, + label: "Economic Status", + target_mask_bits: 0x08, + parameter_family: "whole_game_state_enum", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 108, + label: "Use Wartime Cargos", + target_mask_bits: 0x08, + parameter_family: "special_condition_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 109, + label: "Turbo Diesel Availability", + target_mask_bits: 0x08, + parameter_family: "candidate_availability_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 110, + label: "Disable Stock Buying and Selling", + target_mask_bits: 0x08, + parameter_family: "world_flag_toggle", + runtime_key: Some("world.disable_stock_buying_and_selling"), + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 9, + label: "Confiscate All", + target_mask_bits: 0x01, + parameter_family: "company_confiscation_variant", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 13, + label: "Deactivate Company", + target_mask_bits: 0x01, + parameter_family: "company_lifecycle_toggle", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 14, + label: "Deactivate Player", + target_mask_bits: 0x02, + parameter_family: "player_lifecycle_toggle", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 15, + label: "Retire Train", + target_mask_bits: 0x0d, + parameter_family: "company_or_territory_asset_toggle", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 16, + label: "Company Track Pieces Buildable", + target_mask_bits: 0x01, + parameter_family: "company_build_limit_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }, +]; + +pub(in crate::inspect::smp) fn real_grouped_effect_runtime_status_name( + status: RealGroupedEffectRuntimeStatus, +) -> &'static str { + match status { + RealGroupedEffectRuntimeStatus::Executable => "executable", + RealGroupedEffectRuntimeStatus::ShellOwned => "shell_owned", + RealGroupedEffectRuntimeStatus::EvidenceBlocked => "evidence_blocked", + RealGroupedEffectRuntimeStatus::VariantOrScopeBlocked => "variant_or_scope_blocked", + } +} + +pub(in crate::inspect::smp) fn checked_in_event_effect_descriptor_rows() +-> &'static BTreeMap { + static ROWS: OnceLock> = OnceLock::new(); + ROWS.get_or_init(|| { + let artifact: CheckedInEventEffectsSemanticCatalogArtifact = + serde_json::from_str(include_str!( + "../../../../../../artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json" + )) + .expect("checked-in event-effects semantic catalog should parse"); + artifact + .descriptors + .into_iter() + .map(|row| { + ( + row.descriptor_id, + checked_in_event_effect_descriptor_metadata(row), + ) + }) + .collect() + }) +} + +pub(in crate::inspect::smp) fn checked_in_event_effect_descriptor_metadata( + row: CheckedInEventEffectSemanticRow, +) -> RealGroupedEffectDescriptorMetadata { + let label = Box::leak(row.label.clone().into_boxed_str()) as &'static str; + let parameter_family = Box::leak(row.parameter_family.into_boxed_str()) as &'static str; + let runtime_key = row + .runtime_key + .map(|key| Box::leak(key.into_boxed_str()) as &'static str); + let runtime_status = match row.runtime_status.as_str() { + "executable" => RealGroupedEffectRuntimeStatus::Executable, + "shell_owned" => RealGroupedEffectRuntimeStatus::ShellOwned, + "evidence_blocked" => RealGroupedEffectRuntimeStatus::EvidenceBlocked, + "variant_or_scope_blocked" => RealGroupedEffectRuntimeStatus::VariantOrScopeBlocked, + other => panic!("unknown checked-in event-effect runtime status {other}"), + }; + RealGroupedEffectDescriptorMetadata { + descriptor_id: row.descriptor_id, + label, + target_mask_bits: row.target_mask_bits, + parameter_family, + runtime_key, + runtime_status, + executable_in_runtime: row.executable_in_runtime, + } +} + +pub(crate) fn grouped_effect_descriptor_runtime_status_name( + descriptor_id: u32, +) -> Option<&'static str> { + real_grouped_effect_descriptor_metadata(descriptor_id) + .map(|metadata| real_grouped_effect_runtime_status_name(metadata.runtime_status)) +} diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/fixups.rs b/crates/rrt-runtime/src/inspect/smp/catalog/fixups.rs new file mode 100644 index 0000000..169b8fb --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/fixups.rs @@ -0,0 +1,16 @@ +pub(in crate::inspect::smp) const POST_LOAD_SCENARIO_FIXUP_TITLE_SET: [&str; 14] = [ + "Go West!", + "Germany", + "France", + "State of Germany", + "New Beginnings", + "Dutchlantis", + "Britain", + "New Zealand", + "South East Australia", + "Tex-Mex", + "Germantown", + "The American", + "Central Pacific", + "Orient Express", +]; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/mod.rs b/crates/rrt-runtime/src/inspect/smp/catalog/mod.rs new file mode 100644 index 0000000..3309704 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/mod.rs @@ -0,0 +1,12 @@ +use super::events::real_grouped_effect_descriptor_metadata; +use crate::inspect::smp::*; + +pub(super) mod conditions; +pub(super) mod descriptors; +pub(super) mod fixups; +pub(super) mod offsets; +pub(super) mod special_conditions; +pub(super) mod tags; + +pub(crate) use descriptors::grouped_effect_descriptor_runtime_status_name; +pub use offsets::bundle::SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/offsets/bundle.rs b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/bundle.rs new file mode 100644 index 0000000..df795e8 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/bundle.rs @@ -0,0 +1,13 @@ +pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec; +pub(in crate::inspect::smp) const PREAMBLE_U32_WORD_COUNT: usize = 16; +pub(in crate::inspect::smp) const MIN_ASCII_RUN_LEN: usize = 8; +pub(in crate::inspect::smp) const ASCII_PREVIEW_CHAR_LIMIT: usize = 160; +pub(in crate::inspect::smp) const TAG_OFFSET_SAMPLE_LIMIT: usize = 8; +pub(in crate::inspect::smp) const EARLY_ZERO_RUN_THRESHOLD: usize = 16; +pub(in crate::inspect::smp) const EARLY_PREVIEW_BYTE_LIMIT: usize = 32; +pub(in crate::inspect::smp) const EARLY_ALIGNED_WORD_WINDOW_COUNT: usize = 8; +pub(in crate::inspect::smp) const MAP_TITLE_HINT_ASCII_FRAGMENT_MAX_LEN: usize = 160; +pub(in crate::inspect::smp) const MAP_TITLE_HINT_REFERENCE_PAIR_DISTANCE_LIMIT: usize = 0x100; +pub(in crate::inspect::smp) const SHARED_SIGNATURE_WORDS_1_TO_7: [u32; 7] = [ + 0x00002ee0, 0x00040001, 0x00028000, 0x00010000, 0x00000771, 0x00000771, 0x00000771, +]; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/offsets/events.rs b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/events.rs new file mode 100644 index 0000000..57e35cc --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/events.rs @@ -0,0 +1,26 @@ +pub(in crate::inspect::smp) const EVENT_RUNTIME_COLLECTION_METADATA_TAG: u16 = 0x4e99; +pub(in crate::inspect::smp) const EVENT_RUNTIME_COLLECTION_RECORDS_TAG: u16 = 0x4e9a; +pub(in crate::inspect::smp) const EVENT_RUNTIME_COLLECTION_CLOSE_TAG: u16 = 0x4e9b; +pub(in crate::inspect::smp) const EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION: u32 = 0x000003e9; +pub(in crate::inspect::smp) const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001"; +pub(in crate::inspect::smp) const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1"; +pub(in crate::inspect::smp) const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1"; +pub(in crate::inspect::smp) const PACKED_EVENT_REAL_CONDITION_MARKER: u16 = 0x526f; +pub(in crate::inspect::smp) const PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER: u16 = 0x4eb8; +pub(in crate::inspect::smp) const PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER: u16 = 0x4eb9; +pub(in crate::inspect::smp) const PACKED_EVENT_REAL_CONDITION_ROW_LEN: usize = 0x1e; +pub(in crate::inspect::smp) const PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN: usize = 0x28; +pub(in crate::inspect::smp) const PACKED_EVENT_REAL_GROUP_COUNT: usize = 4; +pub(in crate::inspect::smp) const PACKED_EVENT_REAL_COMPACT_CONTROL_LEN: usize = 37; +pub(in crate::inspect::smp) const PACKED_EVENT_NONDIRECT_CONDITION_ROW_SERIALIZED_LEN: usize = 22; +pub(in crate::inspect::smp) const PACKED_EVENT_NONDIRECT_GROUPED_EFFECT_ROW_SERIALIZED_LEN: usize = + 45; +pub(in crate::inspect::smp) const PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN: usize = 0x64; +pub(in crate::inspect::smp) const PACKED_EVENT_TEXT_BAND_LABELS: [&str; 6] = [ + "primary_text_band", + "secondary_text_band_0", + "secondary_text_band_1", + "secondary_text_band_2", + "secondary_text_band_3", + "secondary_text_band_4", +]; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/offsets/mod.rs b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/mod.rs new file mode 100644 index 0000000..02922a4 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/mod.rs @@ -0,0 +1,5 @@ +pub(in crate::inspect::smp) mod bundle; +pub(in crate::inspect::smp) mod events; +pub(in crate::inspect::smp) mod regions; +pub(in crate::inspect::smp) mod special_conditions; +pub(in crate::inspect::smp) mod world; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/offsets/regions.rs b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/regions.rs new file mode 100644 index 0000000..9ad7c20 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/regions.rs @@ -0,0 +1,15 @@ +pub(in crate::inspect::smp) const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19; +pub(in crate::inspect::smp) const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize = + INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4; +pub(in crate::inspect::smp) const SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX: usize = 16; +pub(in crate::inspect::smp) const SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT: usize = 3; +pub(in crate::inspect::smp) const SAVE_REGION_RECORD_NAME_TAG: u16 = 0x55f1; +pub(in crate::inspect::smp) const SAVE_REGION_RECORD_POLICY_TAG: u16 = 0x55f2; +pub(in crate::inspect::smp) const SAVE_REGION_RECORD_PROFILE_TAG: u16 = 0x55f3; +pub(in crate::inspect::smp) const SAVE_REGION_FIXED_ROW_STRIDE: usize = 0x29; +pub(in crate::inspect::smp) const SAVE_REGION_FIXED_ROW_DWORD_LANE_COUNT: usize = + SAVE_REGION_FIXED_ROW_STRIDE / 4; +pub(in crate::inspect::smp) const SAVE_REGION_FIXED_ROW_CANDIDATE_PROBE_LIMIT: usize = 24; +pub(in crate::inspect::smp) const SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED: u32 = 0x005c87a8; +pub(in crate::inspect::smp) const SAVE_REGION_QUEUED_NOTICE_NODE_KIND: u32 = 7; +pub(in crate::inspect::smp) const SAVE_REGION_QUEUED_NOTICE_NODE_LEN: usize = 0x20; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/offsets/special_conditions.rs b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/special_conditions.rs new file mode 100644 index 0000000..11e72b3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/special_conditions.rs @@ -0,0 +1,80 @@ +pub(in crate::inspect::smp) const SPECIAL_CONDITIONS_OFFSET: usize = 0x0d64; +pub(in crate::inspect::smp) const SPECIAL_CONDITION_COUNT: usize = 36; +pub(in crate::inspect::smp) const SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT: usize = 35; +pub(in crate::inspect::smp) const SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT: usize = 50; +pub(in crate::inspect::smp) const SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT: usize = 49; +pub(in crate::inspect::smp) const SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET: usize = 0x4a7f; +pub(in crate::inspect::smp) const SMP_ALIGNED_RUNTIME_RULE_END_OFFSET: usize = + SPECIAL_CONDITIONS_OFFSET + SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT * 4; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_OFFSET: usize = 0x0df4; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET: usize = 0x0f30; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET: + usize = 0x0f58; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET: usize = + SMP_ALIGNED_RUNTIME_RULE_END_OFFSET; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET: usize = + SMP_ALIGNED_RUNTIME_RULE_END_OFFSET; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET: usize = + SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET + + (POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET - SPECIAL_CONDITIONS_OFFSET); +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET: + usize = SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET + + (POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - SPECIAL_CONDITIONS_OFFSET); +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET: + usize = 0x4b47; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN: usize = 0x12c; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET: usize = + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET + + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN; +pub(in crate::inspect::smp) const POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET: usize = 0x0f59; +pub(in crate::inspect::smp) const POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET: usize = 0x0f75; +pub(in crate::inspect::smp) const POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET: usize = 0x4c74; +pub(in crate::inspect::smp) const POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET: usize = 0x4c78; +pub(in crate::inspect::smp) const POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET: usize = 0x4c7c; +pub(in crate::inspect::smp) const POST_TEXT_FIELD_3_RUNTIME_OBJECT_OFFSET: usize = 0x4c80; +pub(in crate::inspect::smp) const POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET: usize = 0x4c88; +pub(in crate::inspect::smp) const POST_TEXT_FIELD_5_RUNTIME_OBJECT_OFFSET: usize = 0x4c8c; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET: + usize = 0x4c80; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET: + usize = 0x4c8c; +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET: usize = + SPECIAL_CONDITIONS_OFFSET + + (POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET + - SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET); +pub(in crate::inspect::smp) const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET: usize = + SPECIAL_CONDITIONS_OFFSET + + (POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET + - SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET); +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET: usize = 0x0f78; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET: usize = 0x0fa7; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_NEG3_RUNTIME_OBJECT_OFFSET: usize = + 0x4ca2; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_NEG2_RUNTIME_OBJECT_OFFSET: usize = + 0x4cae; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_NEG1_RUNTIME_OBJECT_OFFSET: usize = + 0x4cb2; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_0_RUNTIME_OBJECT_OFFSET: usize = 0x4c93; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_1_RUNTIME_OBJECT_OFFSET: usize = 0x4c97; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_2_RUNTIME_OBJECT_OFFSET: usize = 0x4c98; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_3_RUNTIME_OBJECT_OFFSET: usize = 0x4c99; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_4_RUNTIME_OBJECT_OFFSET: usize = 0x4cba; +pub(in crate::inspect::smp) const LOCOMOTIVE_POLICY_FIELD_5_RUNTIME_OBJECT_OFFSET: usize = 0x4cbe; +pub(in crate::inspect::smp) const PRE_RECIPE_SCALAR_PLATEAU_OFFSET: usize = 0x0fa7; +pub(in crate::inspect::smp) const PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET: usize = 0x0fe7; +pub(in crate::inspect::smp) const RECIPE_BOOK_ROOT_OFFSET: usize = 0x0fe7; +pub(in crate::inspect::smp) const RECIPE_BOOK_COUNT: usize = 12; +pub(in crate::inspect::smp) const RECIPE_BOOK_STRIDE: usize = 0x4e1; +pub(in crate::inspect::smp) const RECIPE_BOOK_HEAD_SAMPLE_LEN: usize = 16; +pub(in crate::inspect::smp) const RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET: usize = 0x3ed; +pub(in crate::inspect::smp) const RECIPE_BOOK_LINE_AREA_OFFSET: usize = 0x3f1; +pub(in crate::inspect::smp) const RECIPE_BOOK_LINE_COUNT: usize = 5; +pub(in crate::inspect::smp) const RECIPE_BOOK_LINE_STRIDE: usize = 0x30; +pub(in crate::inspect::smp) const RECIPE_BOOK_LINE_AREA_LEN: usize = + RECIPE_BOOK_LINE_COUNT * RECIPE_BOOK_LINE_STRIDE; +pub(in crate::inspect::smp) const RECIPE_BOOK_SUMMARY_END_OFFSET: usize = + RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_COUNT * RECIPE_BOOK_STRIDE; +pub(in crate::inspect::smp) const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX: usize = + (POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - SPECIAL_CONDITIONS_OFFSET) / 4; +pub(in crate::inspect::smp) const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT: usize = + (SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET) / 4; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/offsets/world.rs b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/world.rs new file mode 100644 index 0000000..d6546fc --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/offsets/world.rs @@ -0,0 +1,72 @@ +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_CHUNK_TAG: u32 = 0x000032c8; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG: u32 = 0x000032c9; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_LEN: usize = 0x4f2c; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET: usize = + 0x1d; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET: usize = 0x21; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET: + usize = 0x25; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET: usize = + 0x29; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET: usize = 0x0d; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET: usize = 0x11; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET: usize = + 0x15; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET: + usize = 0x19; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_CANDIDATE_FIELDS: [( + &str, + usize, +); + 11] = [ + ( + "current_calendar_tuple_word", + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, + ), + ( + "current_calendar_tuple_word_2", + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET, + ), + ( + "absolute_calendar_counter", + RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET, + ), + ( + "absolute_calendar_counter_mirror", + RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET, + ), + ("selection_context_candidate_0", 0x1d), + ("selection_context_candidate_1", 0x21), + ("issue_0x37_multiplier", 0x25), + ("issue_0x37_value", 0x29), + ("issue_neighbor_candidate_0", 0x2d), + ("issue_neighbor_candidate_1", 0x31), + ("issue_neighbor_candidate_2", 0x35), +]; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET: + usize = 0x0d; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS: + usize = 17; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_BASE_TERMS_OFFSET: usize = + 0x8a; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT: usize = 0x3b; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET: + usize = 0x83; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET: + usize = 0xc1; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET: usize = + 0x0bbf; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET: usize = 0x4a83; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET: usize = 0x4a87; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET: usize = + 0x4a8b; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET: usize = + 0x4a8f; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET: + usize = 0x4c78; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET: + usize = 0x0bda; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS: + [usize; 6] = [0x0bde, 0x0be2, 0x0be6, 0x0bea, 0x0bee, 0x0bf2]; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT: usize = 16; +pub(in crate::inspect::smp) const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE: usize = 9; diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/special_conditions.rs b/crates/rrt-runtime/src/inspect/smp/catalog/special_conditions.rs new file mode 100644 index 0000000..9b34036 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/special_conditions.rs @@ -0,0 +1,275 @@ +use crate::inspect::smp::catalog::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) struct KnownSpecialConditionDefinition { + pub(in crate::inspect::smp) slot_index: u8, + pub(in crate::inspect::smp) hidden: bool, + pub(in crate::inspect::smp) label_id: u32, + pub(in crate::inspect::smp) help_id: u32, + pub(in crate::inspect::smp) label: &'static str, +} + +pub(in crate::inspect::smp) const KNOWN_SPECIAL_CONDITION_DEFINITIONS: + [KnownSpecialConditionDefinition; SPECIAL_CONDITION_COUNT] = [ + KnownSpecialConditionDefinition { + slot_index: 0, + hidden: false, + label_id: 2535, + help_id: 2564, + label: "Disable Stock Buying and Selling", + }, + KnownSpecialConditionDefinition { + slot_index: 1, + hidden: false, + label_id: 2536, + help_id: 2565, + label: "Disable Margin Buying/Short Selling Stock", + }, + KnownSpecialConditionDefinition { + slot_index: 2, + hidden: false, + label_id: 2537, + help_id: 2566, + label: "Disable Company Issue/Buy Back Stock", + }, + KnownSpecialConditionDefinition { + slot_index: 3, + hidden: false, + label_id: 2538, + help_id: 2567, + label: "Disable Issuing/Repaying Bonds", + }, + KnownSpecialConditionDefinition { + slot_index: 4, + hidden: false, + label_id: 2539, + help_id: 2568, + label: "Disable Declaring Bankruptcy", + }, + KnownSpecialConditionDefinition { + slot_index: 5, + hidden: false, + label_id: 2540, + help_id: 2569, + label: "Disable Changing the Dividend Rate", + }, + KnownSpecialConditionDefinition { + slot_index: 6, + hidden: false, + label_id: 2541, + help_id: 2570, + label: "Disable Replacing a Locomotive", + }, + KnownSpecialConditionDefinition { + slot_index: 7, + hidden: false, + label_id: 2542, + help_id: 2571, + label: "Disable Retiring a Train", + }, + KnownSpecialConditionDefinition { + slot_index: 8, + hidden: false, + label_id: 2543, + help_id: 2572, + label: "Disable Changing Cargo Consist On Train", + }, + KnownSpecialConditionDefinition { + slot_index: 9, + hidden: false, + label_id: 2544, + help_id: 2573, + label: "Disable Buying a Train", + }, + KnownSpecialConditionDefinition { + slot_index: 10, + hidden: false, + label_id: 2545, + help_id: 2574, + label: "Disable All Track Building", + }, + KnownSpecialConditionDefinition { + slot_index: 11, + hidden: false, + label_id: 2546, + help_id: 2575, + label: "Disable Unconnected Track Building", + }, + KnownSpecialConditionDefinition { + slot_index: 12, + hidden: false, + label_id: 2547, + help_id: 2576, + label: "Limited Track Building Amount", + }, + KnownSpecialConditionDefinition { + slot_index: 13, + hidden: false, + label_id: 2548, + help_id: 2577, + label: "Disable Building Stations", + }, + KnownSpecialConditionDefinition { + slot_index: 14, + hidden: false, + label_id: 2549, + help_id: 2578, + label: "Disable Building Hotel/Restaurant/Tavern/Post Office", + }, + KnownSpecialConditionDefinition { + slot_index: 15, + hidden: false, + label_id: 2550, + help_id: 2579, + label: "Disable Building Customs House", + }, + KnownSpecialConditionDefinition { + slot_index: 16, + hidden: false, + label_id: 2551, + help_id: 2580, + label: "Disable Building Industry Buildings", + }, + KnownSpecialConditionDefinition { + slot_index: 17, + hidden: false, + label_id: 2552, + help_id: 2581, + label: "Disable Buying Existing Industry Buildings", + }, + KnownSpecialConditionDefinition { + slot_index: 18, + hidden: false, + label_id: 2553, + help_id: 2582, + label: "Disable Being Fired As Chairman", + }, + KnownSpecialConditionDefinition { + slot_index: 19, + hidden: false, + label_id: 2554, + help_id: 2583, + label: "Disable Resigning as Chairman", + }, + KnownSpecialConditionDefinition { + slot_index: 20, + hidden: false, + label_id: 2555, + help_id: 2584, + label: "Disable Chairmanship Takeover", + }, + KnownSpecialConditionDefinition { + slot_index: 21, + hidden: false, + label_id: 2556, + help_id: 2585, + label: "Disable Starting Any Companies", + }, + KnownSpecialConditionDefinition { + slot_index: 22, + hidden: false, + label_id: 2557, + help_id: 2586, + label: "Disable Starting Multiple Companies", + }, + KnownSpecialConditionDefinition { + slot_index: 23, + hidden: false, + label_id: 2558, + help_id: 2587, + label: "Disable Merging Companies", + }, + KnownSpecialConditionDefinition { + slot_index: 24, + hidden: false, + label_id: 2559, + help_id: 2588, + label: "Disable Bulldozing", + }, + KnownSpecialConditionDefinition { + slot_index: 25, + hidden: false, + label_id: 2560, + help_id: 2589, + label: "Show Visited Track", + }, + KnownSpecialConditionDefinition { + slot_index: 26, + hidden: false, + label_id: 2561, + help_id: 2590, + label: "Show Visited Stations", + }, + KnownSpecialConditionDefinition { + slot_index: 27, + hidden: false, + label_id: 2562, + help_id: 2591, + label: "Use Slow Date", + }, + KnownSpecialConditionDefinition { + slot_index: 28, + hidden: false, + label_id: 2563, + help_id: 2592, + label: "Completely Disable Money-Related Things", + }, + KnownSpecialConditionDefinition { + slot_index: 29, + hidden: false, + label_id: 2874, + help_id: 2875, + label: "Use Bio-Accelerator Cars", + }, + KnownSpecialConditionDefinition { + slot_index: 30, + hidden: false, + label_id: 3722, + help_id: 3723, + label: "Disable Cargo Economy", + }, + KnownSpecialConditionDefinition { + slot_index: 31, + hidden: false, + label_id: 3835, + help_id: 3836, + label: "Use Wartime Cargos", + }, + KnownSpecialConditionDefinition { + slot_index: 32, + hidden: false, + label_id: 3850, + help_id: 3851, + label: "Disable Train Crashes", + }, + KnownSpecialConditionDefinition { + slot_index: 33, + hidden: false, + label_id: 3852, + help_id: 3853, + label: "Disable Train Crashes AND Breakdowns", + }, + KnownSpecialConditionDefinition { + slot_index: 34, + hidden: false, + label_id: 3920, + help_id: 3921, + label: "AI Ignore Territories At Startup", + }, + KnownSpecialConditionDefinition { + slot_index: 35, + hidden: true, + label_id: 3, + help_id: 3, + label: "Hidden sentinel", + }, +]; + +pub(in crate::inspect::smp) fn known_special_condition_definition_for_label_id( + label_id: u32, +) -> Option { + KNOWN_SPECIAL_CONDITION_DEFINITIONS + .iter() + .copied() + .find(|definition| !definition.hidden && definition.label_id == label_id) +} diff --git a/crates/rrt-runtime/src/inspect/smp/catalog/tags.rs b/crates/rrt-runtime/src/inspect/smp/catalog/tags.rs new file mode 100644 index 0000000..9a3d6fe --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/catalog/tags.rs @@ -0,0 +1,29 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::inspect::smp) struct KnownTagDefinition { + pub(in crate::inspect::smp) tag_id: u16, + pub(in crate::inspect::smp) label: &'static str, + pub(in crate::inspect::smp) grounded_meaning: &'static str, +} + +pub(in crate::inspect::smp) const KNOWN_TAG_DEFINITIONS: [KnownTagDefinition; 4] = [ + KnownTagDefinition { + tag_id: 0x2cee, + label: "overlay_mask_plane_primary", + grounded_meaning: "Primary one-byte overlay mask plane restored into world offset +0x1655.", + }, + KnownTagDefinition { + tag_id: 0x2d51, + label: "overlay_mask_plane_secondary", + grounded_meaning: "Secondary one-byte overlay mask plane restored into world offset +0x1659.", + }, + KnownTagDefinition { + tag_id: 0x9471, + label: "sidecar_byte_plane_family_low", + grounded_meaning: "Lower bound of the grounded sidecar byte-plane chunk family.", + }, + KnownTagDefinition { + tag_id: 0x9472, + label: "sidecar_byte_plane_family_high", + grounded_meaning: "Upper bound of the grounded sidecar byte-plane chunk family.", + }, +]; diff --git a/crates/rrt-runtime/src/inspect/smp/common.rs b/crates/rrt-runtime/src/inspect/smp/common.rs new file mode 100644 index 0000000..3964e57 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common.rs @@ -0,0 +1,46 @@ +use super::bundle::SmpContainerProfile; +use super::map_title::{build_ascii_preview, is_ascii_preview_byte}; +use super::structures::IndexedCollectionHeaderSummary; +use crate::inspect::smp::catalog::offsets::bundle::MIN_ASCII_RUN_LEN; +use crate::inspect::smp::catalog::offsets::events::EVENT_RUNTIME_COLLECTION_METADATA_TAG; +use crate::inspect::smp::catalog::offsets::regions::{ + INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT, INDEXED_COLLECTION_SERIALIZED_HEADER_LEN, +}; +use crate::inspect::smp::catalog::offsets::world::RT3_SAVE_WORLD_BLOCK_CHUNK_TAG; + +mod ascii; +mod headers; +mod model; +mod names; +mod read; +mod recipe; +mod search; +mod signatures; + +pub(super) use ascii::{ascii_preview, find_first_ascii_run, hex_encode, sha256_hex}; +pub(super) use headers::{ + filter_unclassified_tagged_collection_header_probes_outside_known_spans, + parse_save_tagged_collection_header_probe, + scan_save_unclassified_tagged_collection_header_probes, +}; +pub use model::*; +pub(super) use names::{ + parse_save_len_prefixed_ascii_name, parse_save_len_prefixed_ascii_name_pair, + parse_save_len_prefixed_ascii_name_triplet, + parse_save_len_prefixed_ascii_name_triplet_and_consumed_len, + parse_save_region_profile_collection_probe, +}; +pub(super) use read::{ + parse_nonzero_u32, read_ascii_c_string_at, read_f32_at, read_f64_at, read_i32_at, read_i64_at, + read_u8_at, read_u16_at, read_u16_window, read_u32_at, read_u32_window, read_u64_at, + round_f64_to_i64, +}; +pub(super) use recipe::{ + classify_recipe_book_region_kind, classify_recipe_line_signature, + classify_recipe_runtime_import_branch, classify_recipe_token_layout, + probable_normal_f32_string, probable_recipe_token_high16_ascii_stem, +}; +pub(super) use search::{ + find_next_nonzero_offset, find_u16_le_offsets, find_u32_le_offsets, find_zero_run, +}; +pub(super) use signatures::{compact_nondirect_signature_family, format_u16_word_signature}; diff --git a/crates/rrt-runtime/src/inspect/smp/common/ascii.rs b/crates/rrt-runtime/src/inspect/smp/common/ascii.rs new file mode 100644 index 0000000..ae17bf9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common/ascii.rs @@ -0,0 +1,46 @@ +use crate::inspect::smp::common::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn find_first_ascii_run(bytes: &[u8]) -> Option { + let mut start = None; + + for (index, byte) in bytes.iter().copied().enumerate() { + if is_ascii_preview_byte(byte) { + start.get_or_insert(index); + continue; + } + + if let Some(run_start) = start.take() { + if index - run_start >= MIN_ASCII_RUN_LEN { + return Some(build_ascii_preview(bytes, run_start, index)); + } + } + } + + start.and_then(|run_start| { + if bytes.len() - run_start >= MIN_ASCII_RUN_LEN { + Some(build_ascii_preview(bytes, run_start, bytes.len())) + } else { + None + } + }) +} + +pub(in crate::inspect::smp) fn hex_encode(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +pub(in crate::inspect::smp) fn ascii_preview(bytes: &[u8]) -> String { + bytes + .iter() + .map(|byte| match byte { + 0x20..=0x7e => char::from(*byte), + _ => '.', + }) + .collect() +} + +pub(in crate::inspect::smp) fn sha256_hex(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + format!("{digest:x}") +} diff --git a/crates/rrt-runtime/src/inspect/smp/common/headers.rs b/crates/rrt-runtime/src/inspect/smp/common/headers.rs new file mode 100644 index 0000000..3119246 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common/headers.rs @@ -0,0 +1,273 @@ +use crate::inspect::smp::common::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn parse_save_tagged_collection_header_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + metadata_tag: u32, + records_tag: u32, + close_tag: u32, + source_kind: &str, + semantic_family: &str, + predicate: impl Fn(IndexedCollectionHeaderSummary) -> bool, + mut evidence: Vec, +) -> Option { + if file_extension_hint != Some("gms") { + return None; + } + let profile = container_profile?; + if !matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ) { + return None; + } + + let metadata_offsets = find_u32_le_offsets(bytes, metadata_tag); + let records_offsets = find_u32_le_offsets(bytes, records_tag); + let close_offsets = find_u32_le_offsets(bytes, close_tag); + + let summary = metadata_offsets + .into_iter() + .filter_map(|metadata_tag_offset| { + let records_tag_offset = records_offsets + .iter() + .copied() + .find(|offset| *offset > metadata_tag_offset)?; + let close_tag_offset = close_offsets + .iter() + .copied() + .find(|offset| *offset > records_tag_offset)?; + let payload = bytes.get(metadata_tag_offset + 4..records_tag_offset)?; + if payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { + return None; + } + + let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT) + .map(|index| read_u32_at(payload, index * 4)) + .collect::>>()?; + let header_words: [u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT] = + header_words.try_into().ok()?; + let summary = IndexedCollectionHeaderSummary { + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + direct_collection_flag: header_words[0], + direct_record_stride: header_words[1], + live_id_bound: header_words[4], + live_record_count: header_words[5], + header_words, + }; + predicate(summary).then_some(summary) + }) + .next()?; + + evidence.push(format!( + "exact little-endian u32 tag family 0x{metadata_tag:04x}/0x{records_tag:04x}/0x{close_tag:04x} appears at file offsets 0x{:x}/0x{:x}/0x{:x}", + summary.metadata_tag_offset, summary.records_tag_offset, summary.close_tag_offset + )); + evidence.push(format!( + "header words report direct_collection_flag={}, direct_record_stride=0x{:x}, live_id_bound={}, live_record_count={}", + summary.direct_collection_flag, + summary.direct_record_stride, + summary.live_id_bound, + summary.live_record_count + )); + + Some(SmpSaveTaggedCollectionHeaderProbe { + profile_family: profile.profile_family.clone(), + source_kind: source_kind.to_string(), + semantic_family: semantic_family.to_string(), + metadata_tag_offset: summary.metadata_tag_offset, + records_tag_offset: summary.records_tag_offset, + close_tag_offset: summary.close_tag_offset, + direct_collection_flag: summary.direct_collection_flag, + direct_collection_flag_hex: format!("0x{:08x}", summary.direct_collection_flag), + direct_record_stride: summary.direct_record_stride, + direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride), + live_id_bound: summary.live_id_bound, + live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound), + live_record_count: summary.live_record_count, + live_record_count_hex: format!("0x{:08x}", summary.live_record_count), + header_words: summary.header_words.to_vec(), + header_hex_words: summary + .header_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + evidence, + }) +} + +pub(in crate::inspect::smp) fn scan_save_unclassified_tagged_collection_header_probes( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Vec { + if file_extension_hint != Some("gms") { + return Vec::new(); + } + let Some(profile) = container_profile else { + return Vec::new(); + }; + if !matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ) { + return Vec::new(); + } + let known_metadata_tags = BTreeSet::from([ + RT3_SAVE_WORLD_BLOCK_CHUNK_TAG, + 0x000061a9, + 0x00005209, + 0x000036b1, + EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32, + ]); + let mut low_tag_offsets: BTreeMap> = BTreeMap::new(); + for offset in 0..bytes.len().saturating_sub(4) { + let Some(tag) = read_u32_at(bytes, offset) else { + continue; + }; + if (3..=0xffff).contains(&tag) { + low_tag_offsets.entry(tag).or_default().push(offset); + } + } + let mut probes = Vec::new(); + for (&metadata_tag, metadata_offsets) in &low_tag_offsets { + if known_metadata_tags.contains(&metadata_tag) { + continue; + } + let Some(records_offsets) = low_tag_offsets.get(&(metadata_tag + 1)) else { + continue; + }; + let Some(close_offsets) = low_tag_offsets.get(&(metadata_tag + 2)) else { + continue; + }; + let records_tag = metadata_tag + 1; + let close_tag = metadata_tag + 2; + for &metadata_tag_offset in metadata_offsets { + let mut header_words = [0u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT]; + let mut valid_header = true; + for (index, word) in header_words.iter_mut().enumerate() { + let Some(value) = read_u32_at(bytes, metadata_tag_offset + 4 + index * 4) else { + valid_header = false; + break; + }; + *word = value; + } + if !valid_header { + continue; + } + let summary = IndexedCollectionHeaderSummary { + metadata_tag_offset, + records_tag_offset: 0, + close_tag_offset: 0, + direct_collection_flag: header_words[0], + direct_record_stride: header_words[1], + live_id_bound: header_words[4], + live_record_count: header_words[5], + header_words, + }; + if !matches!(summary.direct_collection_flag, 0 | 1) + || summary.direct_record_stride == 0 + || summary.direct_record_stride > 0x2000 + || summary.live_id_bound == 0 + || summary.live_record_count == 0 + || summary.live_record_count > summary.live_id_bound + || summary.live_id_bound > 0x1000 + || summary.live_record_count > 0x1000 + { + continue; + } + let records_search_start = metadata_tag_offset + 4; + let records_index = + records_offsets.partition_point(|offset| *offset < records_search_start); + let Some(&records_tag_offset) = records_offsets.get(records_index) else { + continue; + }; + let close_search_start = records_tag_offset + 4; + let close_index = close_offsets.partition_point(|offset| *offset < close_search_start); + let Some(&close_tag_offset) = close_offsets.get(close_index) else { + continue; + }; + let records_span_len = close_tag_offset.saturating_sub(records_tag_offset + 4); + if records_span_len == 0 || records_span_len < summary.live_record_count as usize { + continue; + } + if probes + .iter() + .any(|probe: &SmpSaveUnclassifiedTaggedCollectionHeaderProbe| { + probe.metadata_tag_offset == metadata_tag_offset + && probe.records_tag_offset == records_tag_offset + && probe.close_tag_offset == close_tag_offset + }) + { + continue; + } + probes.push(SmpSaveUnclassifiedTaggedCollectionHeaderProbe { + profile_family: profile.profile_family.clone(), + source_kind: "save-unclassified-tagged-header-counts".to_string(), + semantic_family: "scenario-save-unclassified-tagged-header-counts".to_string(), + metadata_tag, + metadata_tag_hex: format!("0x{metadata_tag:08x}"), + records_tag, + records_tag_hex: format!("0x{records_tag:08x}"), + close_tag, + close_tag_hex: format!("0x{close_tag:08x}"), + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + records_span_len, + direct_collection_flag: summary.direct_collection_flag, + direct_collection_flag_hex: format!("0x{:08x}", summary.direct_collection_flag), + direct_record_stride: summary.direct_record_stride, + direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride), + live_id_bound: summary.live_id_bound, + live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound), + live_record_count: summary.live_record_count, + live_record_count_hex: format!("0x{:08x}", summary.live_record_count), + header_words: summary.header_words.to_vec(), + header_hex_words: summary + .header_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + evidence: vec![ + "generic save-side tagged collection scan over plausible low u32 metadata tags not yet claimed by the checked-in collection probes".to_string(), + "candidate uses adjacent metadata/records/close tags with a header that matches the grounded indexed-collection shape (flag, stride, live_id_bound, live_record_count)".to_string(), + ], + }); + } + } + probes.sort_by(|left, right| { + right + .live_record_count + .cmp(&left.live_record_count) + .then_with(|| left.metadata_tag.cmp(&right.metadata_tag)) + .then_with(|| left.metadata_tag_offset.cmp(&right.metadata_tag_offset)) + }); + probes.truncate(32); + probes +} + +pub(in crate::inspect::smp) fn filter_unclassified_tagged_collection_header_probes_outside_known_spans( + probes: Vec, + known_header_probes: &[Option<&SmpSaveTaggedCollectionHeaderProbe>], +) -> Vec { + probes + .into_iter() + .filter(|probe| { + !known_header_probes.iter().flatten().any(|known| { + probe.metadata_tag_offset >= known.metadata_tag_offset + && probe.close_tag_offset <= known.close_tag_offset + }) + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/common/model.rs b/crates/rrt-runtime/src/inspect/smp/common/model.rs new file mode 100644 index 0000000..de95fd5 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common/model.rs @@ -0,0 +1,121 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpAsciiPreview { + pub offset: usize, + pub byte_len: usize, + pub preview: String, + pub truncated: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveTaggedCollectionHeaderProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub metadata_tag_offset: usize, + pub records_tag_offset: usize, + pub close_tag_offset: usize, + pub direct_collection_flag: u32, + pub direct_collection_flag_hex: String, + pub direct_record_stride: u32, + pub direct_record_stride_hex: String, + pub live_id_bound: u32, + pub live_id_bound_hex: String, + pub live_record_count: u32, + pub live_record_count_hex: String, + pub header_words: Vec, + pub header_hex_words: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveUnclassifiedTaggedCollectionHeaderProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub metadata_tag: u32, + pub metadata_tag_hex: String, + pub records_tag: u32, + pub records_tag_hex: String, + pub close_tag: u32, + pub close_tag_hex: String, + pub metadata_tag_offset: usize, + pub records_tag_offset: usize, + pub close_tag_offset: usize, + pub records_span_len: usize, + pub direct_collection_flag: u32, + pub direct_collection_flag_hex: String, + pub direct_record_stride: u32, + pub direct_record_stride_hex: String, + pub live_id_bound: u32, + pub live_id_bound_hex: String, + pub live_record_count: u32, + pub live_record_count_hex: String, + pub header_words: Vec, + pub header_hex_words: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveTrainCollectionDirectoryEntryProbe { + pub live_entry_id: u32, + pub payload_relative_offset: u32, + pub payload_relative_offset_hex: String, + pub payload_absolute_offset: usize, + pub previous_live_entry_id: u32, + pub previous_live_entry_id_hex: String, + pub next_live_entry_id: u32, + pub next_live_entry_id_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveTrainCollectionDirectoryProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub metadata_tag_offset: usize, + pub records_tag_offset: usize, + pub close_tag_offset: usize, + pub directory_root_dword_index: usize, + pub directory_entry_dword_count: usize, + pub live_record_count: u32, + pub live_id_bound: u32, + #[serde(default)] + pub chain_head_live_entry_id: Option, + #[serde(default)] + pub chain_tail_live_entry_id: Option, + pub entries: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveRegionProfileEntryProbe { + pub entry_index: usize, + pub row_relative_offset: usize, + pub name: String, + pub trailing_weight_f32: f32, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveRegionProfileCollectionProbe { + pub direct_collection_flag: u32, + pub entry_stride: u32, + pub live_id_bound: u32, + pub live_record_count: u32, + pub entry_start_relative_offset: usize, + pub trailing_padding_len: usize, + #[serde(default)] + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveDwordCandidate { + pub label: String, + pub relative_offset: usize, + pub relative_offset_hex: String, + pub raw_u32: u32, + pub raw_u32_hex: String, + pub value_i32: i32, + pub value_f32: f32, +} diff --git a/crates/rrt-runtime/src/inspect/smp/common/names.rs b/crates/rrt-runtime/src/inspect/smp/common/names.rs new file mode 100644 index 0000000..3c4ccfa --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common/names.rs @@ -0,0 +1,159 @@ +use crate::inspect::smp::common::*; + +pub(in crate::inspect::smp) fn parse_save_len_prefixed_ascii_name(bytes: &[u8]) -> Option { + let len = *bytes.first()? as usize; + let text_bytes = bytes.get(1..1 + len)?; + let text = std::str::from_utf8(text_bytes).ok()?.trim_end_matches('\0'); + Some(text.to_string()) +} + +pub(in crate::inspect::smp) fn parse_save_varlen_ascii_name_at( + bytes: &[u8], + offset: usize, +) -> Option<(String, usize)> { + let first = *bytes.get(offset)?; + if first == 0 { + return None; + } + let (len, header_len) = if first <= 0x7f { + (first as usize, 1usize) + } else { + let second = *bytes.get(offset + 1)? as usize; + ((((first as usize) & 0x7f) << 8) | second, 2usize) + }; + let start = offset + header_len; + let end = start.checked_add(len)?; + let text = std::str::from_utf8(bytes.get(start..end)?) + .ok()? + .trim_end_matches('\0') + .to_string(); + Some((text, end)) +} + +pub(in crate::inspect::smp) fn parse_save_len_prefixed_ascii_name_pair( + bytes: &[u8], +) -> Option<(String, String)> { + let (first, second, _) = parse_save_len_prefixed_ascii_name_triplet(bytes)?; + Some((first, second)) +} + +pub(in crate::inspect::smp) fn parse_save_len_prefixed_ascii_name_triplet_and_consumed_len( + bytes: &[u8], +) -> Option<((String, String, Option), usize)> { + let (first, first_end) = parse_save_varlen_ascii_name_at(bytes, 0)?; + let mut second_len_offset = first_end; + while matches!(bytes.get(second_len_offset), Some(0)) { + second_len_offset += 1; + } + let (second, second_end) = parse_save_varlen_ascii_name_at(bytes, second_len_offset)?; + if first.is_empty() || second.is_empty() { + return None; + } + let mut third_len_offset = second_end; + while matches!(bytes.get(third_len_offset), Some(0)) { + third_len_offset += 1; + } + let (third, consumed_len) = parse_save_varlen_ascii_name_at(bytes, third_len_offset) + .map(|(text, end)| { + let third = (!text.is_empty()).then_some(text); + (third, end) + }) + .unwrap_or((None, second_end)); + Some(((first, second, third), consumed_len)) +} + +pub(in crate::inspect::smp) fn parse_save_len_prefixed_ascii_name_triplet( + bytes: &[u8], +) -> Option<(String, String, Option)> { + let ((first, second, third), _) = + parse_save_len_prefixed_ascii_name_triplet_and_consumed_len(bytes)?; + Some((first, second, third)) +} + +pub(in crate::inspect::smp) fn parse_save_fixed_ascii_name(bytes: &[u8]) -> Option { + let nul_index = bytes + .iter() + .position(|byte| *byte == 0) + .unwrap_or(bytes.len()); + let text = std::str::from_utf8(bytes.get(..nul_index)?).ok()?; + if text.is_empty() + || !text + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b' ' | b'-' | b'&' | b'/')) + { + return None; + } + Some(text.to_string()) +} + +pub(in crate::inspect::smp) fn parse_save_region_profile_collection_probe( + profile_payload: &[u8], +) -> Option { + let direct_collection_flag = read_u32_at(profile_payload, 0)?; + let entry_stride = read_u32_at(profile_payload, 4)?; + let header_word_2 = read_u32_at(profile_payload, 8)?; + let header_word_3 = read_u32_at(profile_payload, 12)?; + let live_id_bound = read_u32_at(profile_payload, 16)?; + let live_record_count = read_u32_at(profile_payload, 20)?; + let header_word_6 = read_u32_at(profile_payload, 24)?; + let header_word_7 = read_u32_at(profile_payload, 28)?; + if !(direct_collection_flag == 1 + && entry_stride == 0x22 + && header_word_2 == 2 + && header_word_3 == 2 + && live_record_count > 0 + && live_record_count < live_id_bound + && header_word_6 == 0 + && header_word_7 == 1) + { + return None; + } + let entry_stride = entry_stride as usize; + let live_record_count_usize = live_record_count as usize; + let rows_byte_len = live_record_count_usize.checked_mul(entry_stride)?; + let mut matched_probe = None; + for entry_start_relative_offset in 0x20..=0x80 { + if entry_start_relative_offset + rows_byte_len > profile_payload.len() { + break; + } + let mut entries = Vec::with_capacity(live_record_count_usize); + let mut matched = true; + for entry_index in 0..live_record_count_usize { + let row_relative_offset = entry_start_relative_offset + entry_index * entry_stride; + let row = + profile_payload.get(row_relative_offset..row_relative_offset + entry_stride)?; + let name = match parse_save_fixed_ascii_name(row.get(..12)?) { + Some(name) => name, + None => { + matched = false; + break; + } + }; + let trailing_weight_f32 = f32::from_bits(read_u32_at(row, entry_stride - 4)?); + if !trailing_weight_f32.is_finite() || trailing_weight_f32 < 0.0 { + matched = false; + break; + } + entries.push(SmpSaveRegionProfileEntryProbe { + entry_index, + row_relative_offset, + name, + trailing_weight_f32, + }); + } + if matched { + matched_probe = Some(SmpSaveRegionProfileCollectionProbe { + direct_collection_flag, + entry_stride: entry_stride as u32, + live_id_bound, + live_record_count, + entry_start_relative_offset, + trailing_padding_len: profile_payload.len() + - (entry_start_relative_offset + rows_byte_len), + entries, + }); + break; + } + } + matched_probe +} diff --git a/crates/rrt-runtime/src/inspect/smp/common/read.rs b/crates/rrt-runtime/src/inspect/smp/common/read.rs new file mode 100644 index 0000000..ed0be96 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common/read.rs @@ -0,0 +1,102 @@ +pub(in crate::inspect::smp) fn read_u32_window( + bytes: &[u8], + offset: usize, + count: usize, +) -> Vec { + let mut words = Vec::new(); + let end = bytes.len().min(offset + count * 4); + for chunk in bytes[offset..end].chunks_exact(4) { + words.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); + } + words +} + +pub(in crate::inspect::smp) fn read_u16_window( + bytes: &[u8], + offset: usize, + count: usize, +) -> Vec { + let mut words = Vec::new(); + let end = bytes.len().min(offset + count * 2); + for chunk in bytes[offset..end].chunks_exact(2) { + words.push(u16::from_le_bytes([chunk[0], chunk[1]])); + } + words +} + +pub(in crate::inspect::smp) fn read_u8_at(bytes: &[u8], offset: usize) -> Option { + bytes.get(offset).copied() +} + +pub(in crate::inspect::smp) fn read_u16_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 2)?; + Some(u16::from_le_bytes([chunk[0], chunk[1]])) +} + +pub(in crate::inspect::smp) fn read_u32_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 4)?; + Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) +} + +pub(in crate::inspect::smp) fn read_i32_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 4)?; + Some(i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) +} + +pub(in crate::inspect::smp) fn read_i64_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 8)?; + Some(i64::from_le_bytes([ + chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7], + ])) +} + +pub(in crate::inspect::smp) fn read_u64_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 8)?; + Some(u64::from_le_bytes([ + chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7], + ])) +} + +pub(in crate::inspect::smp) fn read_f32_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 4)?; + Some(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) +} + +pub(in crate::inspect::smp) fn read_f64_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 8)?; + Some(f64::from_le_bytes([ + chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7], + ])) +} + +pub(in crate::inspect::smp) fn read_ascii_c_string_at( + bytes: &[u8], + offset: usize, + max_len: usize, +) -> Option { + let chunk = bytes.get(offset..offset + max_len)?; + let nul_index = chunk.iter().position(|byte| *byte == 0).unwrap_or(max_len); + let text = std::str::from_utf8(&chunk[..nul_index]) + .ok()? + .trim() + .to_string(); + Some(text) +} + +pub(in crate::inspect::smp) fn parse_nonzero_u32( + bytes: &[u8], + offset: usize, +) -> Option> { + read_u32_at(bytes, offset).map(|value| (value != 0).then_some(value)) +} + +pub(in crate::inspect::smp) fn round_f64_to_i64(value: f64) -> Option { + if !value.is_finite() { + return None; + } + let rounded = value.round(); + if rounded < i64::MIN as f64 || rounded > i64::MAX as f64 { + return None; + } + Some(rounded as i64) +} diff --git a/crates/rrt-runtime/src/inspect/smp/common/recipe.rs b/crates/rrt-runtime/src/inspect/smp/common/recipe.rs new file mode 100644 index 0000000..0098ccd --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common/recipe.rs @@ -0,0 +1,95 @@ +pub(in crate::inspect::smp) fn probable_normal_f32_string(value: u32) -> Option { + let exponent = (value >> 23) & 0xff; + if exponent == 0 || exponent == 0xff { + return None; + } + let scalar = f32::from_bits(value); + if !scalar.is_finite() { + return None; + } + Some(format!("{scalar:.6}")) +} + +pub(in crate::inspect::smp) fn probable_recipe_token_high16_ascii_stem( + value: u32, +) -> Option { + if value & 0xffff != 0 { + return None; + } + let high = ((value >> 16) & 0xffff) as u16; + if high == 0 { + return None; + } + let low_byte = (high & 0x00ff) as u8; + let high_byte = (high >> 8) as u8; + if !low_byte.is_ascii_alphabetic() || !high_byte.is_ascii_alphabetic() { + return None; + } + Some(format!("{}{}", low_byte as char, high_byte as char)) +} + +pub(in crate::inspect::smp) fn classify_recipe_token_layout(value: u32) -> &'static str { + if value == 0 { + return "zero"; + } + if probable_recipe_token_high16_ascii_stem(value).is_some() { + return "high16-ascii-stem"; + } + if value & 0xffff == 0 { + return "high16-numeric"; + } + if value >> 16 == 0 { + return "low16-marker"; + } + "mixed" +} + +pub(in crate::inspect::smp) fn classify_recipe_line_signature( + mode_word: u32, + supplied_cargo_token_word: u32, + demanded_cargo_token_word: u32, +) -> &'static str { + let supplied_layout = classify_recipe_token_layout(supplied_cargo_token_word); + let demanded_layout = classify_recipe_token_layout(demanded_cargo_token_word); + if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_layout == "high16-numeric" { + return "demand-numeric-entry"; + } + if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_layout == "high16-ascii-stem" { + return "demand-stem-entry"; + } + if mode_word == 0 && demanded_cargo_token_word == 0 && supplied_layout == "high16-numeric" { + return "supply-numeric-entry"; + } + if mode_word != 0 && demanded_cargo_token_word == 0 && supplied_layout == "low16-marker" { + return "supply-marker-entry"; + } + if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_cargo_token_word == 0 { + return "zero"; + } + "mixed" +} + +pub(in crate::inspect::smp) fn classify_recipe_runtime_import_branch( + mode_word: u32, +) -> &'static str { + if mode_word == 0 { + return "zero-mode-skipped"; + } + if mode_word == 1 { + return "mode1-demand-branch"; + } + if mode_word == 3 { + return "mode3-dual-branch"; + } + "nonzero-supply-branch" +} + +pub(in crate::inspect::smp) fn classify_recipe_book_region_kind(bytes: &[u8]) -> &'static str { + if bytes.iter().all(|byte| *byte == 0) { + "zero" + } else if bytes.iter().all(|byte| *byte == 0xcd) { + "cdcd" + } else { + "mixed" + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/common/search.rs b/crates/rrt-runtime/src/inspect/smp/common/search.rs new file mode 100644 index 0000000..0f3f86d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common/search.rs @@ -0,0 +1,52 @@ +pub(in crate::inspect::smp) fn find_u16_le_offsets(bytes: &[u8], needle: u16) -> Vec { + let pattern = needle.to_le_bytes(); + bytes + .windows(pattern.len()) + .enumerate() + .filter_map(|(offset, window)| (window == pattern).then_some(offset)) + .collect() +} + +pub(in crate::inspect::smp) fn find_u32_le_offsets(bytes: &[u8], needle: u32) -> Vec { + let pattern = needle.to_le_bytes(); + bytes + .windows(pattern.len()) + .enumerate() + .filter_map(|(offset, window)| (window == pattern).then_some(offset)) + .collect() +} + +pub(in crate::inspect::smp) fn find_next_nonzero_offset( + bytes: &[u8], + start: usize, +) -> Option { + bytes + .iter() + .enumerate() + .skip(start) + .find_map(|(offset, byte)| (*byte != 0).then_some(offset)) +} + +pub(in crate::inspect::smp) fn find_zero_run( + bytes: &[u8], + start: usize, + min_len: usize, +) -> Option { + let mut run_start = None; + let mut run_len = 0usize; + + for (offset, byte) in bytes.iter().enumerate().skip(start) { + if *byte == 0 { + run_start.get_or_insert(offset); + run_len += 1; + if run_len >= min_len { + return run_start; + } + } else { + run_start = None; + run_len = 0; + } + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/common/signatures.rs b/crates/rrt-runtime/src/inspect/smp/common/signatures.rs new file mode 100644 index 0000000..882a605 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/common/signatures.rs @@ -0,0 +1,50 @@ +pub(in crate::inspect::smp) fn format_u16_word_signature(words: &[u16]) -> String { + words + .iter() + .map(|word| format!("0x{word:04x}")) + .collect::>() + .join(", ") +} + +pub(in crate::inspect::smp) fn compact_nondirect_signature_family( + grouped_marker_relative_offset: Option, + head_signature_words: &[u16], + post_group_signature_words: &[u16], +) -> String { + let grouped_marker_bucket = grouped_marker_relative_offset.unwrap_or(0); + let head_word_2 = head_signature_words.get(2).copied().unwrap_or_default(); + let head_word_4 = head_signature_words.get(4).copied().unwrap_or_default(); + let head_word_6 = head_signature_words.get(6).copied().unwrap_or_default(); + let head_word_8 = head_signature_words.get(8).copied().unwrap_or_default(); + let head_word_10 = head_signature_words.get(10).copied().unwrap_or_default(); + let post_word_1 = post_group_signature_words + .get(1) + .copied() + .unwrap_or_default(); + let post_word_3 = post_group_signature_words + .get(3) + .copied() + .unwrap_or_default(); + let post_word_5 = post_group_signature_words + .get(5) + .copied() + .unwrap_or_default(); + let post_word_7 = post_group_signature_words + .get(7) + .copied() + .unwrap_or_default(); + + format!( + "nondirect-ge{:02x}-h{:04x}-{:04x}-{:04x}-{:04x}-{:04x}-p{:04x}-{:04x}-{:04x}-{:04x}", + grouped_marker_bucket, + head_word_2, + head_word_4, + head_word_6, + head_word_8, + head_word_10, + post_word_1, + post_word_3, + post_word_5, + post_word_7, + ) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/actors.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/actors.rs new file mode 100644 index 0000000..35a5be1 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/actors.rs @@ -0,0 +1,195 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(super) fn decode_actor_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + compact_control: &SmpLoadedPackedEventCompactControlSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_subject: Option, + target_scope_ordinal: u8, +) -> Option { + decode_company_governance_effect(row, descriptor_metadata, target_scope_ordinal) + .or_else(|| { + decode_cash_effect( + row, + descriptor_metadata, + target_subject, + target_scope_ordinal, + ) + }) + .or_else(|| { + decode_company_territory_access_effect( + row, + compact_control, + descriptor_metadata, + target_scope_ordinal, + ) + }) + .or_else(|| decode_confiscate_effect(row, descriptor_metadata, target_scope_ordinal)) + .or_else(|| { + decode_deactivate_effect( + row, + descriptor_metadata, + target_subject, + target_scope_ordinal, + ) + }) + .or_else(|| { + decode_track_laying_capacity_effect(row, descriptor_metadata, target_scope_ordinal) + }) +} + +fn decode_company_governance_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_scope_ordinal: u8, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.parameter_family == "company_governance_scalar" + && row.row_shape == "scalar_assignment" + { + let target = real_grouped_company_target(target_scope_ordinal)?; + let metric = real_grouped_company_governance_metric(descriptor_metadata)?; + return Some(RuntimeEffect::SetCompanyGovernanceScalar { + target, + metric, + value: i64::from(row.raw_scalar_value), + }); + } + None +} + +fn decode_cash_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_subject: Option, + target_scope_ordinal: u8, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 1 + && row.opcode == 8 + && row.row_shape == "multivalue_scalar" + { + return match target_subject { + Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::SetChairmanCash { + target: real_grouped_chairman_target(target_scope_ordinal)?, + value: i64::from(row.raw_scalar_value), + }), + _ => Some(RuntimeEffect::SetPlayerCash { + target: real_grouped_player_target(target_scope_ordinal)?, + value: i64::from(row.raw_scalar_value), + }), + }; + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 2 + && row.opcode == 8 + && row.row_shape == "multivalue_scalar" + { + let target = real_grouped_company_target(target_scope_ordinal)?; + return Some(RuntimeEffect::SetCompanyCash { + target, + value: i64::from(row.raw_scalar_value), + }); + } + + None +} + +fn decode_company_territory_access_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + compact_control: &SmpLoadedPackedEventCompactControlSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_scope_ordinal: u8, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 3 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + { + let target = real_grouped_company_target(target_scope_ordinal)?; + let territory = compact_control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .copied() + .filter(|selector| *selector >= 0) + .map(|selector| RuntimeTerritoryTarget::Ids { + ids: vec![selector as u32], + })?; + return Some(RuntimeEffect::SetCompanyTerritoryAccess { + target, + territory, + value: true, + }); + } + None +} + +fn decode_confiscate_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_scope_ordinal: u8, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 9 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + { + let target = real_grouped_company_target(target_scope_ordinal)?; + return Some(RuntimeEffect::ConfiscateCompanyAssets { target }); + } + None +} + +fn decode_deactivate_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_subject: Option, + target_scope_ordinal: u8, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 13 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + { + let target = real_grouped_company_target(target_scope_ordinal)?; + return Some(RuntimeEffect::DeactivateCompany { target }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 14 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + { + return match target_subject { + Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::DeactivateChairman { + target: real_grouped_chairman_target(target_scope_ordinal)?, + }), + _ => Some(RuntimeEffect::DeactivatePlayer { + target: real_grouped_player_target(target_scope_ordinal)?, + }), + }; + } + + None +} + +fn decode_track_laying_capacity_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_scope_ordinal: u8, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 16 + && row.row_shape == "scalar_assignment" + && row.raw_scalar_value >= 0 + { + let target = real_grouped_company_target(target_scope_ordinal)?; + return Some(RuntimeEffect::SetCompanyTrackLayingCapacity { + target, + value: Some(row.raw_scalar_value as u32), + }); + } + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/catalogs.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/catalogs.rs new file mode 100644 index 0000000..b0aa299 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/catalogs.rs @@ -0,0 +1,69 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(super) fn decode_catalog_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 109 + && row.row_shape == "scalar_assignment" + { + return Some(RuntimeEffect::SetCandidateAvailability { + name: runtime_candidate_availability_name(descriptor_metadata.label), + value: row.raw_scalar_value as u32, + }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.parameter_family == "cargo_price_scalar" + && row.row_shape == "scalar_assignment" + && row.raw_scalar_value >= 0 + { + return match row.descriptor_id { + 105 => Some(RuntimeEffect::SetCargoPriceOverride { + target: RuntimeCargoPriceTarget::All, + value: row.raw_scalar_value as u32, + }), + descriptor_id => grounded_named_cargo_price_label(descriptor_id).map(|name| { + RuntimeEffect::SetCargoPriceOverride { + target: RuntimeCargoPriceTarget::Named { + name: name.to_string(), + }, + value: row.raw_scalar_value as u32, + } + }), + }; + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.parameter_family == "cargo_production_scalar" + && row.row_shape == "scalar_assignment" + && row.raw_scalar_value >= 0 + { + return match descriptor_metadata.descriptor_id { + 177 => Some(RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::All, + value: row.raw_scalar_value as u32, + }), + 178 => Some(RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::Factory, + value: row.raw_scalar_value as u32, + }), + 179 => Some(RuntimeEffect::SetCargoProductionOverride { + target: RuntimeCargoProductionTarget::FarmMine, + value: row.raw_scalar_value as u32, + }), + 230..=240 => { + let slot = descriptor_metadata.descriptor_id.checked_sub(229)?; + Some(RuntimeEffect::SetCargoProductionSlot { + slot, + value: row.raw_scalar_value as u32, + }) + } + _ => None, + }; + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/decode.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/decode.rs new file mode 100644 index 0000000..174baf9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/decode.rs @@ -0,0 +1,53 @@ +use crate::inspect::smp::events::actions::world::decode_world_effect; +use crate::inspect::smp::events::actions::{actors, catalogs, runtime_variables, trains}; +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn decode_real_grouped_effect_actions( + grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary], + compact_control: &SmpLoadedPackedEventCompactControlSummary, +) -> Vec { + grouped_effect_rows + .iter() + .filter_map(|row| decode_real_grouped_effect_action(row, compact_control)) + .collect() +} + +pub(in crate::inspect::smp) fn decode_real_grouped_effect_action( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + compact_control: &SmpLoadedPackedEventCompactControlSummary, +) -> Option { + let descriptor_metadata = real_grouped_effect_descriptor_metadata(row.descriptor_id)?; + let target_scope_ordinal = compact_control + .grouped_target_scope_ordinals_0x7fb + .get(row.group_index) + .copied()?; + let target_subject = derive_real_grouped_target_subject(row, compact_control); + + runtime_variables::decode_runtime_variable_effect( + row, + compact_control, + descriptor_metadata, + target_subject, + target_scope_ordinal, + ) + .or_else(|| { + actors::decode_actor_effect( + row, + compact_control, + descriptor_metadata, + target_subject, + target_scope_ordinal, + ) + }) + .or_else(|| decode_world_effect(row, descriptor_metadata)) + .or_else(|| catalogs::decode_catalog_effect(row, descriptor_metadata)) + .or_else(|| { + trains::decode_train_effect( + row, + compact_control, + descriptor_metadata, + target_scope_ordinal, + ) + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/mod.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/mod.rs new file mode 100644 index 0000000..626a57f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/mod.rs @@ -0,0 +1,18 @@ +mod actors; +mod catalogs; +mod decode; +mod row_summary; +mod runtime_variables; +mod targets; +mod trains; +mod world; + +#[allow(unused_imports)] +pub(in crate::inspect::smp) use decode::{ + decode_real_grouped_effect_action, decode_real_grouped_effect_actions, +}; +pub(in crate::inspect::smp) use row_summary::parse_real_grouped_effect_row_summary; +pub(in crate::inspect::smp) use targets::{ + real_grouped_chairman_target, real_grouped_chairman_target_supported_in_runtime, + real_grouped_company_target, real_grouped_player_target, +}; diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/row_summary.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/row_summary.rs new file mode 100644 index 0000000..ca3b15c --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/row_summary.rs @@ -0,0 +1,160 @@ +use crate::inspect::smp::events::*; + +pub(in crate::inspect::smp) fn parse_real_grouped_effect_row_summary( + row_bytes: &[u8], + group_index: usize, + row_index: usize, + locomotive_name: Option, +) -> Option { + let descriptor_id = read_u32_at(row_bytes, 0)?; + let raw_scalar_value = read_u32_at(row_bytes, 4)? as i32; + let opcode = read_u8_at(row_bytes, 8)?; + let value_byte_0x09 = read_u8_at(row_bytes, 9)?; + let value_dword_0x0d = read_u32_at(row_bytes, 0x0d)?; + let value_byte_0x11 = read_u8_at(row_bytes, 0x11)?; + let value_byte_0x12 = read_u8_at(row_bytes, 0x12)?; + let value_word_0x14 = read_u16_at(row_bytes, 0x14)?; + let value_word_0x16 = read_u16_at(row_bytes, 0x16)?; + let descriptor_metadata = real_grouped_effect_descriptor_metadata(descriptor_id); + let mut row_shape = classify_real_grouped_effect_row_shape( + opcode, + raw_scalar_value, + value_byte_0x11, + value_byte_0x12, + value_word_0x14, + value_word_0x16, + ) + .to_string(); + let mut semantic_family = classify_real_grouped_effect_semantic_family( + opcode, + raw_scalar_value, + value_byte_0x11, + value_byte_0x12, + value_word_0x14, + value_word_0x16, + ) + .to_string(); + if descriptor_metadata.is_some_and(|metadata| { + matches!( + metadata.parameter_family, + "special_condition_scalar" | "candidate_availability_scalar" + ) && opcode == 3 + && value_byte_0x11 == 0 + && value_byte_0x12 == 0 + && value_word_0x14 == 0 + && value_word_0x16 == 0 + }) { + row_shape = "scalar_assignment".to_string(); + semantic_family = "scalar_assignment".to_string(); + } + if descriptor_metadata + .is_some_and(|metadata| metadata.parameter_family == "world_building_spawn") + { + row_shape = "building_spawn_batch".to_string(); + semantic_family = "building_spawn_batch".to_string(); + } + + let mut notes = Vec::new(); + if locomotive_name.is_some() { + notes.push("grouped effect row carries locomotive-name side string".to_string()); + } + if let Some(metadata) = descriptor_metadata { + if metadata.runtime_status != RealGroupedEffectRuntimeStatus::Executable { + notes.push(format!( + "descriptor is recovered in the checked-in effect table as {} parity", + real_grouped_effect_runtime_status_name(metadata.runtime_status) + )); + } + } else { + notes.push("descriptor id not yet recovered in the checked-in effect table".to_string()); + } + if let Some(loco_id) = recovered_locomotive_availability_loco_id(descriptor_id) { + notes.push(format!( + "locomotive availability descriptor maps to live locomotive id {loco_id}" + )); + } + if let Some(loco_id) = recovered_locomotive_cost_loco_id(descriptor_id) { + notes.push(format!( + "locomotive cost descriptor maps to live locomotive id {loco_id}" + )); + } + if let Some(cargo_slot) = recovered_cargo_production_slot(descriptor_id) { + notes.push(format!( + "cargo-production descriptor maps to world production slot {cargo_slot}" + )); + } + if let Some(cargo_label) = grounded_named_cargo_production_label(descriptor_id) { + notes.push(format!( + "named cargo production descriptor maps to cargo {cargo_label}" + )); + } + if descriptor_metadata.is_some_and(|metadata| metadata.parameter_family == "cargo_price_scalar") + { + if let Some(cargo_label) = grounded_named_cargo_price_label(descriptor_id) { + notes.push(format!( + "named cargo price descriptor maps to cargo {cargo_label}" + )); + } + } + if descriptor_metadata + .is_some_and(|metadata| metadata.parameter_family == "world_building_spawn") + { + let candidate_id = descriptor_id.saturating_sub(503); + notes.push(format!( + "add-building descriptor maps to live candidate id {candidate_id}" + )); + notes.push( + "0x430270 add-building consumer uses placement count byte 0x11, center words 0x12/0x14, and radius word 0x16 after the descriptor_id - 503 candidate bridge; it clamps radius to at least 1 and retries up to 200 randomized placements without branching directly on grouped opcode".to_string(), + ); + if candidate_id > 66 { + notes.push( + "current non-hook candidate-name catalogs only ground concrete add-building names through candidate id 66, so this descriptor remains on the checked-in candidate-slot boundary beyond the live RT3 1.05 table".to_string(), + ); + } + } + + Some(SmpLoadedPackedEventGroupedEffectRowSummary { + group_index, + row_index, + descriptor_id, + descriptor_label: descriptor_metadata.map(|metadata| metadata.label.to_string()), + target_mask_bits: descriptor_metadata.map(|metadata| metadata.target_mask_bits), + parameter_family: descriptor_metadata.map(|metadata| metadata.parameter_family.to_string()), + grouped_target_subject: None, + grouped_target_scope: None, + opcode, + raw_scalar_value, + value_byte_0x09, + value_dword_0x0d, + value_byte_0x11, + value_byte_0x12, + value_word_0x14, + value_word_0x16, + row_shape, + semantic_family: Some(semantic_family.clone()), + semantic_preview: Some(build_real_grouped_effect_semantic_preview( + descriptor_metadata.map(|metadata| metadata.label), + &semantic_family, + raw_scalar_value, + value_byte_0x11, + value_byte_0x12, + value_word_0x14, + value_word_0x16, + )), + recovered_cargo_slot: recovered_cargo_production_slot(descriptor_id), + recovered_cargo_class: recovered_cargo_production_slot(descriptor_id) + .and_then(known_cargo_slot_definition) + .map(|definition| runtime_cargo_class_name(definition.cargo_class).to_string()), + recovered_cargo_label: grounded_named_cargo_production_label(descriptor_id) + .or_else(|| { + descriptor_metadata + .filter(|metadata| metadata.parameter_family == "cargo_price_scalar") + .and_then(|_| grounded_named_cargo_price_label(descriptor_id)) + }) + .map(ToString::to_string), + recovered_locomotive_id: recovered_locomotive_availability_loco_id(descriptor_id) + .or_else(|| recovered_locomotive_cost_loco_id(descriptor_id)), + locomotive_name, + notes, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/runtime_variables.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/runtime_variables.rs new file mode 100644 index 0000000..5290e7b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/runtime_variables.rs @@ -0,0 +1,58 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +fn runtime_variable_index(descriptor_id: u32) -> Option { + match descriptor_id { + 39..=42 => Some(descriptor_id - 38), + 43..=46 => Some(descriptor_id - 42), + 47..=50 => Some(descriptor_id - 46), + 51..=54 => Some(descriptor_id - 50), + _ => None, + } +} + +pub(super) fn decode_runtime_variable_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + compact_control: &SmpLoadedPackedEventCompactControlSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_subject: Option, + target_scope_ordinal: u8, +) -> Option { + if !(descriptor_metadata.executable_in_runtime + && descriptor_metadata.parameter_family == "runtime_variable_scalar" + && row.row_shape == "scalar_assignment") + { + return None; + } + + let index = runtime_variable_index(descriptor_metadata.descriptor_id)?; + match target_subject { + Some(RealGroupedTargetSubject::WholeGame) => Some(RuntimeEffect::SetWorldVariable { + index, + value: i64::from(row.raw_scalar_value), + }), + Some(RealGroupedTargetSubject::Company) => Some(RuntimeEffect::SetCompanyVariable { + target: real_grouped_company_target(target_scope_ordinal)?, + index, + value: i64::from(row.raw_scalar_value), + }), + Some(RealGroupedTargetSubject::Player) => Some(RuntimeEffect::SetPlayerVariable { + target: real_grouped_player_target(target_scope_ordinal)?, + index, + value: i64::from(row.raw_scalar_value), + }), + Some(RealGroupedTargetSubject::Territory) => compact_control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .copied() + .filter(|selector| *selector >= 0) + .map(|selector| RuntimeEffect::SetTerritoryVariable { + target: RuntimeTerritoryTarget::Ids { + ids: vec![selector as u32], + }, + index, + value: i64::from(row.raw_scalar_value), + }), + _ => None, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/targets.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/targets.rs new file mode 100644 index 0000000..05088f3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/targets.rs @@ -0,0 +1,43 @@ +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn real_grouped_company_target( + ordinal: u8, +) -> Option { + match ordinal { + 0 => Some(RuntimeCompanyTarget::ConditionTrueCompany), + 1 => Some(RuntimeCompanyTarget::SelectedCompany), + 2 => Some(RuntimeCompanyTarget::HumanCompanies), + 3 => Some(RuntimeCompanyTarget::AiCompanies), + _ => None, + } +} + +pub(in crate::inspect::smp) fn real_grouped_player_target( + ordinal: u8, +) -> Option { + match ordinal { + 0 => Some(RuntimePlayerTarget::ConditionTruePlayer), + 1 => Some(RuntimePlayerTarget::SelectedPlayer), + 2 => Some(RuntimePlayerTarget::HumanPlayers), + 3 => Some(RuntimePlayerTarget::AiPlayers), + _ => None, + } +} + +pub(in crate::inspect::smp) fn real_grouped_chairman_target( + ordinal: u8, +) -> Option { + match ordinal { + 0 => Some(RuntimeChairmanTarget::ConditionTrueChairman), + 1 => Some(RuntimeChairmanTarget::SelectedChairman), + 2 => Some(RuntimeChairmanTarget::HumanChairmen), + 3 => Some(RuntimeChairmanTarget::AiChairmen), + _ => None, + } +} + +pub(in crate::inspect::smp) fn real_grouped_chairman_target_supported_in_runtime( + ordinal: u8, +) -> bool { + real_grouped_chairman_target(ordinal).is_some() +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/trains.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/trains.rs new file mode 100644 index 0000000..b1f173f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/trains.rs @@ -0,0 +1,35 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(super) fn decode_train_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + compact_control: &SmpLoadedPackedEventCompactControlSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, + target_scope_ordinal: u8, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 15 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + { + let company_target = real_grouped_company_target(target_scope_ordinal); + let territory_target = compact_control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .copied() + .filter(|selector| *selector >= 0) + .map(|selector| RuntimeTerritoryTarget::Ids { + ids: vec![selector as u32], + }); + if company_target.is_none() && territory_target.is_none() { + return None; + } + return Some(RuntimeEffect::RetireTrains { + company_target, + territory_target, + locomotive_name: row.locomotive_name.clone(), + }); + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/actions/world.rs b/crates/rrt-runtime/src/inspect/smp/events/actions/world.rs new file mode 100644 index 0000000..0ef2128 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/actions/world.rs @@ -0,0 +1,67 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(super) fn decode_world_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + descriptor_metadata: RealGroupedEffectDescriptorMetadata, +) -> Option { + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 8 + && row.row_shape == "scalar_assignment" + { + return Some(RuntimeEffect::SetEconomicStatusCode { + value: row.raw_scalar_value, + }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 108 + && row.row_shape == "scalar_assignment" + { + return Some(RuntimeEffect::SetSpecialCondition { + label: descriptor_metadata.label.to_string(), + value: row.raw_scalar_value as u32, + }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 122 + && row.row_shape == "scalar_assignment" + { + return Some(RuntimeEffect::SetLimitedTrackBuildingAmount { + value: row.raw_scalar_value, + }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.parameter_family == "world_scalar_override" + && row.row_shape == "scalar_assignment" + { + return Some(RuntimeEffect::SetWorldScalarOverride { + key: runtime_world_scalar_key(descriptor_metadata)?, + value: i64::from(row.raw_scalar_value), + }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.parameter_family == "territory_access_cost_scalar" + && row.row_shape == "scalar_assignment" + && row.raw_scalar_value >= 0 + { + return Some(RuntimeEffect::SetTerritoryAccessCost { + value: row.raw_scalar_value as u32, + }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.parameter_family == "world_flag_toggle" + && row.row_shape == "bool_toggle" + { + return Some(RuntimeEffect::SetWorldFlag { + key: runtime_world_flag_key(descriptor_metadata)?, + value: row.raw_scalar_value != 0, + }); + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/collection/control_lane.rs b/crates/rrt-runtime/src/inspect/smp/events/collection/control_lane.rs new file mode 100644 index 0000000..494bcc3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/collection/control_lane.rs @@ -0,0 +1,484 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(super) struct ControlLaneAnalysis { + pub(super) records_with_trigger_kind: usize, + pub(super) records_missing_trigger_kind: usize, + pub(super) nondirect_compact_record_count: usize, + pub(super) nondirect_compact_records_missing_trigger_kind: usize, + pub(super) trigger_kinds_present: Vec, + pub(super) add_building_dispatch_strip_record_indexes: Vec, + pub(super) add_building_dispatch_strip_descriptor_labels: Vec, + pub(super) add_building_dispatch_strip_records_with_trigger_kind: usize, + pub(super) add_building_dispatch_strip_records_missing_trigger_kind: usize, + pub(super) add_building_dispatch_strip_row_shape_families: Vec, + pub(super) add_building_dispatch_strip_signature_families: Vec, + pub(super) add_building_dispatch_strip_condition_tuple_families: Vec, + pub(super) add_building_dispatch_strip_signature_condition_clusters: Vec, + pub(super) control_lane_notes: Vec, +} + +pub(super) fn build_control_lane_analysis( + records: &[SmpLoadedPackedEventRecordSummary], +) -> ControlLaneAnalysis { + let records_with_trigger_kind = records + .iter() + .filter(|record| record.trigger_kind.is_some()) + .count(); + let records_missing_trigger_kind = records.len().saturating_sub(records_with_trigger_kind); + let nondirect_compact_record_count = records + .iter() + .filter(|record| record.payload_family == "real_packed_nondirect_compact_v1") + .count(); + let nondirect_compact_records_missing_trigger_kind = records + .iter() + .filter(|record| { + record.payload_family == "real_packed_nondirect_compact_v1" + && record.trigger_kind.is_none() + }) + .count(); + let mut trigger_kinds_present = records + .iter() + .filter_map(|record| record.trigger_kind) + .collect::>(); + trigger_kinds_present.sort_unstable(); + trigger_kinds_present.dedup(); + let mut mutation_candidate_record_indexes = records + .iter() + .filter(|record| { + record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + }) + }) + .map(|record| record.record_index) + .collect::>(); + mutation_candidate_record_indexes.sort_unstable(); + mutation_candidate_record_indexes.dedup(); + let mut mutation_candidate_opcodes = records + .iter() + .flat_map(|record| record.grouped_effect_rows.iter().map(|row| row.opcode)) + .filter(|opcode| opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(*opcode)) + .collect::>(); + mutation_candidate_opcodes.sort_unstable(); + mutation_candidate_opcodes.dedup(); + let mut opcode_0x08_record_indexes = records + .iter() + .filter(|record| { + record + .grouped_effect_rows + .iter() + .any(|row| row.opcode == 0x08) + }) + .map(|record| record.record_index) + .collect::>(); + opcode_0x08_record_indexes.sort_unstable(); + opcode_0x08_record_indexes.dedup(); + let mut add_building_dispatch_strip_record_indexes = records + .iter() + .filter(|record| { + record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) + }) + }) + .map(|record| record.record_index) + .collect::>(); + add_building_dispatch_strip_record_indexes.sort_unstable(); + add_building_dispatch_strip_record_indexes.dedup(); + let add_building_dispatch_strip_records_with_trigger_kind = records + .iter() + .filter(|record| { + record.trigger_kind.is_some() + && record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) + }) + }) + .count(); + let add_building_dispatch_strip_records_missing_trigger_kind = records + .iter() + .filter(|record| { + record.trigger_kind.is_none() + && record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) + }) + }) + .count(); + let mut mutation_candidate_descriptor_labels = records + .iter() + .flat_map(|record| record.grouped_effect_rows.iter()) + .filter(|row| opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode)) + .filter_map(|row| row.descriptor_label.clone()) + .collect::>(); + mutation_candidate_descriptor_labels.sort_unstable(); + mutation_candidate_descriptor_labels.dedup(); + let mut add_building_dispatch_strip_descriptor_labels = records + .iter() + .flat_map(|record| record.grouped_effect_rows.iter()) + .filter(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) + }) + .filter_map(|row| row.descriptor_label.clone()) + .collect::>(); + add_building_dispatch_strip_descriptor_labels.sort_unstable(); + add_building_dispatch_strip_descriptor_labels.dedup(); + let mut add_building_dispatch_strip_row_shape_families = records + .iter() + .filter(|record| { + record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) + }) + }) + .map(|record| { + compact_event_dispatch_row_shape_family_from_summary_rows(&record.grouped_effect_rows) + }) + .collect::>(); + add_building_dispatch_strip_row_shape_families.sort_unstable(); + add_building_dispatch_strip_row_shape_families.dedup(); + let mut add_building_dispatch_strip_signature_families = records + .iter() + .filter(|record| { + record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) + }) + }) + .map(|record| { + compact_event_signature_family_from_notes(&record.notes) + .unwrap_or_else(|| "unknown-signature-family".to_string()) + }) + .collect::>(); + add_building_dispatch_strip_signature_families.sort_unstable(); + add_building_dispatch_strip_signature_families.dedup(); + let mut add_building_dispatch_strip_condition_tuple_families = records + .iter() + .filter(|record| { + record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) + }) + }) + .map(|record| { + compact_event_dispatch_condition_tuple_family_from_summary_rows( + &record.standalone_condition_rows, + ) + }) + .collect::>(); + add_building_dispatch_strip_condition_tuple_families.sort_unstable(); + add_building_dispatch_strip_condition_tuple_families.dedup(); + let mut add_building_dispatch_strip_signature_condition_clusters = records + .iter() + .filter(|record| { + record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) + }) + }) + .map(|record| { + compact_event_dispatch_signature_condition_cluster_from_summary_rows( + compact_event_signature_family_from_notes(&record.notes).as_deref(), + &record.standalone_condition_rows, + ) + }) + .collect::>(); + add_building_dispatch_strip_signature_condition_clusters.sort_unstable(); + add_building_dispatch_strip_signature_condition_clusters.dedup(); + let mut mutation_candidate_unknown_descriptor_ids = records + .iter() + .flat_map(|record| record.grouped_effect_rows.iter()) + .filter(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + && row.descriptor_label.is_none() + }) + .map(|row| row.descriptor_id) + .collect::>(); + mutation_candidate_unknown_descriptor_ids.sort_unstable(); + mutation_candidate_unknown_descriptor_ids.dedup(); + let mut mutation_candidate_special_condition_label_matches = + mutation_candidate_unknown_descriptor_ids + .iter() + .filter_map(|descriptor_id| { + known_special_condition_label_for_compact_descriptor_id(*descriptor_id) + .map(|label| format!("{descriptor_id} -> {label}")) + }) + .collect::>(); + mutation_candidate_special_condition_label_matches.sort(); + mutation_candidate_special_condition_label_matches.dedup(); + let mut dispatch_strip_unknown_condition_ids = records + .iter() + .filter(|record| { + record.grouped_effect_rows.iter().any(|row| { + opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) + }) + }) + .flat_map(|record| record.standalone_condition_rows.iter()) + .filter(|row| row.raw_condition_id >= 0 && row.metric.is_none()) + .map(|row| row.raw_condition_id) + .collect::>(); + dispatch_strip_unknown_condition_ids.sort_unstable(); + dispatch_strip_unknown_condition_ids.dedup(); + + let control_lane_notes = build_control_lane_notes( + records, + records_with_trigger_kind, + nondirect_compact_record_count, + nondirect_compact_records_missing_trigger_kind, + &trigger_kinds_present, + &mutation_candidate_record_indexes, + &mutation_candidate_opcodes, + &mutation_candidate_descriptor_labels, + &mutation_candidate_unknown_descriptor_ids, + &mutation_candidate_special_condition_label_matches, + &dispatch_strip_unknown_condition_ids, + &opcode_0x08_record_indexes, + &add_building_dispatch_strip_record_indexes, + &add_building_dispatch_strip_descriptor_labels, + &add_building_dispatch_strip_row_shape_families, + &add_building_dispatch_strip_signature_families, + &add_building_dispatch_strip_condition_tuple_families, + &add_building_dispatch_strip_signature_condition_clusters, + add_building_dispatch_strip_records_with_trigger_kind, + ); + + ControlLaneAnalysis { + records_with_trigger_kind, + records_missing_trigger_kind, + nondirect_compact_record_count, + nondirect_compact_records_missing_trigger_kind, + trigger_kinds_present, + add_building_dispatch_strip_record_indexes, + add_building_dispatch_strip_descriptor_labels, + add_building_dispatch_strip_records_with_trigger_kind, + add_building_dispatch_strip_records_missing_trigger_kind, + add_building_dispatch_strip_row_shape_families, + add_building_dispatch_strip_signature_families, + add_building_dispatch_strip_condition_tuple_families, + add_building_dispatch_strip_signature_condition_clusters, + control_lane_notes, + } +} + +#[allow(clippy::too_many_arguments)] +fn build_control_lane_notes( + records: &[SmpLoadedPackedEventRecordSummary], + records_with_trigger_kind: usize, + nondirect_compact_record_count: usize, + nondirect_compact_records_missing_trigger_kind: usize, + trigger_kinds_present: &[u8], + mutation_candidate_record_indexes: &[usize], + mutation_candidate_opcodes: &[u8], + mutation_candidate_descriptor_labels: &[String], + mutation_candidate_unknown_descriptor_ids: &[u32], + mutation_candidate_special_condition_label_matches: &[String], + dispatch_strip_unknown_condition_ids: &[i32], + opcode_0x08_record_indexes: &[usize], + add_building_dispatch_strip_record_indexes: &[usize], + add_building_dispatch_strip_descriptor_labels: &[String], + add_building_dispatch_strip_row_shape_families: &[String], + add_building_dispatch_strip_signature_families: &[String], + add_building_dispatch_strip_condition_tuple_families: &[String], + add_building_dispatch_strip_signature_condition_clusters: &[String], + add_building_dispatch_strip_records_with_trigger_kind: usize, +) -> Vec { + let mut control_lane_notes = Vec::new(); + if nondirect_compact_record_count != 0 + && nondirect_compact_record_count == nondirect_compact_records_missing_trigger_kind + { + control_lane_notes.push( + "all compact non-direct rows currently decode row bodies only and still lack a decoded trigger/control lane".to_string(), + ); + control_lane_notes.push( + "direct disassembly now grounds that as a real loader boundary: 0x0042db20 allocates linked 0x1e/0x28 row nodes from the 0x4e9a slice and leaves [event+0x7ee..0x80f] to the separate 0x0042e050 full-event clone path, so missing trigger-kind bytes on this family are not just a parser gap".to_string(), + ); + control_lane_notes.push( + "the checked 0x0042e050 caller census is narrow too: current direct caller 0x004dba23 sits under the event-editor duplication path rather than 0x00433130 load, so ordinary 0x4e9a restore is not currently grounded to inherit trigger/control bytes through that deep-copy seam".to_string(), + ); + control_lane_notes.push( + "the first non-editor positive control-lane writer is bounded away from ordinary restore too: 0x00430b50 allocates a fresh live runtime-effect row through 0x00432ea0 -> 0x0042d5a0, seeds [event+0x7ef] to 2 or 3 plus adjacent control bytes, and is reached from 0x004323a0 service at 0x0043232e rather than 0x00433130 load".to_string(), + ); + control_lane_notes.push( + "the remaining non-editor [event+0x7ef] mutators are bounded away from restore too: the 0x00443200..0x004436e3 sweep searches existing live runtime-event names through 0x005a57cf (including strings like 'New Beginnings', 'Chicago to New York', 'The American', and 'Labor') and retags already-live records, so it reads as scenario-specific live maintenance rather than the missing 0x4e9a restore owner".to_string(), + ); + control_lane_notes.push( + "direct disassembly of 0x004323a0 now makes the live gate explicit too: the per-record service returns before dispatch unless one-shot latch [event+0x81f] is clear, mode byte [event+0x7ef] matches the selected trigger kind from 0x00432f40, and compact chain root [event+0x00] is nonzero; its kind-8 side path at 0x00432ca1..0x00432cb0 only calls 0x00438710 on already-live records with [event+0x7ef] == 8".to_string(), + ); + control_lane_notes.push( + "the post-load name-driven retagger is narrower than a bulk trigger-kind materializer too: direct disassembly of 0x00442c30 (called from 0x00443a50 at 0x00444b50) shows a hardcoded scenario-name patch table over already-live records in 0x0062be18/0x0062bae0, and the checked cases mostly tweak modifier bytes [event+0x7f9/+0x7fa] or nested payload scalars on records that already carry concrete kinds such as 7 ('Open Aus', 'The American'), 6 ('Test connections'), 5 ('Win - Gold'), and 1 ('Win - Silver' / 'Win - Bronze')".to_string(), + ); + control_lane_notes.push( + "direct disassembly now boxes in the explicit trigger-kind materializations inside that same retagger too: the 'SP - GOLD' branch at 0x00443526 rewrites [event+0x7ef] from 1 to 5 on live runtime-event id 1 when the scenario flag [world+0x66de] is set and payload-root kind 7 carries subtype byte 5, while the 'Labor' branch at 0x00443601 rewrites [event+0x7ef] from 0 to 2 on live runtime-event id 0x0d when the same scenario flag is set and the checked 0x3c -> 0x3d child payload pair carries the matching negative scalar sentinel".to_string(), + ); + } + if records_with_trigger_kind != 0 { + control_lane_notes.push(format!( + "decoded trigger kinds present in this collection = {:?}", + trigger_kinds_present + )); + } + if !mutation_candidate_record_indexes.is_empty() { + control_lane_notes.push(format!( + "records with grouped opcodes already in the 0x00431b20 dispatch strip = {:?}", + mutation_candidate_record_indexes + )); + if records_with_trigger_kind == 0 { + control_lane_notes.push( + "decoded grouped rows already reach the 0x00431b20 dispatch strip in this collection even though the current inspection surface recovered no trigger/control kind bytes for those records" + .to_string(), + ); + if nondirect_compact_record_count == records.len() { + control_lane_notes.push( + "every currently decoded dispatch-strip row in this collection still sits in the nondirect compact 0x4e99/0x4e9a/0x4e9b family with null [event+0x7ef], so the direct full-record 0x4e21/0x4e22 framing path is not currently bridging trigger-kind control bytes for these mutation-capable rows".to_string(), + ); + } + } + control_lane_notes.push(format!( + "0x00431b20 dispatch-strip opcodes present in decoded grouped rows = {:?}", + mutation_candidate_opcodes + )); + if !mutation_candidate_descriptor_labels.is_empty() { + control_lane_notes.push(format!( + "decoded grouped descriptor labels present in the 0x00431b20 dispatch strip = {:?}", + mutation_candidate_descriptor_labels + )); + } + if !mutation_candidate_unknown_descriptor_ids.is_empty() { + control_lane_notes.push(format!( + "grouped descriptor ids still missing checked-in labels in the 0x00431b20 dispatch strip = {:?}", + mutation_candidate_unknown_descriptor_ids + )); + } + if !mutation_candidate_special_condition_label_matches.is_empty() { + control_lane_notes.push(format!( + "unlabeled 0x00431b20 dispatch-strip descriptor ids matching known special-condition label_id-2000 values = {:?}", + mutation_candidate_special_condition_label_matches + )); + } + if !dispatch_strip_unknown_condition_ids.is_empty() { + control_lane_notes.push(format!( + "standalone condition ids still missing checked-in labels in the 0x00431b20 dispatch strip = {:?}", + dispatch_strip_unknown_condition_ids + )); + } + if !opcode_0x08_record_indexes.is_empty() { + control_lane_notes.push(format!( + "records with opcode 0x08 in the 0x00431b20 dispatch strip = {:?}", + opcode_0x08_record_indexes + )); + control_lane_notes.push( + "checked-in function-map evidence currently grounds opcode 0x08 on the 0x00426d60 company_deactivate_and_clear_chairman_share_links branch".to_string(), + ); + } + if !add_building_dispatch_strip_record_indexes.is_empty() { + control_lane_notes.push(format!( + "records with Add Building descriptors in the 0x00431b20 dispatch strip = {:?}", + add_building_dispatch_strip_record_indexes + )); + control_lane_notes.push(format!( + "decoded Add Building descriptor labels present in the 0x00431b20 dispatch strip = {:?}", + add_building_dispatch_strip_descriptor_labels + )); + control_lane_notes.push(format!( + "Add Building row-shape families present in the 0x00431b20 dispatch strip = {:?}", + add_building_dispatch_strip_row_shape_families + )); + control_lane_notes.push(format!( + "Add Building signature families present in the 0x00431b20 dispatch strip = {:?}", + add_building_dispatch_strip_signature_families + )); + control_lane_notes.push(format!( + "Add Building condition-tuple families present in the 0x00431b20 dispatch strip = {:?}", + add_building_dispatch_strip_condition_tuple_families + )); + control_lane_notes.push(format!( + "Add Building signature/condition clusters present in the 0x00431b20 dispatch strip = {:?}", + add_building_dispatch_strip_signature_condition_clusters + )); + if add_building_dispatch_strip_records_with_trigger_kind == 0 { + control_lane_notes.push( + "every currently decoded Add Building dispatch-strip row still has null trigger kind, so the missing control-lane mapping remains the blocker above the already-grounded add-building descriptor bridge".to_string(), + ); + } + } + } + + control_lane_notes +} + +pub(in crate::inspect::smp) fn opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip( + opcode: u8, +) -> bool { + matches!(opcode, 0x04..=0x08 | 0x0d | 0x10..=0x13 | 0x16) +} + +pub(in crate::inspect::smp) fn compact_event_dispatch_add_building_descriptor_id( + descriptor_id: u32, +) -> bool { + (503..=613).contains(&descriptor_id) +} + +pub(in crate::inspect::smp) fn compact_event_signature_family_from_notes( + notes: &[String], +) -> Option { + notes.iter().find_map(|note| { + note.strip_prefix("compact signature family = ") + .map(ToString::to_string) + }) +} + +pub(in crate::inspect::smp) fn compact_event_dispatch_condition_tuple_family_from_summary_rows( + rows: &[SmpLoadedPackedEventConditionRowSummary], +) -> String { + if rows.is_empty() { + return "[]".to_string(); + } + let parts = rows + .iter() + .map(|row| match &row.metric { + Some(metric) => format!("{}:{}:{}", row.raw_condition_id, row.subtype, metric), + None => format!("{}:{}", row.raw_condition_id, row.subtype), + }) + .collect::>(); + format!("[{}]", parts.join(",")) +} + +pub(in crate::inspect::smp) fn compact_event_dispatch_row_shape_family_from_summary_rows( + rows: &[SmpLoadedPackedEventGroupedEffectRowSummary], +) -> String { + if rows.is_empty() { + return "[]".to_string(); + } + let parts = rows + .iter() + .map(|row| { + format!( + "{}:{}:{}", + row.group_index, row.opcode, row.raw_scalar_value + ) + }) + .collect::>(); + format!("[{}]", parts.join(",")) +} + +pub(in crate::inspect::smp) fn compact_event_dispatch_signature_condition_cluster_from_summary_rows( + signature_family: Option<&str>, + rows: &[SmpLoadedPackedEventConditionRowSummary], +) -> String { + format!( + "{} :: {}", + signature_family.unwrap_or("unknown-signature-family"), + compact_event_dispatch_condition_tuple_family_from_summary_rows(rows) + ) +} + +pub(in crate::inspect::smp) fn known_special_condition_label_for_compact_descriptor_id( + descriptor_id: u32, +) -> Option<&'static str> { + let label_id = descriptor_id.checked_add(2000)?; + KNOWN_SPECIAL_CONDITION_DEFINITIONS + .iter() + .find(|definition| definition.label_id == label_id) + .map(|definition| definition.label) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/collection/live_ids.rs b/crates/rrt-runtime/src/inspect/smp/events/collection/live_ids.rs new file mode 100644 index 0000000..f6786c4 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/collection/live_ids.rs @@ -0,0 +1,34 @@ +pub(in crate::inspect::smp) fn decode_live_entry_ids_from_tombstone_bitset( + bitset: &[u8], + live_id_bound: u32, +) -> Option> { + let ids = decode_live_entry_ids_with_mapping(bitset, live_id_bound, false); + if ids.is_some() { + return ids; + } + decode_live_entry_ids_with_mapping(bitset, live_id_bound, true) +} + +pub(in crate::inspect::smp) fn decode_live_entry_ids_with_mapping( + bitset: &[u8], + live_id_bound: u32, + subtract_one: bool, +) -> Option> { + let mut live_entry_ids = Vec::new(); + + for entry_id in 1..=live_id_bound { + let bit_index = if subtract_one { + entry_id.checked_sub(1)? + } else { + entry_id + }; + let byte_index = usize::try_from(bit_index / 8).ok()?; + let bit_mask = 1u8.checked_shl(bit_index % 8).unwrap_or(0); + let tombstone_byte = *bitset.get(byte_index)?; + if tombstone_byte & bit_mask == 0 { + live_entry_ids.push(entry_id); + } + } + + Some(live_entry_ids) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/collection/mod.rs b/crates/rrt-runtime/src/inspect/smp/events/collection/mod.rs new file mode 100644 index 0000000..b949a13 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/collection/mod.rs @@ -0,0 +1,4 @@ +mod control_lane; +pub(super) mod live_ids; +pub(super) mod records; +pub(super) mod summary; diff --git a/crates/rrt-runtime/src/inspect/smp/events/collection/records.rs b/crates/rrt-runtime/src/inspect/smp/events/collection/records.rs new file mode 100644 index 0000000..0799c18 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/collection/records.rs @@ -0,0 +1,62 @@ +use crate::inspect::smp::events::*; + +pub(in crate::inspect::smp) fn parse_event_runtime_record_summaries( + records_payload: &[u8], + records_payload_offset: usize, + live_entry_ids: &[u32], +) -> Vec { + try_parse_synthetic_event_runtime_record_summaries( + records_payload, + records_payload_offset, + live_entry_ids, + ) + .or_else(|| { + try_parse_real_event_runtime_record_summaries( + records_payload, + records_payload_offset, + live_entry_ids, + ) + }) + .unwrap_or_else(|| { + build_unsupported_event_runtime_record_summaries( + live_entry_ids, + "0x4e9a payload did not match the current packed-event record decode harness", + ) + }) +} + +pub(in crate::inspect::smp) fn build_unsupported_event_runtime_record_summaries( + live_entry_ids: &[u32], + note: &str, +) -> Vec { + live_entry_ids + .iter() + .copied() + .enumerate() + .map( + |(record_index, live_entry_id)| SmpLoadedPackedEventRecordSummary { + record_index, + live_entry_id, + payload_offset: None, + payload_len: None, + decode_status: "unsupported_framing".to_string(), + payload_family: "unsupported_framing".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + notes: vec![note.to_string()], + }, + ) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/collection/summary.rs b/crates/rrt-runtime/src/inspect/smp/events/collection/summary.rs new file mode 100644 index 0000000..3a05932 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/collection/summary.rs @@ -0,0 +1,193 @@ +use super::control_lane::build_control_lane_analysis; +use super::live_ids::decode_live_entry_ids_from_tombstone_bitset; +use super::records::parse_event_runtime_record_summaries; +use crate::inspect::smp::bundle::SmpContainerProfile; +use crate::inspect::smp::events::*; +use crate::inspect::smp::save_load::SmpSaveLoadSummary; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn parse_event_runtime_collection_summary( + bytes: &[u8], + container_profile: Option<&SmpContainerProfile>, + save_load_summary: Option<&SmpSaveLoadSummary>, +) -> Option { + parse_event_runtime_collection_summary_with_tag_width( + bytes, + container_profile, + save_load_summary, + 2, + ) + .or_else(|| { + parse_event_runtime_collection_summary_with_tag_width( + bytes, + container_profile, + save_load_summary, + 4, + ) + }) +} + +pub(in crate::inspect::smp) fn parse_event_runtime_collection_summary_with_tag_width( + bytes: &[u8], + container_profile: Option<&SmpContainerProfile>, + save_load_summary: Option<&SmpSaveLoadSummary>, + tag_width: usize, +) -> Option { + let (metadata_offsets, record_offsets, close_offsets) = match tag_width { + 2 => ( + find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG), + find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG), + find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG), + ), + 4 => ( + find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32), + find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32), + find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32), + ), + _ => return None, + }; + + for metadata_tag_offset in metadata_offsets { + let packed_state_version = read_u32_at(bytes, metadata_tag_offset + tag_width)?; + if packed_state_version != EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION { + continue; + } + + let records_tag_offset = record_offsets + .iter() + .copied() + .find(|offset| *offset > metadata_tag_offset + tag_width + 4)?; + let close_tag_offset = close_offsets + .iter() + .copied() + .find(|offset| *offset > records_tag_offset)?; + let metadata_payload = + bytes.get(metadata_tag_offset + tag_width + 4..records_tag_offset)?; + if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { + continue; + } + + let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT) + .map(|index| read_u32_at(metadata_payload, index * 4)) + .collect::>>()?; + let direct_collection_flag = header_words[0]; + let direct_record_stride = usize::try_from(header_words[1]).ok()?; + let live_id_bound = header_words[4]; + let live_record_count = usize::try_from(header_words[5]).ok()?; + + let records_payload = bytes.get(records_tag_offset + tag_width..close_tag_offset)?; + let (source_kind, live_entry_ids) = if direct_collection_flag == 0 && tag_width == 4 { + ( + "packed-event-runtime-collection-nondirect".to_string(), + (1..=u32::try_from(live_record_count).ok()?).collect::>(), + ) + } else { + if direct_collection_flag == 0 || direct_record_stride == 0 { + continue; + } + + let bitset_len = ((usize::try_from(live_id_bound).ok()?).saturating_add(15)) / 8; + let payload_bytes = direct_record_stride.checked_mul(live_record_count)?; + if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + bitset_len { + continue; + } + if metadata_payload.len() < bitset_len + payload_bytes { + continue; + } + + let bitset_offset = metadata_payload.len() - bitset_len - payload_bytes; + if bitset_offset < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { + continue; + } + let bitset = metadata_payload.get(bitset_offset..bitset_offset + bitset_len)?; + let live_entry_ids = + decode_live_entry_ids_from_tombstone_bitset(bitset, live_id_bound)?; + if live_entry_ids.len() != live_record_count { + continue; + } + ( + "packed-event-runtime-collection".to_string(), + live_entry_ids, + ) + }; + + let records = if source_kind == "packed-event-runtime-collection-nondirect" { + try_parse_nondirect_event_runtime_record_summaries( + records_payload, + records_tag_offset + tag_width, + &live_entry_ids, + ) + .unwrap_or_else(|| { + parse_event_runtime_record_summaries( + records_payload, + records_tag_offset + tag_width, + &live_entry_ids, + ) + }) + } else { + parse_event_runtime_record_summaries( + records_payload, + records_tag_offset + tag_width, + &live_entry_ids, + ) + }; + + let decoded_record_count = records + .iter() + .filter(|record| record.decode_status != "unsupported_framing") + .count(); + let imported_runtime_record_count = records + .iter() + .filter(|record| record.executable_import_ready) + .count(); + let control_lane = build_control_lane_analysis(&records); + + return Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind, + mechanism_family: save_load_summary + .map(|summary| summary.mechanism_family.clone()) + .unwrap_or_else(|| "unknown".to_string()), + mechanism_confidence: save_load_summary + .map(|summary| summary.mechanism_confidence.clone()) + .unwrap_or_else(|| "inferred".to_string()), + container_profile_family: container_profile + .map(|profile| profile.profile_family.clone()), + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + packed_state_version, + packed_state_version_hex: format!("0x{packed_state_version:08x}"), + live_id_bound, + live_record_count, + live_entry_ids, + decoded_record_count, + imported_runtime_record_count, + records_with_trigger_kind: control_lane.records_with_trigger_kind, + records_missing_trigger_kind: control_lane.records_missing_trigger_kind, + nondirect_compact_record_count: control_lane.nondirect_compact_record_count, + nondirect_compact_records_missing_trigger_kind: control_lane + .nondirect_compact_records_missing_trigger_kind, + trigger_kinds_present: control_lane.trigger_kinds_present, + add_building_dispatch_strip_record_indexes: control_lane + .add_building_dispatch_strip_record_indexes, + add_building_dispatch_strip_descriptor_labels: control_lane + .add_building_dispatch_strip_descriptor_labels, + add_building_dispatch_strip_records_with_trigger_kind: control_lane + .add_building_dispatch_strip_records_with_trigger_kind, + add_building_dispatch_strip_records_missing_trigger_kind: control_lane + .add_building_dispatch_strip_records_missing_trigger_kind, + add_building_dispatch_strip_row_shape_families: control_lane + .add_building_dispatch_strip_row_shape_families, + add_building_dispatch_strip_signature_families: control_lane + .add_building_dispatch_strip_signature_families, + add_building_dispatch_strip_condition_tuple_families: control_lane + .add_building_dispatch_strip_condition_tuple_families, + add_building_dispatch_strip_signature_condition_clusters: control_lane + .add_building_dispatch_strip_signature_condition_clusters, + control_lane_notes: control_lane.control_lane_notes, + records, + }); + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/compat.rs b/crates/rrt-runtime/src/inspect/smp/events/compat.rs new file mode 100644 index 0000000..5b16a84 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/compat.rs @@ -0,0 +1,115 @@ +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn runtime_effect_supported_for_save_import( + effect: &RuntimeEffect, +) -> bool { + match effect { + RuntimeEffect::SetChairmanCash { target, .. } + | RuntimeEffect::DeactivateChairman { target } => matches!( + target, + RuntimeChairmanTarget::AllActive + | RuntimeChairmanTarget::HumanChairmen + | RuntimeChairmanTarget::AiChairmen + | RuntimeChairmanTarget::SelectedChairman + | RuntimeChairmanTarget::ConditionTrueChairman + | RuntimeChairmanTarget::Ids { .. } + ), + RuntimeEffect::SetWorldFlag { .. } + | RuntimeEffect::SetWorldVariable { .. } + | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } + | RuntimeEffect::SetEconomicStatusCode { .. } + | RuntimeEffect::SetCompanyGovernanceScalar { .. } + | RuntimeEffect::SetCandidateAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. } + | RuntimeEffect::SetNamedLocomotiveCost { .. } + | RuntimeEffect::SetCargoPriceOverride { .. } + | RuntimeEffect::SetCargoProductionOverride { .. } + | RuntimeEffect::SetCargoProductionSlot { .. } + | RuntimeEffect::SetWorldScalarOverride { .. } + | RuntimeEffect::SetTerritoryAccessCost { .. } + | RuntimeEffect::SetSpecialCondition { .. } + | RuntimeEffect::ConfiscateCompanyAssets { .. } + | RuntimeEffect::DeactivateCompany { .. } + | RuntimeEffect::DeactivatePlayer { .. } + | RuntimeEffect::SetCompanyTrackLayingCapacity { .. } + | RuntimeEffect::RetireTrains { .. } + | RuntimeEffect::ActivateEventRecord { .. } + | RuntimeEffect::DeactivateEventRecord { .. } + | RuntimeEffect::RemoveEventRecord { .. } => true, + RuntimeEffect::SetPlayerCash { target, .. } + | RuntimeEffect::SetPlayerVariable { target, .. } => matches!( + target, + RuntimePlayerTarget::AllActive + | RuntimePlayerTarget::Ids { .. } + | RuntimePlayerTarget::HumanPlayers + | RuntimePlayerTarget::AiPlayers + | RuntimePlayerTarget::SelectedPlayer + | RuntimePlayerTarget::ConditionTruePlayer + ), + RuntimeEffect::SetCompanyTerritoryAccess { + target, territory, .. + } => { + matches!( + target, + RuntimeCompanyTarget::AllActive + | RuntimeCompanyTarget::Ids { .. } + | RuntimeCompanyTarget::HumanCompanies + | RuntimeCompanyTarget::AiCompanies + | RuntimeCompanyTarget::SelectedCompany + | RuntimeCompanyTarget::ConditionTrueCompany + ) && matches!( + territory, + RuntimeTerritoryTarget::AllTerritories | RuntimeTerritoryTarget::Ids { .. } + ) + } + RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyVariable { target, .. } + | RuntimeEffect::AdjustCompanyCash { target, .. } + | RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!( + target, + RuntimeCompanyTarget::AllActive + | RuntimeCompanyTarget::Ids { .. } + | RuntimeCompanyTarget::HumanCompanies + | RuntimeCompanyTarget::AiCompanies + | RuntimeCompanyTarget::SelectedCompany + | RuntimeCompanyTarget::ConditionTrueCompany + ), + RuntimeEffect::SetTerritoryVariable { target, .. } => matches!( + target, + RuntimeTerritoryTarget::AllTerritories | RuntimeTerritoryTarget::Ids { .. } + ), + RuntimeEffect::AppendEventRecord { record } => record + .effects + .iter() + .all(runtime_effect_supported_for_save_import), + } +} + +pub(in crate::inspect::smp) fn runtime_condition_supported_for_save_import( + condition: &RuntimeCondition, +) -> bool { + match condition { + RuntimeCondition::WorldVariableThreshold { .. } + | RuntimeCondition::CompanyNumericThreshold { .. } + | RuntimeCondition::CompanyVariableThreshold { .. } + | RuntimeCondition::PlayerVariableThreshold { .. } + | RuntimeCondition::ChairmanNumericThreshold { .. } + | RuntimeCondition::TerritoryNumericThreshold { .. } + | RuntimeCondition::TerritoryVariableThreshold { .. } + | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } + | RuntimeCondition::SpecialConditionThreshold { .. } + | RuntimeCondition::CandidateAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionSlotThreshold { .. } + | RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::FactoryProductionTotalThreshold { .. } + | RuntimeCondition::FarmMineProductionTotalThreshold { .. } + | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } + | RuntimeCondition::EconomicStatusCodeThreshold { .. } + | RuntimeCondition::WorldFlagEquals { .. } => true, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/conditions/labels.rs b/crates/rrt-runtime/src/inspect/smp/events/conditions/labels.rs new file mode 100644 index 0000000..eca660e --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/conditions/labels.rs @@ -0,0 +1,132 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn real_ordinary_condition_metric_label( + metadata: RealOrdinaryConditionMetadata, + candidate_name: Option<&str>, +) -> String { + match metadata.kind { + RealOrdinaryConditionKind::Numeric(_) => metadata.label.to_string(), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition { + label, + }) => format!("Special Condition: {label}"), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => { + match candidate_name { + Some(name) => format!("Candidate Availability: {name}"), + None => "Candidate Availability".to_string(), + } + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability, + ) => match candidate_name { + Some(name) => format!("Named Locomotive Availability: {name}"), + None => "Named Locomotive Availability".to_string(), + }, + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => { + match candidate_name { + Some(name) => format!("Named Locomotive Cost: {name}"), + None => "Named Locomotive Cost".to_string(), + } + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { + match candidate_name { + Some(name) => format!("Cargo Production: {name}"), + None => "Cargo Production".to_string(), + } + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => { + "Cargo Production Total".to_string() + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal) => { + "Factory Production Total".to_string() + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FarmMineProductionTotal) => { + "Farm/Mine Production Total".to_string() + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::OtherCargoProductionTotal, + ) => "Other Cargo Production Total".to_string(), + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::LimitedTrackBuildingAmount, + ) => "Limited Track Building Amount".to_string(), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => { + "Territory Access Cost".to_string() + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => { + "Economic Status".to_string() + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { .. }) => { + format!("World Flag: {}", metadata.label) + } + } +} + +pub(in crate::inspect::smp) fn real_ordinary_condition_semantic_family( + metadata: RealOrdinaryConditionMetadata, +) -> &'static str { + match metadata.kind { + RealOrdinaryConditionKind::Numeric(_) => "numeric_threshold", + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { .. }) => { + "world_flag_equals" + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability + | RealWorldConditionKind::NamedLocomotiveCost + | RealWorldConditionKind::CargoProductionSlot + | RealWorldConditionKind::CargoProductionTotal + | RealWorldConditionKind::FactoryProductionTotal + | RealWorldConditionKind::FarmMineProductionTotal + | RealWorldConditionKind::OtherCargoProductionTotal + | RealWorldConditionKind::LimitedTrackBuildingAmount + | RealWorldConditionKind::TerritoryAccessCost, + ) => "world_scalar_threshold", + RealOrdinaryConditionKind::WorldState(_) => "world_state_threshold", + } +} + +pub(in crate::inspect::smp) fn decode_real_condition_comparator( + subtype: u8, +) -> Option { + match subtype { + 0 => Some(RuntimeConditionComparator::Ge), + 1 => Some(RuntimeConditionComparator::Le), + 2 => Some(RuntimeConditionComparator::Gt), + 3 => Some(RuntimeConditionComparator::Lt), + 4 => Some(RuntimeConditionComparator::Eq), + 5 => Some(RuntimeConditionComparator::Ne), + _ => None, + } +} + +pub(in crate::inspect::smp) fn decode_real_condition_threshold(flag_bytes: &[u8]) -> Option { + let raw = flag_bytes.get(0..4)?; + let mut bytes = [0u8; 4]; + bytes.copy_from_slice(raw); + Some(i32::from_le_bytes(bytes).into()) +} + +pub(in crate::inspect::smp) fn condition_comparator_label( + comparator: RuntimeConditionComparator, +) -> String { + match comparator { + RuntimeConditionComparator::Ge => "ge".to_string(), + RuntimeConditionComparator::Le => "le".to_string(), + RuntimeConditionComparator::Gt => "gt".to_string(), + RuntimeConditionComparator::Lt => "lt".to_string(), + RuntimeConditionComparator::Eq => "eq".to_string(), + RuntimeConditionComparator::Ne => "ne".to_string(), + } +} + +pub(in crate::inspect::smp) fn condition_comparator_symbol( + comparator: RuntimeConditionComparator, +) -> &'static str { + match comparator { + RuntimeConditionComparator::Ge => ">=", + RuntimeConditionComparator::Le => "<=", + RuntimeConditionComparator::Gt => ">", + RuntimeConditionComparator::Lt => "<", + RuntimeConditionComparator::Eq => "==", + RuntimeConditionComparator::Ne => "!=", + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/conditions/mod.rs b/crates/rrt-runtime/src/inspect/smp/events/conditions/mod.rs new file mode 100644 index 0000000..ab505fc --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/conditions/mod.rs @@ -0,0 +1,7 @@ +mod labels; +mod negative_sentinel; +mod row_decode; + +pub(in crate::inspect::smp) use labels::*; +pub(in crate::inspect::smp) use negative_sentinel::*; +pub(in crate::inspect::smp) use row_decode::*; diff --git a/crates/rrt-runtime/src/inspect/smp/events/conditions/negative_sentinel.rs b/crates/rrt-runtime/src/inspect/smp/events/conditions/negative_sentinel.rs new file mode 100644 index 0000000..2ca902a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/conditions/negative_sentinel.rs @@ -0,0 +1,49 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn derive_negative_sentinel_scope_summary( + rows: &[SmpLoadedPackedEventConditionRowSummary], + control: &SmpLoadedPackedEventCompactControlSummary, +) -> Option { + let source_row_indexes = rows + .iter() + .filter(|row| row.raw_condition_id == -1) + .map(|row| row.row_index) + .collect::>(); + if source_row_indexes.is_empty() { + return None; + } + + Some(SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: decode_company_condition_test_scope(control.modifier_flag_0x7f9)?, + player_test_scope: decode_player_condition_test_scope(control.modifier_flag_0x7fa)?, + territory_scope_selector_is_0x63: control.primary_selector_0x7f0 == 0x63, + source_row_indexes, + }) +} + +pub(in crate::inspect::smp) fn decode_company_condition_test_scope( + value: u8, +) -> Option { + match value { + 0 => Some(RuntimeCompanyConditionTestScope::Disabled), + 1 => Some(RuntimeCompanyConditionTestScope::AllCompanies), + 2 => Some(RuntimeCompanyConditionTestScope::SelectedCompanyOnly), + 3 => Some(RuntimeCompanyConditionTestScope::AiCompaniesOnly), + 4 => Some(RuntimeCompanyConditionTestScope::HumanCompaniesOnly), + _ => None, + } +} + +pub(in crate::inspect::smp) fn decode_player_condition_test_scope( + value: u8, +) -> Option { + match value { + 0 => Some(RuntimePlayerConditionTestScope::Disabled), + 1 => Some(RuntimePlayerConditionTestScope::AllPlayers), + 2 => Some(RuntimePlayerConditionTestScope::SelectedPlayerOnly), + 3 => Some(RuntimePlayerConditionTestScope::AiPlayersOnly), + 4 => Some(RuntimePlayerConditionTestScope::HumanPlayersOnly), + _ => None, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/conditions/row_decode.rs b/crates/rrt-runtime/src/inspect/smp/events/conditions/row_decode.rs new file mode 100644 index 0000000..9272f95 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/conditions/row_decode.rs @@ -0,0 +1,370 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn parse_real_condition_row_summary( + row_bytes: &[u8], + row_index: usize, + candidate_name: Option, +) -> Option { + let raw_condition_id = read_u32_at(row_bytes, 0)? as i32; + let subtype = read_u8_at(row_bytes, 4)?; + let flag_bytes = row_bytes + .get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)? + .to_vec(); + let candidate_name_display = candidate_name.clone(); + let candidate_name_ref = candidate_name_display.as_deref(); + let ordinary_metadata = real_ordinary_condition_metadata(raw_condition_id); + let comparator = ordinary_metadata + .and_then(|_| decode_real_condition_comparator(subtype)) + .map(condition_comparator_label); + let metric = ordinary_metadata + .map(|metadata| real_ordinary_condition_metric_label(metadata, candidate_name_ref)); + let threshold = ordinary_metadata.and_then(|_| decode_real_condition_threshold(&flag_bytes)); + let requires_candidate_name_binding = ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::Numeric( + RealOrdinaryConditionMetric::Territory(_) + | RealOrdinaryConditionMetric::CompanyTerritory(_) + ) + ) && candidate_name.is_some() + }); + let mut notes = Vec::new(); + if raw_condition_id < 0 { + notes.push("negative sentinel-style condition row id".to_string()); + } + if candidate_name.is_some() { + notes.push("condition row carries candidate-name side string".to_string()); + } + if ordinary_metadata.is_none() && raw_condition_id >= 0 { + notes.push( + "ordinary condition id is not yet recovered in the checked-in condition table" + .to_string(), + ); + } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) + ) && candidate_name.is_none() + }) { + notes.push( + "candidate-availability condition row is missing its candidate-name side string" + .to_string(), + ); + } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability + | RealWorldConditionKind::NamedLocomotiveCost + ) + ) && candidate_name.is_none() + }) { + notes.push("named locomotive condition row is missing its side-string binding".to_string()); + } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) + ) && candidate_name.is_none() + }) { + notes.push( + "named cargo-production condition row is missing its side-string binding".to_string(), + ); + } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FactoryProductionTotal + | RealWorldConditionKind::FarmMineProductionTotal + | RealWorldConditionKind::OtherCargoProductionTotal + ) + ) + }) { + notes.push( + "checked-in RT3.lng label is known, but this cargo aggregate condition family is not yet lowered" + .to_string(), + ); + } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) + ) && candidate_name_ref + .and_then(recovered_cargo_production_slot_from_condition_name) + .is_none() + }) { + notes.push( + "named cargo-production condition side string does not yet map to a checked-in cargo slot" + .to_string(), + ); + } + Some(SmpLoadedPackedEventConditionRowSummary { + row_index, + raw_condition_id, + subtype, + flag_bytes, + candidate_name, + comparator, + metric, + semantic_family: ordinary_metadata + .map(|metadata| real_ordinary_condition_semantic_family(metadata).to_string()), + semantic_preview: ordinary_metadata.and_then(|metadata| { + threshold.map(|value| { + let comparator_text = decode_real_condition_comparator(subtype) + .map(condition_comparator_symbol) + .unwrap_or("?"); + let metric_label = + real_ordinary_condition_metric_label(metadata, candidate_name_ref); + format!("Test {} {} {}", metric_label, comparator_text, value) + }) + }), + recovered_cargo_slot: ordinary_metadata.and_then(|metadata| match metadata.kind { + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { + candidate_name_ref.and_then(recovered_cargo_production_slot_from_condition_name) + } + _ => None, + }), + recovered_cargo_class: ordinary_metadata.and_then(|metadata| match metadata.kind { + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { + candidate_name_ref + .and_then(recovered_cargo_production_slot_from_condition_name) + .and_then(known_cargo_slot_definition) + .map(|definition| runtime_cargo_class_name(definition.cargo_class).to_string()) + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FactoryProductionTotal, + ) => Some("factory".to_string()), + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FarmMineProductionTotal, + ) => Some("farm_mine".to_string()), + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::OtherCargoProductionTotal, + ) => Some("other".to_string()), + _ => None, + }), + requires_candidate_name_binding, + notes, + }) +} + +pub(in crate::inspect::smp) fn decode_real_condition_rows( + rows: &[SmpLoadedPackedEventConditionRowSummary], + negative_sentinel_scope: Option<&SmpLoadedPackedEventNegativeSentinelScopeSummary>, +) -> Vec { + rows.iter() + .filter(|row| row.raw_condition_id >= 0) + .filter_map(|row| decode_real_condition_row(row, negative_sentinel_scope)) + .collect() +} + +pub(in crate::inspect::smp) fn decode_real_condition_row( + row: &SmpLoadedPackedEventConditionRowSummary, + negative_sentinel_scope: Option<&SmpLoadedPackedEventNegativeSentinelScopeSummary>, +) -> Option { + let metadata = real_ordinary_condition_metadata(row.raw_condition_id)?; + let comparator = decode_real_condition_comparator(row.subtype)?; + let value = decode_real_condition_threshold(&row.flag_bytes)?; + match metadata.kind { + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(index)) => { + Some(RuntimeCondition::WorldVariableThreshold { + index, + comparator, + value, + }) + } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(metric)) => { + Some(RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric, + comparator, + value, + }) + } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(index)) => { + Some(RuntimeCondition::CompanyVariableThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + index, + comparator, + value, + }) + } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(index)) => { + negative_sentinel_scope.and_then(|scope| { + real_condition_player_target(scope).map(|target| { + RuntimeCondition::PlayerVariableThreshold { + target, + index, + comparator, + value, + } + }) + }) + } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(metric)) => { + negative_sentinel_scope.and_then(|scope| { + real_condition_chairman_target(scope).map(|target| { + RuntimeCondition::ChairmanNumericThreshold { + target, + metric, + comparator, + value, + } + }) + }) + } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable( + index, + )) => negative_sentinel_scope + .filter(|scope| scope.territory_scope_selector_is_0x63) + .map(|_| RuntimeCondition::TerritoryVariableThreshold { + target: RuntimeTerritoryTarget::AllTerritories, + index, + comparator, + value, + }), + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => { + negative_sentinel_scope + .filter(|scope| scope.territory_scope_selector_is_0x63) + .map(|_| RuntimeCondition::TerritoryNumericThreshold { + target: RuntimeTerritoryTarget::AllTerritories, + metric, + comparator, + value, + }) + } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + metric, + )) => negative_sentinel_scope + .filter(|scope| scope.territory_scope_selector_is_0x63) + .map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + territory: RuntimeTerritoryTarget::AllTerritories, + metric, + comparator, + value, + }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition { + label, + }) => Some(RuntimeCondition::SpecialConditionThreshold { + label: label.to_string(), + comparator, + value, + }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => row + .candidate_name + .as_ref() + .map(|name| RuntimeCondition::CandidateAvailabilityThreshold { + name: name.clone(), + comparator, + value, + }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { + row.candidate_name.as_ref().and_then(|name| { + recovered_cargo_production_slot_from_condition_name(name).map(|slot| { + let label = known_cargo_slot_definition(slot) + .map(|definition| definition.label.to_string()) + .unwrap_or_else(|| name.clone()); + RuntimeCondition::CargoProductionSlotThreshold { + slot, + label, + comparator, + value, + } + }) + }) + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability, + ) => row.candidate_name.as_ref().map(|name| { + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: name.clone(), + comparator, + value, + } + }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => row + .candidate_name + .as_ref() + .map(|name| RuntimeCondition::NamedLocomotiveCostThreshold { + name: name.clone(), + comparator, + value, + }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => { + Some(RuntimeCondition::CargoProductionTotalThreshold { comparator, value }) + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal) => { + Some(RuntimeCondition::FactoryProductionTotalThreshold { comparator, value }) + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FarmMineProductionTotal) => { + Some(RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value }) + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::OtherCargoProductionTotal, + ) => Some(RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value }), + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::LimitedTrackBuildingAmount, + ) => Some(RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => { + Some(RuntimeCondition::TerritoryAccessCostThreshold { comparator, value }) + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => { + Some(RuntimeCondition::EconomicStatusCodeThreshold { comparator, value }) + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { key }) => { + decode_world_flag_condition(comparator, value, key) + } + } +} + +pub(in crate::inspect::smp) fn decode_world_flag_condition( + comparator: RuntimeConditionComparator, + value: i64, + key: &'static str, +) -> Option { + let bool_value = match (comparator, value) { + (RuntimeConditionComparator::Eq, 0) | (RuntimeConditionComparator::Ne, 1) => false, + (RuntimeConditionComparator::Eq, 1) | (RuntimeConditionComparator::Ne, 0) => true, + _ => return None, + }; + Some(RuntimeCondition::WorldFlagEquals { + key: key.to_string(), + value: bool_value, + }) +} + +pub(in crate::inspect::smp) fn real_condition_chairman_target( + scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, +) -> Option { + match scope.player_test_scope { + RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimeChairmanTarget::AllActive), + RuntimePlayerConditionTestScope::SelectedPlayerOnly => { + Some(RuntimeChairmanTarget::SelectedChairman) + } + RuntimePlayerConditionTestScope::AiPlayersOnly => Some(RuntimeChairmanTarget::AiChairmen), + RuntimePlayerConditionTestScope::HumanPlayersOnly => { + Some(RuntimeChairmanTarget::HumanChairmen) + } + RuntimePlayerConditionTestScope::Disabled => None, + } +} + +pub(in crate::inspect::smp) fn real_condition_player_target( + scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, +) -> Option { + match scope.player_test_scope { + RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimePlayerTarget::AllActive), + RuntimePlayerConditionTestScope::SelectedPlayerOnly => { + Some(RuntimePlayerTarget::SelectedPlayer) + } + RuntimePlayerConditionTestScope::AiPlayersOnly => Some(RuntimePlayerTarget::AiPlayers), + RuntimePlayerConditionTestScope::HumanPlayersOnly => { + Some(RuntimePlayerTarget::HumanPlayers) + } + RuntimePlayerConditionTestScope::Disabled => None, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/descriptors/cargo.rs b/crates/rrt-runtime/src/inspect/smp/events/descriptors/cargo.rs new file mode 100644 index 0000000..a844032 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/descriptors/cargo.rs @@ -0,0 +1,211 @@ +use super::super::super::*; +use crate::inspect::smp::world::known_cargo_slot_definition_for_descriptor_id; +use std::collections::BTreeMap; +use std::sync::OnceLock; + +pub(in crate::inspect::smp) const GROUNDED_NAMED_CARGO_PRODUCTION_LABELS: [(&str, &str); 50] = [ + ("Alcohol", "Alcohol Production"), + ("Aluminum", "Aluminum Production"), + ("Ammunition", "Ammunition Production"), + ("Automobiles", "Automobiles Production"), + ("Bauxite", "Bauxite Production"), + ("Ceramics", "Ceramics Production"), + ("Cheese", "Cheese Production"), + ("Chemicals", "Chemicals Production"), + ("Clothing", "Clothing Production"), + ("Coal", "Coal Production"), + ("Coffee", "Coffee Production"), + ("Concrete", "Concrete Production"), + ("Corn", "Corn Production"), + ("Cotton", "Cotton Production"), + ("Crystals", "Crystals Production"), + ("Diesel", "Diesel Production"), + ("Dye", "Dye Production"), + ("Electronics", "Electronics Production"), + ("Fertilizer", "Fertilizer Production"), + ("Furniture", "Furniture Production"), + ("Goods", "Goods Production"), + ("Grain", "Grain Production"), + ("Ingots", "Ingots Production"), + ("Iron", "Iron Production"), + ("Livestock", "Livestock Production"), + ("Logs", "Logs Production"), + ("Lumber", "Lumber Production"), + ("Machinery", "Machinery Production"), + ("Mail", "Mail Production"), + ("Meat", "Meat Production"), + ("Medicine", "Medicine Production"), + ("Milk", "Milk Production"), + ("Oil", "Oil Production"), + ("Ore", "Ore Production"), + ("Paper", "Paper Production"), + ("Passengers", "Passengers Production"), + ("Plastic", "Plastic Production"), + ("Produce", "Produce Production"), + ("Pulpwood", "Pulpwood Production"), + ("Rice", "Rice Production"), + ("Rubber", "Rubber Production"), + ("Steel", "Steel Production"), + ("Sugar", "Sugar Production"), + ("Tires", "Tires Production"), + ("Toys", "Toys Production"), + ("Troops", "Troops Production"), + ("Uranium", "Uranium Production"), + ("Waste", "Waste Production"), + ("Weapons", "Weapons Production"), + ("Wool", "Wool Production"), +]; + +#[derive(Debug, Deserialize)] +pub(in crate::inspect::smp) struct CheckedInCargoBindingsArtifact { + bindings: Vec, +} + +#[derive(Debug, Deserialize)] +pub(in crate::inspect::smp) struct CheckedInCargoBindingRow { + descriptor_id: u32, + band: String, + cargo_name: String, +} + +pub(in crate::inspect::smp) fn recovered_cargo_price_descriptor_metadata( + descriptor_id: u32, +) -> Option { + (descriptor_id == 105).then_some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: "All Cargo Prices", + target_mask_bits: 0x08, + parameter_family: "cargo_price_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }) +} + +pub(in crate::inspect::smp) fn recovered_cargo_economics_descriptor_metadata( + descriptor_id: u32, +) -> Option { + match descriptor_id { + 177 => Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: "All Cargo Production", + target_mask_bits: 0x08, + parameter_family: "cargo_production_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }), + 178 => Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: "All Factory Production", + target_mask_bits: 0x08, + parameter_family: "cargo_production_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }), + 179 => Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: "All Farm/Mine Production", + target_mask_bits: 0x08, + parameter_family: "cargo_production_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }), + _ => None, + } +} + +pub(in crate::inspect::smp) fn grounded_named_cargo_price_bindings() +-> &'static BTreeMap { + static BINDINGS: OnceLock> = OnceLock::new(); + BINDINGS.get_or_init(|| { + let artifact: CheckedInCargoBindingsArtifact = serde_json::from_str(include_str!( + "../../../../../../../artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json" + )) + .expect("checked-in cargo bindings artifact should parse"); + artifact + .bindings + .into_iter() + .filter(|binding| binding.band == "cargo_price_named") + .map(|binding| { + let cargo_name = Box::leak(binding.cargo_name.into_boxed_str()) as &'static str; + let descriptor_label = + Box::leak(format!("{cargo_name} Price").into_boxed_str()) as &'static str; + (binding.descriptor_id, (cargo_name, descriptor_label)) + }) + .collect() + }) +} + +pub(crate) fn grounded_named_cargo_price_label(descriptor_id: u32) -> Option<&'static str> { + grounded_named_cargo_price_bindings() + .get(&descriptor_id) + .map(|(cargo_label, _)| *cargo_label) +} + +pub(in crate::inspect::smp) fn grounded_named_cargo_production_label( + descriptor_id: u32, +) -> Option<&'static str> { + let index = descriptor_id.checked_sub(180)? as usize; + GROUNDED_NAMED_CARGO_PRODUCTION_LABELS + .get(index) + .map(|(cargo_label, _)| *cargo_label) +} + +pub(in crate::inspect::smp) fn grounded_named_cargo_production_descriptor_label( + descriptor_id: u32, +) -> Option<&'static str> { + let index = descriptor_id.checked_sub(180)? as usize; + GROUNDED_NAMED_CARGO_PRODUCTION_LABELS + .get(index) + .map(|(_, descriptor_label)| *descriptor_label) +} + +pub(in crate::inspect::smp) fn recovered_cargo_production_descriptor_metadata( + descriptor_id: u32, +) -> Option { + if let Some(label) = grounded_named_cargo_production_descriptor_label(descriptor_id) { + return Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label, + target_mask_bits: 0x08, + parameter_family: "cargo_production_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }); + } + recovered_cargo_production_label(descriptor_id).map(|label| { + RealGroupedEffectDescriptorMetadata { + descriptor_id, + label, + target_mask_bits: 0x08, + parameter_family: "cargo_production_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + } + }) +} + +pub(in crate::inspect::smp) fn recovered_cargo_production_slot(descriptor_id: u32) -> Option { + let slot = descriptor_id.checked_sub(229)?; + (1..=11).contains(&slot).then_some(slot) +} + +pub(in crate::inspect::smp) fn recovered_cargo_production_label( + descriptor_id: u32, +) -> Option<&'static str> { + known_cargo_slot_definition_for_descriptor_id(descriptor_id).map(|definition| definition.label) +} + +pub(in crate::inspect::smp) fn recovered_cargo_production_slot_from_condition_name( + name: &str, +) -> Option { + KNOWN_CARGO_SLOT_DEFINITIONS + .iter() + .find(|definition| definition.label == name) + .map(|definition| definition.slot_id) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/descriptors/classification.rs b/crates/rrt-runtime/src/inspect/smp/events/descriptors/classification.rs new file mode 100644 index 0000000..5bd54d4 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/descriptors/classification.rs @@ -0,0 +1,73 @@ +pub(in crate::inspect::smp) fn classify_real_grouped_effect_semantic_family( + opcode: u8, + raw_scalar_value: i32, + value_byte_0x11: u8, + value_byte_0x12: u8, + value_word_0x14: u16, + value_word_0x16: u16, +) -> &'static str { + if opcode == 8 { + return "multivalue_scalar"; + } + if value_byte_0x11 != 0 || value_byte_0x12 != 0 || value_word_0x14 != 0 || value_word_0x16 != 0 + { + return "timed_duration"; + } + if raw_scalar_value == 0 || raw_scalar_value == 1 { + return "bool_toggle"; + } + "scalar_assignment" +} + +pub(in crate::inspect::smp) fn classify_real_grouped_effect_row_shape( + opcode: u8, + raw_scalar_value: i32, + value_byte_0x11: u8, + value_byte_0x12: u8, + value_word_0x14: u16, + value_word_0x16: u16, +) -> &'static str { + if opcode == 8 { + return "multivalue_scalar"; + } + if value_byte_0x11 != 0 || value_byte_0x12 != 0 || value_word_0x14 != 0 || value_word_0x16 != 0 + { + return "timed_duration"; + } + if raw_scalar_value == 0 || raw_scalar_value == 1 { + return "bool_toggle"; + } + "scalar_assignment" +} + +pub(in crate::inspect::smp) fn build_real_grouped_effect_semantic_preview( + descriptor_label: Option<&str>, + semantic_family: &str, + raw_scalar_value: i32, + value_byte_0x11: u8, + value_byte_0x12: u8, + value_word_0x14: u16, + value_word_0x16: u16, +) -> String { + let label = descriptor_label.unwrap_or("descriptor"); + match semantic_family { + "bool_toggle" => { + let state = if raw_scalar_value == 0 { + "FALSE" + } else { + "TRUE" + }; + format!("Set {label} to {state}") + } + "building_spawn_batch" => format!( + "Batch place {label} with scalar {raw_scalar_value}, count {value_byte_0x11}, and span words [{value_word_0x14}, {value_word_0x16}]" + ), + "timed_duration" => format!( + "Set {label} to {raw_scalar_value} for {value_word_0x14} years {value_word_0x16} months" + ), + "multivalue_scalar" => format!( + "Set {label} to {raw_scalar_value} with aux [{value_byte_0x11}, {value_byte_0x12}, {value_word_0x14}, {value_word_0x16}]" + ), + _ => format!("Set {label} to {raw_scalar_value}"), + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/descriptors/locomotives.rs b/crates/rrt-runtime/src/inspect/smp/events/descriptors/locomotives.rs new file mode 100644 index 0000000..683c740 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/descriptors/locomotives.rs @@ -0,0 +1,207 @@ +use super::super::super::*; +use std::collections::BTreeMap; +use std::sync::OnceLock; + +pub(in crate::inspect::smp) fn recovered_locomotive_availability_descriptor_metadata( + descriptor_id: u32, +) -> Option { + if let Some(loco_id) = recovered_locomotive_availability_loco_id(descriptor_id) { + let label = recovered_locomotive_availability_label(loco_id); + let executable_in_runtime = (loco_id as usize) <= GROUNDED_LOCOMOTIVE_PREFIX.len(); + return Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label, + target_mask_bits: 0x08, + parameter_family: "locomotive_availability_scalar", + runtime_key: None, + runtime_status: if executable_in_runtime { + RealGroupedEffectRuntimeStatus::Executable + } else { + RealGroupedEffectRuntimeStatus::EvidenceBlocked + }, + executable_in_runtime, + }); + } + (457..=474) + .contains(&descriptor_id) + .then(|| RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: upper_band_locomotive_availability_label(descriptor_id), + target_mask_bits: 0x08, + parameter_family: "locomotive_availability_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::EvidenceBlocked, + executable_in_runtime: false, + }) +} + +pub(in crate::inspect::smp) fn recovered_locomotive_availability_loco_id( + descriptor_id: u32, +) -> Option { + if (241..=351).contains(&descriptor_id) { + return Some(descriptor_id - 240); + } + None +} + +pub(in crate::inspect::smp) fn grounded_locomotive_name(loco_id: u32) -> Option<&'static str> { + let index = loco_id.checked_sub(1)? as usize; + GROUNDED_LOCOMOTIVE_PREFIX.get(index).copied() +} + +pub(in crate::inspect::smp) fn recovered_locomotive_availability_label( + loco_id: u32, +) -> &'static str { + static LABELS: OnceLock> = OnceLock::new(); + LABELS + .get_or_init(|| { + (1..=111) + .map(|loco_id| { + let label = grounded_locomotive_name(loco_id) + .map(|name| format!("{name} Availability")) + .unwrap_or_else(|| { + format!("Lower-Band Locomotive Availability Slot {loco_id}") + }); + (loco_id, Box::leak(label.into_boxed_str()) as &'static str) + }) + .collect() + }) + .get(&loco_id) + .copied() + .expect("lower-band locomotive availability label should exist") +} + +pub(in crate::inspect::smp) fn upper_band_locomotive_availability_label( + descriptor_id: u32, +) -> &'static str { + static LABELS: OnceLock> = OnceLock::new(); + LABELS + .get_or_init(|| { + (457..=474) + .map(|descriptor_id| { + let label = format!( + "Upper-Band Locomotive Availability Slot {}", + descriptor_id - 456 + ); + ( + descriptor_id, + Box::leak(label.into_boxed_str()) as &'static str, + ) + }) + .collect() + }) + .get(&descriptor_id) + .copied() + .expect("upper-band locomotive availability label should exist") +} + +pub(in crate::inspect::smp) fn recovered_locomotive_cost_loco_id( + descriptor_id: u32, +) -> Option { + if (352..=451).contains(&descriptor_id) { + return Some(descriptor_id - 351); + } + None +} + +pub(in crate::inspect::smp) fn recovered_locomotive_cost_label( + descriptor_id: u32, +) -> Option<&'static str> { + static LABELS: OnceLock> = OnceLock::new(); + LABELS + .get_or_init(|| { + (352..=451) + .filter_map(|descriptor_id| { + recovered_locomotive_cost_loco_id(descriptor_id).map(|loco_id| { + let label = grounded_locomotive_name(loco_id) + .map(|name| format!("{name} Cost")) + .unwrap_or_else(|| { + format!("Lower-Band Locomotive Cost Slot {loco_id}") + }); + let label = Box::leak(label.into_boxed_str()) as &'static str; + (descriptor_id, label) + }) + }) + .collect() + }) + .get(&descriptor_id) + .copied() + .or_else(|| upper_band_locomotive_cost_label(descriptor_id)) +} + +pub(in crate::inspect::smp) fn upper_band_locomotive_cost_label( + descriptor_id: u32, +) -> Option<&'static str> { + static LABELS: OnceLock> = OnceLock::new(); + LABELS + .get_or_init(|| { + (475..=502) + .map(|descriptor_id| { + let label = format!("Upper-Band Locomotive Cost Slot {}", descriptor_id - 474); + ( + descriptor_id, + Box::leak(label.into_boxed_str()) as &'static str, + ) + }) + .collect() + }) + .get(&descriptor_id) + .copied() +} + +pub(in crate::inspect::smp) fn recovered_locomotive_cost_descriptor_metadata( + descriptor_id: u32, +) -> Option { + recovered_locomotive_cost_label(descriptor_id).map(|label| { + let executable_in_runtime = recovered_locomotive_cost_loco_id(descriptor_id) + .is_some_and(|loco_id| (loco_id as usize) <= GROUNDED_LOCOMOTIVE_PREFIX.len()); + RealGroupedEffectDescriptorMetadata { + descriptor_id, + label, + target_mask_bits: 0x08, + parameter_family: "locomotive_cost_scalar", + runtime_key: None, + runtime_status: if executable_in_runtime { + RealGroupedEffectRuntimeStatus::Executable + } else { + RealGroupedEffectRuntimeStatus::EvidenceBlocked + }, + executable_in_runtime, + } + }) +} + +pub(in crate::inspect::smp) fn recovered_locomotive_policy_descriptor_metadata( + descriptor_id: u32, +) -> Option { + match descriptor_id { + 454 => Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: "All Steam Locos Avail.", + target_mask_bits: 0x08, + parameter_family: "world_flag_toggle", + runtime_key: Some("world.all_steam_locos_available"), + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }), + 455 => Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: "All Diesel Locos Avail.", + target_mask_bits: 0x08, + parameter_family: "world_flag_toggle", + runtime_key: Some("world.all_diesel_locos_available"), + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }), + 456 => Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: "All Electric Locos Avail.", + target_mask_bits: 0x08, + parameter_family: "world_flag_toggle", + runtime_key: Some("world.all_electric_locos_available"), + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }), + _ => None, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/descriptors/metadata.rs b/crates/rrt-runtime/src/inspect/smp/events/descriptors/metadata.rs new file mode 100644 index 0000000..97a3c1b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/descriptors/metadata.rs @@ -0,0 +1,52 @@ +use super::super::super::*; +use super::cargo::{ + recovered_cargo_economics_descriptor_metadata, recovered_cargo_price_descriptor_metadata, + recovered_cargo_production_descriptor_metadata, +}; +use super::locomotives::{ + recovered_locomotive_availability_descriptor_metadata, + recovered_locomotive_cost_descriptor_metadata, recovered_locomotive_policy_descriptor_metadata, +}; +use super::special_conditions::{ + special_condition_world_scalar_descriptor_metadata, + special_condition_world_toggle_descriptor_metadata, +}; + +pub(in crate::inspect::smp) fn real_grouped_effect_descriptor_metadata( + descriptor_id: u32, +) -> Option { + recovered_cargo_price_descriptor_metadata(descriptor_id) + .or_else(|| recovered_cargo_economics_descriptor_metadata(descriptor_id)) + .or_else(|| recovered_cargo_production_descriptor_metadata(descriptor_id)) + .or_else(|| recovered_locomotive_availability_descriptor_metadata(descriptor_id)) + .or_else(|| recovered_locomotive_cost_descriptor_metadata(descriptor_id)) + .or_else(|| recovered_territory_access_cost_descriptor_metadata(descriptor_id)) + .or_else(|| recovered_locomotive_policy_descriptor_metadata(descriptor_id)) + .or_else(|| special_condition_world_scalar_descriptor_metadata(descriptor_id)) + .or_else(|| special_condition_world_toggle_descriptor_metadata(descriptor_id)) + .or_else(|| { + REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA + .iter() + .copied() + .find(|metadata| metadata.descriptor_id == descriptor_id) + }) + .or_else(|| { + checked_in_event_effect_descriptor_rows() + .get(&descriptor_id) + .copied() + }) +} + +fn recovered_territory_access_cost_descriptor_metadata( + descriptor_id: u32, +) -> Option { + (descriptor_id == 453).then_some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: "Territory Access Cost", + target_mask_bits: 0x08, + parameter_family: "territory_access_cost_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/descriptors/mod.rs b/crates/rrt-runtime/src/inspect/smp/events/descriptors/mod.rs new file mode 100644 index 0000000..552b0dd --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/descriptors/mod.rs @@ -0,0 +1,7 @@ +pub(super) mod cargo; +pub(super) mod classification; +pub(super) mod locomotives; +pub(super) mod metadata; +pub(super) mod runtime_keys; +pub(super) mod special_conditions; +pub(super) mod targets; diff --git a/crates/rrt-runtime/src/inspect/smp/events/descriptors/runtime_keys.rs b/crates/rrt-runtime/src/inspect/smp/events/descriptors/runtime_keys.rs new file mode 100644 index 0000000..c578e10 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/descriptors/runtime_keys.rs @@ -0,0 +1,72 @@ +use super::super::super::*; + +pub(in crate::inspect::smp) fn runtime_candidate_availability_name(label: &str) -> String { + label + .strip_suffix(" Availability") + .unwrap_or(label) + .to_string() +} + +pub(in crate::inspect::smp) fn runtime_world_flag_key( + descriptor_metadata: RealGroupedEffectDescriptorMetadata, +) -> Option { + descriptor_metadata + .runtime_key + .map(str::to_string) + .or_else(|| { + (descriptor_metadata.parameter_family == "world_flag_toggle") + .then(|| runtime_world_flag_key_from_label(descriptor_metadata.label)) + }) +} + +pub(crate) fn runtime_world_flag_key_from_label(label: &str) -> String { + normalize_runtime_world_key(label) +} + +pub(in crate::inspect::smp) fn runtime_world_scalar_key( + descriptor_metadata: RealGroupedEffectDescriptorMetadata, +) -> Option { + descriptor_metadata + .runtime_key + .map(str::to_string) + .or_else(|| { + (descriptor_metadata.parameter_family == "world_scalar_override") + .then(|| normalize_runtime_world_key(descriptor_metadata.label)) + }) +} + +pub(crate) fn runtime_world_scalar_key_from_label(label: &str) -> String { + normalize_runtime_world_key(label) +} + +pub(in crate::inspect::smp) fn normalize_runtime_world_key(label: &str) -> String { + let mut key = String::with_capacity(label.len() + 6); + key.push_str("world."); + let mut last_was_underscore = false; + for ch in label.chars() { + if ch.is_ascii_alphanumeric() { + key.push(ch.to_ascii_lowercase()); + last_was_underscore = false; + } else if !last_was_underscore { + key.push('_'); + last_was_underscore = true; + } + } + while key.ends_with('_') { + key.pop(); + } + key +} + +pub(in crate::inspect::smp) fn real_grouped_company_governance_metric( + descriptor_metadata: RealGroupedEffectDescriptorMetadata, +) -> Option { + match descriptor_metadata.label { + "Credit Rating" => Some(RuntimeCompanyMetric::CreditRating), + "Prime Rate" => Some(RuntimeCompanyMetric::PrimeRate), + "Book Value Per Share" => Some(RuntimeCompanyMetric::BookValuePerShare), + "Investor Confidence" => Some(RuntimeCompanyMetric::InvestorConfidence), + "Management Attitude" => Some(RuntimeCompanyMetric::ManagementAttitude), + _ => None, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/descriptors/special_conditions.rs b/crates/rrt-runtime/src/inspect/smp/events/descriptors/special_conditions.rs new file mode 100644 index 0000000..54beda9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/descriptors/special_conditions.rs @@ -0,0 +1,45 @@ +use super::super::super::*; + +pub(in crate::inspect::smp) fn special_condition_world_scalar_descriptor_metadata( + descriptor_id: u32, +) -> Option { + let slot_index = descriptor_id.checked_sub(110)? as usize; + if slot_index != 12 { + return None; + } + let definition = KNOWN_SPECIAL_CONDITION_DEFINITIONS.get(slot_index)?; + if definition.hidden { + return None; + } + Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: definition.label, + target_mask_bits: 0x08, + parameter_family: "world_track_build_limit_scalar", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }) +} + +pub(in crate::inspect::smp) fn special_condition_world_toggle_descriptor_metadata( + descriptor_id: u32, +) -> Option { + let slot_index = descriptor_id.checked_sub(110)? as usize; + if !(1..=34).contains(&slot_index) || matches!(slot_index, 12 | 31) { + return None; + } + let definition = KNOWN_SPECIAL_CONDITION_DEFINITIONS.get(slot_index)?; + if definition.hidden { + return None; + } + Some(RealGroupedEffectDescriptorMetadata { + descriptor_id, + label: definition.label, + target_mask_bits: 0x08, + parameter_family: "world_flag_toggle", + runtime_key: None, + runtime_status: RealGroupedEffectRuntimeStatus::Executable, + executable_in_runtime: true, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/descriptors/targets.rs b/crates/rrt-runtime/src/inspect/smp/events/descriptors/targets.rs new file mode 100644 index 0000000..e4eaf4d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/descriptors/targets.rs @@ -0,0 +1,108 @@ +use crate::inspect::smp::events::{ + RealGroupedTargetSubject, SmpLoadedPackedEventCompactControlSummary, + SmpLoadedPackedEventGroupedEffectRowSummary, +}; + +pub(in crate::inspect::smp) fn derive_real_grouped_target_subject( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + compact_control: &SmpLoadedPackedEventCompactControlSummary, +) -> Option { + if row.parameter_family.as_deref() == Some("company_governance_scalar") { + return Some(RealGroupedTargetSubject::Company); + } + if row.parameter_family.as_deref() == Some("world_scalar_override") { + return Some(RealGroupedTargetSubject::WholeGame); + } + match row.target_mask_bits { + Some(0x08) => Some(RealGroupedTargetSubject::WholeGame), + Some(0x01) => Some(RealGroupedTargetSubject::Company), + Some(0x04) => Some(RealGroupedTargetSubject::Territory), + Some(0x02) => match compact_control + .grouped_scope_checkboxes_0x7ff + .get(row.group_index) + .copied() + { + Some(2) => Some(RealGroupedTargetSubject::Chairman), + _ => Some(RealGroupedTargetSubject::Player), + }, + _ if row.descriptor_id == 3 => Some(RealGroupedTargetSubject::Territory), + _ if row.descriptor_id == 15 + && compact_control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .is_some_and(|selector| *selector >= 0) => + { + Some(RealGroupedTargetSubject::Territory) + } + _ => None, + } +} + +pub(in crate::inspect::smp) fn real_grouped_target_subject_name( + subject: RealGroupedTargetSubject, +) -> &'static str { + match subject { + RealGroupedTargetSubject::Company => "company", + RealGroupedTargetSubject::Player => "player", + RealGroupedTargetSubject::Chairman => "chairman", + RealGroupedTargetSubject::Territory => "territory", + RealGroupedTargetSubject::WholeGame => "whole_game", + } +} + +pub(in crate::inspect::smp) fn derive_real_grouped_target_scope_name( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + compact_control: &SmpLoadedPackedEventCompactControlSummary, + target_subject: Option, + target_scope_ordinal: Option, +) -> Option { + match target_subject { + Some(RealGroupedTargetSubject::Company) => target_scope_ordinal + .map(real_grouped_company_scope_name) + .map(str::to_string), + Some(RealGroupedTargetSubject::Player) => target_scope_ordinal + .map(real_grouped_player_scope_name) + .map(str::to_string), + Some(RealGroupedTargetSubject::Chairman) => target_scope_ordinal + .map(real_grouped_chairman_scope_name) + .map(str::to_string), + Some(RealGroupedTargetSubject::Territory) => compact_control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .copied() + .filter(|selector| *selector >= 0) + .map(|_| "specified_territories".to_string()), + Some(RealGroupedTargetSubject::WholeGame) => Some("whole_game".to_string()), + None => None, + } +} + +pub(in crate::inspect::smp) fn real_grouped_company_scope_name(ordinal: u8) -> &'static str { + match ordinal { + 0 => "condition_true_company", + 1 => "selected_company", + 2 => "human_companies", + 3 => "ai_companies", + _ => "unsupported_company_scope", + } +} + +pub(in crate::inspect::smp) fn real_grouped_player_scope_name(ordinal: u8) -> &'static str { + match ordinal { + 0 => "condition_true_player", + 1 => "selected_player", + 2 => "human_players", + 3 => "ai_players", + _ => "unsupported_player_scope", + } +} + +pub(in crate::inspect::smp) fn real_grouped_chairman_scope_name(ordinal: u8) -> &'static str { + match ordinal { + 0 => "condition_true_chairman", + 1 => "selected_chairman", + 2 => "human_chairmen", + 3 => "ai_chairmen", + _ => "unsupported_chairman_scope", + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/mod.rs b/crates/rrt-runtime/src/inspect/smp/events/mod.rs new file mode 100644 index 0000000..3f5c4e2 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/mod.rs @@ -0,0 +1,80 @@ +use super::world::{known_cargo_slot_definition, runtime_cargo_class_name}; +use crate::inspect::smp::catalog::conditions::{ + RealOrdinaryConditionKind, RealOrdinaryConditionMetadata, RealOrdinaryConditionMetric, + RealWorldConditionKind, real_ordinary_condition_metadata, +}; +use crate::inspect::smp::catalog::descriptors::{ + RealGroupedEffectDescriptorMetadata, RealGroupedEffectRuntimeStatus, + real_grouped_effect_runtime_status_name, +}; +use crate::inspect::smp::catalog::offsets::events::{ + EVENT_RUNTIME_COLLECTION_CLOSE_TAG, EVENT_RUNTIME_COLLECTION_METADATA_TAG, + EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION, EVENT_RUNTIME_COLLECTION_RECORDS_TAG, + PACKED_EVENT_NONDIRECT_CONDITION_ROW_SERIALIZED_LEN, + PACKED_EVENT_NONDIRECT_GROUPED_EFFECT_ROW_SERIALIZED_LEN, + PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN, PACKED_EVENT_REAL_COMPACT_CONTROL_LEN, + PACKED_EVENT_REAL_CONDITION_MARKER, PACKED_EVENT_REAL_CONDITION_ROW_LEN, + PACKED_EVENT_REAL_GROUP_COUNT, PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER, + PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN, PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER, + PACKED_EVENT_RECORD_SYNTHETIC_MAGIC, PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC, + PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC, PACKED_EVENT_TEXT_BAND_LABELS, +}; +use crate::inspect::smp::common::{ + ascii_preview, compact_nondirect_signature_family, find_u16_le_offsets, find_u32_le_offsets, + format_u16_word_signature, read_ascii_c_string_at, read_i32_at, read_i64_at, read_u8_at, + read_u16_at, read_u16_window, read_u32_at, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum RealGroupedTargetSubject { + Company, + Player, + Chairman, + Territory, + WholeGame, +} + +mod actions; +mod collection; +mod compat; +mod conditions; +mod descriptors; +mod model; +mod nondirect; +mod real_records; +mod synthetic; + +pub(super) use actions::*; +pub(super) use collection::live_ids::decode_live_entry_ids_from_tombstone_bitset; +#[cfg(test)] +pub(super) use collection::records::build_unsupported_event_runtime_record_summaries; +pub(super) use collection::summary::parse_event_runtime_collection_summary; +pub(super) use compat::*; +pub(super) use conditions::*; +pub(super) use descriptors::cargo::{ + grounded_named_cargo_production_label, recovered_cargo_production_slot, + recovered_cargo_production_slot_from_condition_name, +}; +pub(super) use descriptors::classification::{ + build_real_grouped_effect_semantic_preview, classify_real_grouped_effect_row_shape, + classify_real_grouped_effect_semantic_family, +}; +pub(super) use descriptors::locomotives::{ + recovered_locomotive_availability_loco_id, recovered_locomotive_cost_loco_id, +}; +pub(super) use descriptors::metadata::real_grouped_effect_descriptor_metadata; +pub(super) use descriptors::runtime_keys::{ + real_grouped_company_governance_metric, runtime_candidate_availability_name, + runtime_world_flag_key, runtime_world_scalar_key, +}; +pub(super) use descriptors::targets::{ + derive_real_grouped_target_scope_name, derive_real_grouped_target_subject, + real_grouped_target_subject_name, +}; +pub(super) use nondirect::*; +pub(super) use real_records::*; +pub(super) use synthetic::*; + +pub(crate) use descriptors::cargo::grounded_named_cargo_price_label; +pub(crate) use descriptors::runtime_keys::runtime_world_scalar_key_from_label; +pub use model::*; diff --git a/crates/rrt-runtime/src/inspect/smp/events/model.rs b/crates/rrt-runtime/src/inspect/smp/events/model.rs new file mode 100644 index 0000000..691eef6 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/model.rs @@ -0,0 +1,195 @@ +use crate::inspect::smp::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedEventRuntimeCollectionSummary { + pub source_kind: String, + pub mechanism_family: String, + pub mechanism_confidence: String, + #[serde(default)] + pub container_profile_family: Option, + pub metadata_tag_offset: usize, + pub records_tag_offset: usize, + pub close_tag_offset: usize, + pub packed_state_version: u32, + pub packed_state_version_hex: String, + pub live_id_bound: u32, + pub live_record_count: usize, + pub live_entry_ids: Vec, + #[serde(default)] + pub decoded_record_count: usize, + #[serde(default)] + pub imported_runtime_record_count: usize, + #[serde(default)] + pub records_with_trigger_kind: usize, + #[serde(default)] + pub records_missing_trigger_kind: usize, + #[serde(default)] + pub nondirect_compact_record_count: usize, + #[serde(default)] + pub nondirect_compact_records_missing_trigger_kind: usize, + #[serde(default)] + pub trigger_kinds_present: Vec, + #[serde(default)] + pub add_building_dispatch_strip_record_indexes: Vec, + #[serde(default)] + pub add_building_dispatch_strip_descriptor_labels: Vec, + #[serde(default)] + pub add_building_dispatch_strip_records_with_trigger_kind: usize, + #[serde(default)] + pub add_building_dispatch_strip_records_missing_trigger_kind: usize, + #[serde(default)] + pub add_building_dispatch_strip_row_shape_families: Vec, + #[serde(default)] + pub add_building_dispatch_strip_signature_families: Vec, + #[serde(default)] + pub add_building_dispatch_strip_condition_tuple_families: Vec, + #[serde(default)] + pub add_building_dispatch_strip_signature_condition_clusters: Vec, + #[serde(default)] + pub control_lane_notes: Vec, + #[serde(default)] + pub records: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPackedEventRecordSummary { + pub record_index: usize, + pub live_entry_id: u32, + #[serde(default)] + pub payload_offset: Option, + #[serde(default)] + pub payload_len: Option, + pub decode_status: String, + #[serde(default)] + pub payload_family: String, + #[serde(default)] + pub trigger_kind: Option, + #[serde(default)] + pub active: Option, + #[serde(default)] + pub marks_collection_dirty: Option, + #[serde(default)] + pub one_shot: Option, + #[serde(default)] + pub compact_control: Option, + #[serde(default)] + pub text_bands: Vec, + #[serde(default)] + pub standalone_condition_row_count: usize, + #[serde(default)] + pub standalone_condition_rows: Vec, + #[serde(default)] + pub negative_sentinel_scope: Option, + #[serde(default)] + pub grouped_effect_row_counts: Vec, + #[serde(default)] + pub grouped_effect_rows: Vec, + #[serde(default)] + pub decoded_conditions: Vec, + #[serde(default)] + pub decoded_actions: Vec, + #[serde(default)] + pub executable_import_ready: bool, + #[serde(default)] + pub notes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPackedEventNegativeSentinelScopeSummary { + pub company_test_scope: RuntimeCompanyConditionTestScope, + pub player_test_scope: RuntimePlayerConditionTestScope, + pub territory_scope_selector_is_0x63: bool, + #[serde(default)] + pub source_row_indexes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPackedEventCompactControlSummary { + pub mode_byte_0x7ef: u8, + pub primary_selector_0x7f0: u32, + pub grouped_mode_0x7f4: u8, + pub one_shot_header_0x7f5: u32, + pub modifier_flag_0x7f9: u8, + pub modifier_flag_0x7fa: u8, + pub grouped_target_scope_ordinals_0x7fb: Vec, + pub grouped_scope_checkboxes_0x7ff: Vec, + pub summary_toggle_0x800: u8, + pub grouped_territory_selectors_0x80f: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPackedEventTextBandSummary { + pub label: String, + pub packed_len: usize, + pub present: bool, + pub preview: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPackedEventConditionRowSummary { + pub row_index: usize, + pub raw_condition_id: i32, + pub subtype: u8, + #[serde(default)] + pub flag_bytes: Vec, + #[serde(default)] + pub candidate_name: Option, + #[serde(default)] + pub comparator: Option, + #[serde(default)] + pub metric: Option, + #[serde(default)] + pub semantic_family: Option, + #[serde(default)] + pub semantic_preview: Option, + #[serde(default)] + pub recovered_cargo_slot: Option, + #[serde(default)] + pub recovered_cargo_class: Option, + #[serde(default)] + pub requires_candidate_name_binding: bool, + #[serde(default)] + pub notes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPackedEventGroupedEffectRowSummary { + pub group_index: usize, + pub row_index: usize, + pub descriptor_id: u32, + #[serde(default)] + pub descriptor_label: Option, + #[serde(default)] + pub target_mask_bits: Option, + #[serde(default)] + pub parameter_family: Option, + #[serde(default)] + pub grouped_target_subject: Option, + #[serde(default)] + pub grouped_target_scope: Option, + pub opcode: u8, + pub raw_scalar_value: i32, + pub value_byte_0x09: u8, + pub value_dword_0x0d: u32, + pub value_byte_0x11: u8, + pub value_byte_0x12: u8, + pub value_word_0x14: u16, + pub value_word_0x16: u16, + pub row_shape: String, + #[serde(default)] + pub semantic_family: Option, + #[serde(default)] + pub semantic_preview: Option, + #[serde(default)] + pub recovered_cargo_slot: Option, + #[serde(default)] + pub recovered_cargo_class: Option, + #[serde(default)] + pub recovered_cargo_label: Option, + #[serde(default)] + pub recovered_locomotive_id: Option, + #[serde(default)] + pub locomotive_name: Option, + #[serde(default)] + pub notes: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/nondirect.rs b/crates/rrt-runtime/src/inspect/smp/events/nondirect.rs new file mode 100644 index 0000000..b6acedd --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/nondirect.rs @@ -0,0 +1,401 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::map_title::is_ascii_preview_byte; + +pub(in crate::inspect::smp) fn try_parse_nondirect_event_runtime_record_summaries( + records_payload: &[u8], + records_payload_offset: usize, + live_entry_ids: &[u32], +) -> Option> { + let marker_offsets = + find_u32_le_offsets(records_payload, PACKED_EVENT_REAL_CONDITION_MARKER as u32); + if marker_offsets.len() != live_entry_ids.len() || marker_offsets.first().copied() != Some(0) { + return None; + } + + let mut record_offsets = marker_offsets; + record_offsets.push(records_payload.len()); + let mut records = Vec::with_capacity(live_entry_ids.len()); + + for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() { + let start = *record_offsets.get(record_index)?; + let end = *record_offsets.get(record_index + 1)?; + let record_body = records_payload.get(start..end)?; + let record = parse_nondirect_event_runtime_record_summary( + record_body, + records_payload_offset + start, + record_index, + live_entry_id, + ) + .or_else(|| { + build_nondirect_event_runtime_record_summary_from_signatures( + record_body, + records_payload_offset + start, + record_index, + live_entry_id, + ) + })?; + records.push(record); + } + + Some(records) +} + +pub(in crate::inspect::smp) fn parse_nondirect_event_runtime_record_summary( + record_body: &[u8], + payload_offset: usize, + record_index: usize, + live_entry_id: u32, +) -> Option { + let mut cursor = 0usize; + if read_u32_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER as u32 { + return None; + } + cursor += 4; + + let standalone_condition_row_count = usize::try_from(read_u32_at(record_body, cursor)?).ok()?; + cursor += 4; + let mut standalone_condition_rows = Vec::with_capacity(standalone_condition_row_count); + for row_index in 0..standalone_condition_row_count { + let remaining_minimum = standalone_condition_row_count + .checked_sub(row_index + 1)? + .checked_mul(PACKED_EVENT_NONDIRECT_CONDITION_ROW_SERIALIZED_LEN)? + .checked_add(4)? + .checked_add(PACKED_EVENT_REAL_GROUP_COUNT.checked_mul(4)?)? + .checked_add(4)?; + let (row, consumed_len) = parse_nondirect_condition_row_summary( + record_body.get(cursor..)?, + row_index, + remaining_minimum, + )?; + standalone_condition_rows.push(row); + cursor += consumed_len; + } + + let grouped_marker_relative_offset = cursor; + if read_u32_at(record_body, cursor)? != PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER as u32 { + return None; + } + cursor += 4; + + let mut grouped_effect_row_counts = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); + let mut grouped_effect_rows = Vec::new(); + for group_index in 0..PACKED_EVENT_REAL_GROUP_COUNT { + let group_row_count = usize::try_from(read_u32_at(record_body, cursor)?).ok()?; + cursor += 4; + grouped_effect_row_counts.push(group_row_count); + for row_index in 0..group_row_count { + let remaining_groups_minimum = (group_row_count - row_index - 1) + .checked_mul(PACKED_EVENT_NONDIRECT_GROUPED_EFFECT_ROW_SERIALIZED_LEN)? + .checked_add((PACKED_EVENT_REAL_GROUP_COUNT - group_index - 1).checked_mul(4)?)? + .checked_add(4)?; + let (row, consumed_len) = parse_nondirect_grouped_effect_row_summary( + record_body.get(cursor..)?, + group_index, + row_index, + remaining_groups_minimum, + )?; + grouped_effect_rows.push(row); + cursor += consumed_len; + } + } + + let end_marker_relative_offset = cursor; + if read_u32_at(record_body, cursor)? != PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER as u32 { + return None; + } + cursor += 4; + if cursor != record_body.len() { + return None; + } + + let head_signature_words = read_u16_window(record_body, 0, 18); + let post_group_signature_words = + read_u16_window(record_body, grouped_marker_relative_offset + 4, 12); + let ascii_preview_before_grouped_marker = record_body + .get(..grouped_marker_relative_offset) + .map(ascii_preview); + + let mut notes = vec![ + "decoded from compact non-direct 0x4e99/0x4e9a/0x4e9b map-bundle row framing recovered from the paired 0x433060/0x430d70 writer strip and 0x433130/0x42db20 loader strip".to_string(), + format!( + "compact signature family = {}", + compact_nondirect_signature_family( + Some(grouped_marker_relative_offset), + &head_signature_words, + &post_group_signature_words, + ) + ), + format!( + "head signature u16 words = {}", + format_u16_word_signature(&head_signature_words) + ), + format!( + "grouped-effect marker 0x4eb8 at relative offset +0x{grouped_marker_relative_offset:x}" + ), + format!( + "row terminator marker 0x4eb9 at relative offset +0x{end_marker_relative_offset:x}" + ), + ]; + if !post_group_signature_words.is_empty() { + notes.push(format!( + "post-group signature u16 words = {}", + format_u16_word_signature(&post_group_signature_words) + )); + } + if let Some(preview) = ascii_preview_before_grouped_marker { + notes.push(format!("ascii preview before grouped marker = {preview}")); + } + notes.push(format!( + "compact non-direct grouped row counts by group = {:?}", + grouped_effect_row_counts + )); + notes.push( + "the compact non-direct row body reconstructs standalone/grouped rows only; the separate 0x42e050 full-event clone helper is the nearby owner that copies text bands plus control lane [event+0x7ee..0x80f] between live runtime-event rows".to_string(), + ); + notes.push( + "direct disassembly of 0x0042db20 now grounds that absence too: this loader allocates 0x1e-byte standalone-condition nodes and 0x28-byte grouped-row nodes from the 0x4e9a slice, but does not materialize the compact control lane [event+0x7ee..0x80f] or a trigger-kind byte for this non-direct row family".to_string(), + ); + notes.push( + "the adjacent deep-copy seam is bounded too: current direct caller census shows 0x0042e050 reached from editor duplication at 0x004dba23, not from the ordinary 0x00433130 load path, so this non-direct row family is not currently grounded to inherit trigger/control bytes during restore through that helper".to_string(), + ); + notes.push( + "the first non-editor positive control-lane writer is bounded away from ordinary restore too: 0x00430b50 allocates a fresh live runtime-effect row through 0x00432ea0 -> 0x0042d5a0, seeds [event+0x7ef] to 2 or 3 plus adjacent control bytes, and is only reached from the 0x004323a0 follow-on service strip rather than the 0x00433130 nondirect load path".to_string(), + ); + + let decoded_conditions = decode_real_condition_rows(&standalone_condition_rows, None); + + Some(SmpLoadedPackedEventRecordSummary { + record_index, + live_entry_id, + payload_offset: Some(payload_offset), + payload_len: Some(cursor), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_nondirect_compact_v1".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count, + standalone_condition_rows, + negative_sentinel_scope: None, + grouped_effect_row_counts, + grouped_effect_rows, + decoded_conditions, + decoded_actions: Vec::new(), + executable_import_ready: false, + notes, + }) +} + +pub(in crate::inspect::smp) fn build_nondirect_event_runtime_record_summary_from_signatures( + record_body: &[u8], + payload_offset: usize, + record_index: usize, + live_entry_id: u32, +) -> Option { + let grouped_marker_relative_offset = + find_u32_le_offsets(record_body, PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER as u32) + .into_iter() + .next(); + let end_marker_relative_offset = find_u32_le_offsets( + record_body, + PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER as u32, + ) + .into_iter() + .next(); + let head_signature_words = read_u16_window(record_body, 0, 18); + let post_group_signature_words = grouped_marker_relative_offset + .map(|offset| offset + 4) + .map(|offset| read_u16_window(record_body, offset, 12)) + .unwrap_or_default(); + let ascii_preview_before_grouped_marker = grouped_marker_relative_offset + .and_then(|offset| record_body.get(..offset).map(ascii_preview)); + + let mut notes = vec![ + "decoded from non-direct 0x4e99/0x4e9a/0x4e9b map-bundle row segmentation using 0x526f-delimited slices".to_string(), + format!( + "compact signature family = {}", + compact_nondirect_signature_family( + grouped_marker_relative_offset, + &head_signature_words, + &post_group_signature_words, + ) + ), + format!( + "head signature u16 words = {}", + format_u16_word_signature(&head_signature_words) + ), + ]; + if let Some(offset) = grouped_marker_relative_offset { + notes.push(format!( + "grouped-effect marker 0x4eb8 at relative offset +0x{offset:x}" + )); + if !post_group_signature_words.is_empty() { + notes.push(format!( + "post-group signature u16 words = {}", + format_u16_word_signature(&post_group_signature_words) + )); + } + } + if let Some(offset) = end_marker_relative_offset { + notes.push(format!( + "row terminator marker 0x4eb9 at relative offset +0x{offset:x}" + )); + } + if let Some(preview) = ascii_preview_before_grouped_marker { + notes.push(format!("ascii preview before grouped marker = {preview}")); + } + + Some(SmpLoadedPackedEventRecordSummary { + record_index, + live_entry_id, + payload_offset: Some(payload_offset), + payload_len: Some(record_body.len()), + decode_status: "compact_nondirect_parity_only".to_string(), + payload_family: "real_packed_nondirect_compact_v1".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + notes, + }) +} + +pub(in crate::inspect::smp) fn parse_nondirect_condition_row_summary( + record_body: &[u8], + row_index: usize, + remaining_minimum: usize, +) -> Option<(SmpLoadedPackedEventConditionRowSummary, usize)> { + let mut cursor = 0usize; + let mut row_bytes = vec![0u8; PACKED_EVENT_REAL_CONDITION_ROW_LEN]; + row_bytes + .get_mut(0..4)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes[4] = read_u8_at(record_body, cursor)?; + cursor += 1; + row_bytes + .get_mut(5..9)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(9..13)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes[0x0d] = read_u8_at(record_body, cursor)?; + cursor += 1; + row_bytes + .get_mut(0x0e..0x12)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(0x12..0x16)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + let candidate_name = + maybe_parse_nondirect_optional_name_block(record_body, &mut cursor, remaining_minimum)?; + let mut row = parse_real_condition_row_summary(&row_bytes, row_index, candidate_name)?; + row.notes.push( + "condition row reconstructed from the compact non-direct serializer fields under 0x430e80" + .to_string(), + ); + Some((row, cursor)) +} + +pub(in crate::inspect::smp) fn parse_nondirect_grouped_effect_row_summary( + record_body: &[u8], + group_index: usize, + row_index: usize, + remaining_minimum: usize, +) -> Option<(SmpLoadedPackedEventGroupedEffectRowSummary, usize)> { + let mut cursor = 0usize; + let mut row_bytes = vec![0u8; PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN]; + row_bytes + .get_mut(0..4)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(4..8)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes[8] = read_u8_at(record_body, cursor)?; + cursor += 1; + row_bytes + .get_mut(9..13)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(0x0d..0x11)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(0x11..0x15)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(0x12..0x16)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(0x14..0x18)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(0x16..0x1a)? + .copy_from_slice(record_body.get(cursor..cursor + 4)?); + cursor += 4; + row_bytes + .get_mut(0x18..0x24)? + .copy_from_slice(record_body.get(cursor..cursor + 12)?); + cursor += 12; + let locomotive_name = + maybe_parse_nondirect_optional_name_block(record_body, &mut cursor, remaining_minimum)?; + let mut row = + parse_real_grouped_effect_row_summary(&row_bytes, group_index, row_index, locomotive_name)?; + row.notes.push( + "grouped effect row reconstructed from the compact non-direct serializer fields under 0x430f68" + .to_string(), + ); + Some((row, cursor)) +} + +pub(in crate::inspect::smp) fn maybe_parse_nondirect_optional_name_block( + record_body: &[u8], + cursor: &mut usize, + remaining_minimum: usize, +) -> Option> { + if record_body.len() < *cursor + PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN { + return Some(None); + } + if record_body.len() + < *cursor + PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN + remaining_minimum + { + return Some(None); + } + let block = + record_body.get(*cursor..*cursor + PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN)?; + let name = read_ascii_c_string_at(block, 0, PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN); + let Some(name) = name.filter(|name| { + !name.is_empty() + && block + .iter() + .copied() + .all(|byte| byte == 0 || is_ascii_preview_byte(byte)) + }) else { + return Some(None); + }; + *cursor += PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN; + Some(Some(name)) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/real_records.rs b/crates/rrt-runtime/src/inspect/smp/events/real_records.rs new file mode 100644 index 0000000..a401251 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/real_records.rs @@ -0,0 +1,278 @@ +use crate::inspect::smp::events::*; + +pub(in crate::inspect::smp) fn try_parse_real_event_runtime_record_summaries( + records_payload: &[u8], + records_payload_offset: usize, + live_entry_ids: &[u32], +) -> Option> { + let mut cursor = 0usize; + let mut records = Vec::with_capacity(live_entry_ids.len()); + + for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() { + let (record, consumed_len) = parse_real_event_runtime_record_summary( + records_payload.get(cursor..)?, + records_payload_offset + cursor, + record_index, + live_entry_id, + )?; + records.push(record); + cursor += consumed_len; + } + + if cursor != records_payload.len() { + return None; + } + + Some(records) +} + +pub(in crate::inspect::smp) fn parse_real_event_runtime_record_summary( + record_body: &[u8], + payload_offset: usize, + record_index: usize, + live_entry_id: u32, +) -> Option<(SmpLoadedPackedEventRecordSummary, usize)> { + let mut cursor = 0usize; + let mut text_bands = Vec::with_capacity(PACKED_EVENT_TEXT_BAND_LABELS.len()); + for label in PACKED_EVENT_TEXT_BAND_LABELS { + let packed_len = usize::from(read_u16_at(record_body, cursor)?); + cursor += 2; + let band_bytes = record_body.get(cursor..cursor + packed_len)?; + cursor += packed_len; + text_bands.push(SmpLoadedPackedEventTextBandSummary { + label: label.to_string(), + packed_len, + present: packed_len != 0, + preview: ascii_preview(band_bytes), + }); + } + + let compact_control = parse_optional_real_compact_control_summary(record_body, &mut cursor)?; + + if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER { + return None; + } + cursor += 2; + let standalone_condition_row_count = usize::from(read_u16_at(record_body, cursor)?); + cursor += 2; + + let mut standalone_condition_rows = Vec::with_capacity(standalone_condition_row_count); + for row_index in 0..standalone_condition_row_count { + let row_bytes = record_body.get(cursor..cursor + PACKED_EVENT_REAL_CONDITION_ROW_LEN)?; + cursor += PACKED_EVENT_REAL_CONDITION_ROW_LEN; + let candidate_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?; + standalone_condition_rows.push(parse_real_condition_row_summary( + row_bytes, + row_index, + candidate_name, + )?); + } + + if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER { + return None; + } + cursor += 2; + + let mut grouped_effect_row_counts = Vec::with_capacity(4); + for _ in 0..4 { + grouped_effect_row_counts.push(usize::from(read_u16_at(record_body, cursor)?)); + cursor += 2; + } + + let mut grouped_effect_rows = + Vec::with_capacity(grouped_effect_row_counts.iter().sum::()); + for (group_index, row_count) in grouped_effect_row_counts.iter().copied().enumerate() { + for row_index in 0..row_count { + let row_bytes = + record_body.get(cursor..cursor + PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN)?; + cursor += PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN; + let locomotive_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?; + grouped_effect_rows.push(parse_real_grouped_effect_row_summary( + row_bytes, + group_index, + row_index, + locomotive_name, + )?); + } + } + if let Some(control) = compact_control.as_ref() { + for row in &mut grouped_effect_rows { + let target_subject = derive_real_grouped_target_subject(row, control); + let target_scope_ordinal = control + .grouped_target_scope_ordinals_0x7fb + .get(row.group_index) + .copied(); + row.grouped_target_subject = target_subject + .map(real_grouped_target_subject_name) + .map(str::to_string); + row.grouped_target_scope = derive_real_grouped_target_scope_name( + row, + control, + target_subject, + target_scope_ordinal, + ); + let company_target_present = control + .grouped_target_scope_ordinals_0x7fb + .get(row.group_index) + .copied() + .and_then(real_grouped_company_target) + .is_some(); + let chairman_target_present = control + .grouped_target_scope_ordinals_0x7fb + .get(row.group_index) + .copied() + .is_some_and(real_grouped_chairman_target_supported_in_runtime); + let territory_target_present = control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .is_some_and(|selector| *selector >= 0); + if row.descriptor_id == 15 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + && !company_target_present + && !territory_target_present + { + row.notes + .push("retire train row is missing company and territory scope".to_string()); + } + if row.descriptor_id == 3 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + && (!company_target_present || !territory_target_present) + { + row.notes + .push("territory access row is missing company or territory scope".to_string()); + } + if matches!(target_subject, Some(RealGroupedTargetSubject::Chairman)) + && !chairman_target_present + { + let ordinal = target_scope_ordinal.unwrap_or(u8::MAX); + row.notes.push(format!( + "chairman row uses unsupported grouped target scope ordinal {ordinal}" + )); + } + } + } + + let negative_sentinel_scope = compact_control.as_ref().and_then(|control| { + derive_negative_sentinel_scope_summary(&standalone_condition_rows, control) + }); + let decoded_conditions = + decode_real_condition_rows(&standalone_condition_rows, negative_sentinel_scope.as_ref()); + let decoded_actions = compact_control + .as_ref() + .map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control)) + .unwrap_or_default(); + let ordinary_condition_row_count = standalone_condition_rows + .iter() + .filter(|row| row.raw_condition_id >= 0) + .count(); + let executable_import_ready = !grouped_effect_rows.is_empty() + && decoded_actions.len() == grouped_effect_rows.len() + && decoded_conditions.len() == ordinary_condition_row_count + && decoded_actions + .iter() + .all(runtime_effect_supported_for_save_import) + && decoded_conditions + .iter() + .all(runtime_condition_supported_for_save_import); + let consumed_len = cursor; + Some(( + SmpLoadedPackedEventRecordSummary { + record_index, + live_entry_id, + payload_offset: Some(payload_offset), + payload_len: Some(consumed_len), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: compact_control.as_ref().map(|control| control.mode_byte_0x7ef), + active: None, + marks_collection_dirty: None, + one_shot: compact_control + .as_ref() + .map(|control| control.one_shot_header_0x7f5 != 0), + compact_control, + text_bands, + standalone_condition_row_count, + standalone_condition_rows, + negative_sentinel_scope, + grouped_effect_row_counts, + grouped_effect_rows, + decoded_conditions, + decoded_actions, + executable_import_ready, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(), + ], + }, + consumed_len, + )) +} + +pub(in crate::inspect::smp) fn parse_optional_real_compact_control_summary( + record_body: &[u8], + cursor: &mut usize, +) -> Option> { + if read_u16_at(record_body, *cursor)? == PACKED_EVENT_REAL_CONDITION_MARKER { + return Some(None); + } + + let end = cursor.checked_add(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN)?; + let bytes = record_body.get(*cursor..end)?; + let mut local = 0usize; + let mode_byte_0x7ef = read_u8_at(bytes, local)?; + local += 1; + let primary_selector_0x7f0 = read_u32_at(bytes, local)?; + local += 4; + let grouped_mode_0x7f4 = read_u8_at(bytes, local)?; + local += 1; + let one_shot_header_0x7f5 = read_u32_at(bytes, local)?; + local += 4; + let modifier_flag_0x7f9 = read_u8_at(bytes, local)?; + local += 1; + let modifier_flag_0x7fa = read_u8_at(bytes, local)?; + local += 1; + + let mut grouped_target_scope_ordinals_0x7fb = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); + for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { + grouped_target_scope_ordinals_0x7fb.push(read_u8_at(bytes, local)?); + local += 1; + } + + let mut grouped_scope_checkboxes_0x7ff = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); + for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { + grouped_scope_checkboxes_0x7ff.push(read_u8_at(bytes, local)?); + local += 1; + } + + let summary_toggle_0x800 = read_u8_at(bytes, local)?; + local += 1; + + let mut grouped_territory_selectors_0x80f = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); + for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { + grouped_territory_selectors_0x80f.push(read_i32_at(bytes, local)?); + local += 4; + } + + if local != bytes.len() { + return None; + } + if read_u16_at(record_body, end)? != PACKED_EVENT_REAL_CONDITION_MARKER { + return None; + } + + *cursor = end; + Some(Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef, + primary_selector_0x7f0, + grouped_mode_0x7f4, + one_shot_header_0x7f5, + modifier_flag_0x7f9, + modifier_flag_0x7fa, + grouped_target_scope_ordinals_0x7fb, + grouped_scope_checkboxes_0x7ff, + summary_toggle_0x800, + grouped_territory_selectors_0x80f, + })) +} diff --git a/crates/rrt-runtime/src/inspect/smp/events/synthetic.rs b/crates/rrt-runtime/src/inspect/smp/events/synthetic.rs new file mode 100644 index 0000000..f8b3e55 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/events/synthetic.rs @@ -0,0 +1,269 @@ +use crate::inspect::smp::events::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn try_parse_synthetic_event_runtime_record_summaries( + records_payload: &[u8], + records_payload_offset: usize, + live_entry_ids: &[u32], +) -> Option> { + if !records_payload.starts_with(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC) { + return None; + } + + let mut cursor = PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC.len(); + let mut records = Vec::with_capacity(live_entry_ids.len()); + for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() { + let record_len = usize::try_from(read_u32_at(records_payload, cursor)?).ok()?; + cursor += 4; + let record_body = records_payload.get(cursor..cursor + record_len)?; + records.push(parse_synthetic_event_runtime_record_summary( + record_body, + records_payload_offset + cursor, + record_index, + live_entry_id, + )?); + cursor += record_len; + } + + if cursor != records_payload.len() { + return None; + } + + Some(records) +} + +pub(in crate::inspect::smp) fn parse_synthetic_event_runtime_record_summary( + record_body: &[u8], + payload_offset: usize, + record_index: usize, + live_entry_id: u32, +) -> Option { + if !record_body.starts_with(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC) { + return None; + } + + let mut cursor = PACKED_EVENT_RECORD_SYNTHETIC_MAGIC.len(); + let trigger_kind = read_u8_at(record_body, cursor)?; + cursor += 1; + let flags = read_u8_at(record_body, cursor)?; + cursor += 1; + let standalone_condition_row_count = usize::from(read_u8_at(record_body, cursor)?); + cursor += 1; + let action_count = usize::from(read_u8_at(record_body, cursor)?); + cursor += 1; + + let mut grouped_effect_row_counts = Vec::with_capacity(4); + for _ in 0..4 { + grouped_effect_row_counts.push(usize::from(read_u8_at(record_body, cursor)?)); + cursor += 1; + } + + let mut text_bands = Vec::with_capacity(PACKED_EVENT_TEXT_BAND_LABELS.len()); + for label in PACKED_EVENT_TEXT_BAND_LABELS { + let packed_len = usize::from(read_u16_at(record_body, cursor)?); + cursor += 2; + let band_bytes = record_body.get(cursor..cursor + packed_len)?; + cursor += packed_len; + text_bands.push(SmpLoadedPackedEventTextBandSummary { + label: label.to_string(), + packed_len, + present: packed_len != 0, + preview: ascii_preview(band_bytes), + }); + } + + let mut decoded_actions = Vec::with_capacity(action_count); + for _ in 0..action_count { + decoded_actions.push(parse_synthetic_packed_event_action( + record_body, + &mut cursor, + )?); + } + + if cursor != record_body.len() { + return None; + } + + let executable_import_ready = decoded_actions + .iter() + .all(runtime_effect_supported_for_save_import); + + Some(SmpLoadedPackedEventRecordSummary { + record_index, + live_entry_id, + payload_offset: Some(payload_offset), + payload_len: Some(record_body.len()), + decode_status: if executable_import_ready { + "executable".to_string() + } else { + "parity_only".to_string() + }, + payload_family: "synthetic_harness".to_string(), + trigger_kind: Some(trigger_kind), + active: Some(flags & 0x01 != 0), + marks_collection_dirty: Some(flags & 0x02 != 0), + one_shot: Some(flags & 0x04 != 0), + compact_control: None, + text_bands, + standalone_condition_row_count, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts, + grouped_effect_rows: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions, + executable_import_ready, + notes: vec!["decoded from the current synthetic packed-event record harness".to_string()], + }) +} + +pub(in crate::inspect::smp) fn parse_synthetic_packed_event_action( + bytes: &[u8], + cursor: &mut usize, +) -> Option { + let opcode = read_u8_at(bytes, *cursor)?; + *cursor += 1; + match opcode { + 0x01 => { + let key = parse_len_prefixed_string(bytes, cursor)?; + let value = read_u8_at(bytes, *cursor)? != 0; + *cursor += 1; + Some(RuntimeEffect::SetWorldFlag { key, value }) + } + 0x02 => { + let target = parse_synthetic_company_target(bytes, cursor)?; + let delta = read_i64_at(bytes, *cursor)?; + *cursor += 8; + Some(RuntimeEffect::AdjustCompanyCash { target, delta }) + } + 0x03 => { + let target = parse_synthetic_company_target(bytes, cursor)?; + let delta = read_i64_at(bytes, *cursor)?; + *cursor += 8; + Some(RuntimeEffect::AdjustCompanyDebt { target, delta }) + } + 0x04 => { + let name = parse_len_prefixed_string(bytes, cursor)?; + let value = read_u32_at(bytes, *cursor)?; + *cursor += 4; + Some(RuntimeEffect::SetCandidateAvailability { name, value }) + } + 0x05 => { + let label = parse_len_prefixed_string(bytes, cursor)?; + let value = read_u32_at(bytes, *cursor)?; + *cursor += 4; + Some(RuntimeEffect::SetSpecialCondition { label, value }) + } + 0x06 => { + let template_len = usize::try_from(read_u32_at(bytes, *cursor)?).ok()?; + *cursor += 4; + let template_bytes = bytes.get(*cursor..*cursor + template_len)?; + let record = parse_synthetic_event_runtime_record_template(template_bytes)?; + *cursor += template_len; + Some(RuntimeEffect::AppendEventRecord { + record: Box::new(record), + }) + } + 0x07 => { + let record_id = read_u32_at(bytes, *cursor)?; + *cursor += 4; + Some(RuntimeEffect::ActivateEventRecord { record_id }) + } + 0x08 => { + let record_id = read_u32_at(bytes, *cursor)?; + *cursor += 4; + Some(RuntimeEffect::DeactivateEventRecord { record_id }) + } + 0x09 => { + let record_id = read_u32_at(bytes, *cursor)?; + *cursor += 4; + Some(RuntimeEffect::RemoveEventRecord { record_id }) + } + _ => None, + } +} + +pub(in crate::inspect::smp) fn parse_synthetic_event_runtime_record_template( + bytes: &[u8], +) -> Option { + if !bytes.starts_with(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC) { + return None; + } + + let mut cursor = PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC.len(); + let record_id = read_u32_at(bytes, cursor)?; + cursor += 4; + let trigger_kind = read_u8_at(bytes, cursor)?; + cursor += 1; + let flags = read_u8_at(bytes, cursor)?; + cursor += 1; + let action_count = usize::from(read_u8_at(bytes, cursor)?); + cursor += 1; + cursor += 1; + + let mut effects = Vec::with_capacity(action_count); + for _ in 0..action_count { + effects.push(parse_synthetic_packed_event_action(bytes, &mut cursor)?); + } + + if cursor != bytes.len() { + return None; + } + + Some(RuntimeEventRecordTemplate { + record_id, + trigger_kind, + active: flags & 0x01 != 0, + marks_collection_dirty: flags & 0x02 != 0, + one_shot: flags & 0x04 != 0, + conditions: Vec::new(), + effects, + }) +} + +pub(in crate::inspect::smp) fn parse_synthetic_company_target( + bytes: &[u8], + cursor: &mut usize, +) -> Option { + let target_kind = read_u8_at(bytes, *cursor)?; + *cursor += 1; + match target_kind { + 0x00 => Some(RuntimeCompanyTarget::AllActive), + 0x01 => { + let count = usize::from(read_u8_at(bytes, *cursor)?); + *cursor += 1; + let mut ids = Vec::with_capacity(count); + for _ in 0..count { + ids.push(read_u32_at(bytes, *cursor)?); + *cursor += 4; + } + Some(RuntimeCompanyTarget::Ids { ids }) + } + _ => None, + } +} + +pub(in crate::inspect::smp) fn parse_len_prefixed_string( + bytes: &[u8], + cursor: &mut usize, +) -> Option { + let len = usize::from(read_u8_at(bytes, *cursor)?); + *cursor += 1; + let text_bytes = bytes.get(*cursor..*cursor + len)?; + *cursor += len; + Some(String::from_utf8_lossy(text_bytes).into_owned()) +} + +pub(in crate::inspect::smp) fn parse_optional_u16_len_prefixed_string( + bytes: &[u8], + cursor: &mut usize, +) -> Option> { + let len = usize::from(read_u16_at(bytes, *cursor)?); + *cursor += 2; + if len == 0 { + return Some(None); + } + let text_bytes = bytes.get(*cursor..*cursor + len)?; + *cursor += len; + Some(Some(String::from_utf8_lossy(text_bytes).into_owned())) +} diff --git a/crates/rrt-runtime/src/inspect/smp/map_title.rs b/crates/rrt-runtime/src/inspect/smp/map_title.rs new file mode 100644 index 0000000..931addd --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/map_title.rs @@ -0,0 +1,238 @@ +use super::bundle::{ + SmpContainerProfile, classify_container_profile, classify_header_variant_probe, + classify_secondary_variant_probe, parse_shared_header, probe_early_content_layout, +}; +use crate::inspect::smp::common::*; +use crate::inspect::smp::*; + +mod model; + +pub use model::*; + +pub fn inspect_map_title_hint_file( + path: &Path, +) -> Result, Box> { + let bytes = fs::read(path)?; + let file_extension_hint = path + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.to_ascii_lowercase()); + Ok(inspect_map_title_hint_bytes( + &bytes, + file_extension_hint.as_deref(), + )) +} + +pub fn inspect_map_title_hint_bytes( + bytes: &[u8], + file_extension_hint: Option<&str>, +) -> Option { + let shared_header = parse_shared_header(bytes); + let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); + let first_ascii_run = find_first_ascii_run(bytes); + let early_content_probe = first_ascii_run + .as_ref() + .and_then(|ascii_run| probe_early_content_layout(bytes, ascii_run)); + let secondary_variant_probe = early_content_probe + .as_ref() + .and_then(classify_secondary_variant_probe); + let container_profile = classify_container_profile( + file_extension_hint, + header_variant_probe.as_ref(), + secondary_variant_probe.as_ref(), + ); + parse_map_title_hint_probe(bytes, file_extension_hint, container_profile.as_ref()) +} + +pub(super) fn parse_map_title_hint_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + if file_extension_hint != Some("gmp") { + return None; + } + + let grounded_title_hits = POST_LOAD_SCENARIO_FIXUP_TITLE_SET + .iter() + .filter_map(|title| { + let offset = find_first_subsequence_offset(bytes, title.as_bytes())?; + Some(SmpMapTitleHintTitleHit { + title: (*title).to_string(), + earliest_offset: offset, + }) + }) + .collect::>(); + + let embedded_map_references = find_ascii_fragment_occurrences_with_suffix(bytes, ".gmp") + .into_iter() + .map(|(offset, text)| SmpMapTitleHintMapReference { offset, text }) + .collect::>(); + + let adjacent_reference_title_pairs = + build_map_title_hint_adjacent_pairs(&embedded_map_references, &grounded_title_hits); + let strongest_same_stem_pair = adjacent_reference_title_pairs + .iter() + .find(|pair| pair.normalized_stem_match) + .cloned(); + + if grounded_title_hits.is_empty() + && embedded_map_references.is_empty() + && strongest_same_stem_pair.is_none() + { + return None; + } + + Some(SmpMapTitleHintProbe { + source_kind: "grounded-title-string-scan".to_string(), + profile_family: container_profile.map(|profile| profile.profile_family.clone()), + grounded_title_hits, + embedded_map_references, + adjacent_reference_title_pairs, + strongest_same_stem_pair, + }) +} + +pub(super) fn build_map_title_hint_adjacent_pairs( + map_references: &[SmpMapTitleHintMapReference], + title_hits: &[SmpMapTitleHintTitleHit], +) -> Vec { + let mut pairs = Vec::new(); + + for map_reference in map_references { + let mut best_pair: Option = None; + for title_hit in title_hits { + let byte_distance = map_reference.offset.abs_diff(title_hit.earliest_offset); + if byte_distance > MAP_TITLE_HINT_REFERENCE_PAIR_DISTANCE_LIMIT { + continue; + } + let candidate = SmpMapTitleHintAdjacentPair { + map_reference_offset: map_reference.offset, + map_reference_text: map_reference.text.clone(), + title_offset: title_hit.earliest_offset, + title: title_hit.title.clone(), + byte_distance, + normalized_stem_match: normalize_map_title_hint_stem(&map_reference.text) + == normalize_map_title_hint_stem(&title_hit.title), + }; + let replace = match &best_pair { + Some(current) => { + (candidate.normalized_stem_match && !current.normalized_stem_match) + || (candidate.normalized_stem_match == current.normalized_stem_match + && candidate.byte_distance < current.byte_distance) + } + None => true, + }; + if replace { + best_pair = Some(candidate); + } + } + if let Some(pair) = best_pair { + pairs.push(pair); + } + } + + pairs.sort_by_key(|pair| { + ( + !pair.normalized_stem_match, + pair.byte_distance, + pair.map_reference_offset, + ) + }); + pairs +} + +pub(super) fn normalize_map_title_hint_stem(text: &str) -> String { + text.trim() + .trim_end_matches(".gmp") + .trim_end_matches(".GMP") + .to_ascii_lowercase() +} + +pub(super) fn find_first_subsequence_offset(bytes: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() || bytes.len() < needle.len() { + return None; + } + bytes + .windows(needle.len()) + .position(|window| window == needle) +} + +pub(super) fn find_ascii_fragment_occurrences_with_suffix( + bytes: &[u8], + suffix: &str, +) -> Vec<(usize, String)> { + let suffix_bytes = suffix.as_bytes(); + if suffix_bytes.is_empty() || bytes.len() < suffix_bytes.len() { + return Vec::new(); + } + + let mut occurrences = Vec::new(); + let mut seen_offsets = BTreeSet::new(); + for offset in 0..=bytes.len() - suffix_bytes.len() { + if &bytes[offset..offset + suffix_bytes.len()] != suffix_bytes { + continue; + } + if let Some((start, text)) = extract_ascii_fragment_containing(bytes, offset) { + if seen_offsets.insert(start) { + occurrences.push((start, text)); + } + } + } + occurrences +} + +pub(super) fn extract_ascii_fragment_containing( + bytes: &[u8], + offset: usize, +) -> Option<(usize, String)> { + if offset >= bytes.len() || !is_map_title_hint_ascii_fragment_byte(bytes[offset]) { + return None; + } + + let mut start = offset; + while start > 0 && is_map_title_hint_ascii_fragment_byte(bytes[start - 1]) { + start -= 1; + } + + let mut end = offset; + while end < bytes.len() && is_map_title_hint_ascii_fragment_byte(bytes[end]) { + end += 1; + } + + if end <= start { + return None; + } + let len = (end - start).min(MAP_TITLE_HINT_ASCII_FRAGMENT_MAX_LEN); + let text = String::from_utf8_lossy(&bytes[start..start + len]) + .trim() + .to_string(); + if text.is_empty() { + return None; + } + Some((start, text)) +} + +pub(super) fn is_map_title_hint_ascii_fragment_byte(byte: u8) -> bool { + matches!(byte, b' ' | b'!' | b'-' | b'.' | b'/' | b'\\' | b':' | b'_' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') +} + +pub(super) fn build_ascii_preview(bytes: &[u8], start: usize, end: usize) -> SmpAsciiPreview { + let byte_len = end - start; + let preview_bytes = &bytes[start..end]; + let preview = String::from_utf8_lossy( + &preview_bytes[..preview_bytes.len().min(ASCII_PREVIEW_CHAR_LIMIT)], + ) + .into_owned(); + + SmpAsciiPreview { + offset: start, + byte_len, + truncated: byte_len > ASCII_PREVIEW_CHAR_LIMIT, + preview, + } +} + +pub(super) fn is_ascii_preview_byte(byte: u8) -> bool { + matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x21..=0x7e) +} diff --git a/crates/rrt-runtime/src/inspect/smp/map_title/model.rs b/crates/rrt-runtime/src/inspect/smp/map_title/model.rs new file mode 100644 index 0000000..8ccb212 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/map_title/model.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpMapTitleHintTitleHit { + pub title: String, + pub earliest_offset: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpMapTitleHintMapReference { + pub offset: usize, + pub text: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpMapTitleHintAdjacentPair { + pub map_reference_offset: usize, + pub map_reference_text: String, + pub title_offset: usize, + pub title: String, + pub byte_distance: usize, + pub normalized_stem_match: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpMapTitleHintProbe { + pub source_kind: String, + pub profile_family: Option, + pub grounded_title_hits: Vec, + pub embedded_map_references: Vec, + pub adjacent_reference_title_pairs: Vec, + pub strongest_same_stem_pair: Option, +} diff --git a/crates/rrt-runtime/src/inspect/smp/mod.rs b/crates/rrt-runtime/src/inspect/smp/mod.rs new file mode 100644 index 0000000..13b99fd --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/mod.rs @@ -0,0 +1,150 @@ +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::cmp::Reverse; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; + +use crate::event::conditions::{RuntimeCondition, RuntimeConditionComparator}; +use crate::event::effects::RuntimeEffect; +use crate::event::metrics::{ + RuntimeChairmanMetric, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, + RuntimePlayerConditionTestScope, RuntimeTerritoryMetric, RuntimeTrackMetric, +}; +use crate::event::records::RuntimeEventRecordTemplate; +use crate::event::targets::{ + RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, + RuntimeChairmanTarget, RuntimeCompanyTarget, RuntimePlayerTarget, RuntimeTerritoryTarget, +}; +use crate::state::{ + RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeTrackPieceCounts, +}; + +pub mod bundle; +pub(crate) mod catalog; +mod common; +pub mod events; +pub mod map_title; +pub mod profiles; +pub mod regions; +pub mod save_load; +pub mod services; +pub mod special_conditions; +pub mod structures; +#[cfg(test)] +mod tests; +pub mod world; + +use catalog::conditions::{ + GROUNDED_LOCOMOTIVE_PREFIX, KNOWN_CARGO_SLOT_DEFINITIONS, KnownCargoSlotDefinition, +}; +#[cfg(test)] +use catalog::conditions::{ + REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID, REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, + REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID, REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID, + REAL_CHAIRMAN_CASH_CONDITION_ID, REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID, + REAL_CHAIRMAN_NET_WORTH_CONDITION_ID, REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID, + REAL_CREDIT_RATING_CONDITION_ID, REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID, + REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID, REAL_INVESTOR_CONFIDENCE_CONDITION_ID, + REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID, REAL_MANAGEMENT_ATTITUDE_CONDITION_ID, + REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID, REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID, + REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID, REAL_PLAYER_VARIABLE_3_CONDITION_ID, + REAL_PRIME_RATE_CONDITION_ID, REAL_TERRITORY_ACCESS_COST_CONDITION_ID, + REAL_TERRITORY_VARIABLE_4_CONDITION_ID, REAL_WORLD_VARIABLE_1_CONDITION_ID, + real_ordinary_condition_metadata, +}; +#[cfg(test)] +use catalog::descriptors::real_grouped_effect_runtime_status_name; +use catalog::descriptors::{ + REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA, RealGroupedEffectDescriptorMetadata, + RealGroupedEffectRuntimeStatus, checked_in_event_effect_descriptor_rows, +}; +use catalog::fixups::POST_LOAD_SCENARIO_FIXUP_TITLE_SET; +use catalog::offsets::bundle::{ + ASCII_PREVIEW_CHAR_LIMIT, MAP_TITLE_HINT_ASCII_FRAGMENT_MAX_LEN, + MAP_TITLE_HINT_REFERENCE_PAIR_DISTANCE_LIMIT, +}; +#[cfg(test)] +use catalog::offsets::events::{ + EVENT_RUNTIME_COLLECTION_CLOSE_TAG, EVENT_RUNTIME_COLLECTION_METADATA_TAG, + EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION, EVENT_RUNTIME_COLLECTION_RECORDS_TAG, + PACKED_EVENT_REAL_COMPACT_CONTROL_LEN, PACKED_EVENT_REAL_CONDITION_MARKER, + PACKED_EVENT_REAL_CONDITION_ROW_LEN, PACKED_EVENT_REAL_GROUP_COUNT, + PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER, PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN, + PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER, PACKED_EVENT_RECORD_SYNTHETIC_MAGIC, + PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC, PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC, +}; +use catalog::offsets::regions::{ + INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT, INDEXED_COLLECTION_SERIALIZED_HEADER_LEN, + SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT, + SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX, SAVE_REGION_FIXED_ROW_CANDIDATE_PROBE_LIMIT, + SAVE_REGION_FIXED_ROW_DWORD_LANE_COUNT, SAVE_REGION_FIXED_ROW_STRIDE, + SAVE_REGION_QUEUED_NOTICE_NODE_KIND, SAVE_REGION_QUEUED_NOTICE_NODE_LEN, + SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED, SAVE_REGION_RECORD_NAME_TAG, + SAVE_REGION_RECORD_POLICY_TAG, SAVE_REGION_RECORD_PROFILE_TAG, +}; +use catalog::offsets::special_conditions::{ + LOCOMOTIVE_POLICY_FIELD_0_RUNTIME_OBJECT_OFFSET, + LOCOMOTIVE_POLICY_FIELD_1_RUNTIME_OBJECT_OFFSET, + LOCOMOTIVE_POLICY_FIELD_2_RUNTIME_OBJECT_OFFSET, + LOCOMOTIVE_POLICY_FIELD_3_RUNTIME_OBJECT_OFFSET, + LOCOMOTIVE_POLICY_FIELD_4_RUNTIME_OBJECT_OFFSET, + LOCOMOTIVE_POLICY_FIELD_5_RUNTIME_OBJECT_OFFSET, + LOCOMOTIVE_POLICY_FIELD_NEG1_RUNTIME_OBJECT_OFFSET, + LOCOMOTIVE_POLICY_FIELD_NEG2_RUNTIME_OBJECT_OFFSET, + LOCOMOTIVE_POLICY_FIELD_NEG3_RUNTIME_OBJECT_OFFSET, LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET, + LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET, + POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET, + POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET, + POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET, + POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET, + POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET, + POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, POST_SPECIAL_CONDITIONS_SCALAR_OFFSET, + POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET, + POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET, POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET, + POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET, POST_TEXT_FIELD_3_RUNTIME_OBJECT_OFFSET, + POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET, POST_TEXT_FIELD_5_RUNTIME_OBJECT_OFFSET, + POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET, POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET, + PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET, PRE_RECIPE_SCALAR_PLATEAU_OFFSET, RECIPE_BOOK_COUNT, + RECIPE_BOOK_HEAD_SAMPLE_LEN, RECIPE_BOOK_LINE_AREA_LEN, RECIPE_BOOK_LINE_AREA_OFFSET, + RECIPE_BOOK_LINE_COUNT, RECIPE_BOOK_LINE_STRIDE, RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET, + RECIPE_BOOK_ROOT_OFFSET, RECIPE_BOOK_STRIDE, RECIPE_BOOK_SUMMARY_END_OFFSET, + SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT, SMP_ALIGNED_RUNTIME_RULE_END_OFFSET, + SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT, + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT, + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX, SPECIAL_CONDITION_COUNT, + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT, SPECIAL_CONDITIONS_OFFSET, +}; +#[cfg(test)] +use catalog::offsets::world::{ + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE, RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT, + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG, + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS, + RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS, + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET, RT3_SAVE_WORLD_BLOCK_LEN, + RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG, + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET, +}; +use catalog::special_conditions::{ + KNOWN_SPECIAL_CONDITION_DEFINITIONS, known_special_condition_definition_for_label_id, +}; +use common::*; + +pub use catalog::SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION; diff --git a/crates/rrt-runtime/src/inspect/smp/profiles/classic.rs b/crates/rrt-runtime/src/inspect/smp/profiles/classic.rs new file mode 100644 index 0000000..14cf83a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/profiles/classic.rs @@ -0,0 +1,102 @@ +use crate::inspect::smp::profiles::*; + +pub(in crate::inspect::smp) fn parse_classic_rehydrate_profile_probe( + bytes: &[u8], + runtime_post_span_probe: Option<&SmpRuntimePostSpanProbe>, +) -> Option { + let post_span = runtime_post_span_probe?; + if post_span.profile_family != "rt3-classic-save-container-v1" { + return None; + } + + let progress_32dc_offset = + parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x32dc)?; + let progress_3714_offset = + parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x3714)?; + let progress_3715_offset = + parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x3715)?; + let packed_profile_offset = progress_3714_offset + 4; + let packed_profile_len = progress_3715_offset.checked_sub(packed_profile_offset)?; + if packed_profile_len != 0x108 { + return None; + } + + let ascii_runs = + collect_ascii_previews_in_range(bytes, packed_profile_offset, progress_3715_offset, 4); + let packed_profile_block = + parse_classic_packed_profile_block(bytes, packed_profile_offset, packed_profile_len)?; + + Some(SmpClassicRehydrateProfileProbe { + profile_family: post_span.profile_family.clone(), + progress_32dc_offset, + progress_3714_offset, + progress_3715_offset, + packed_profile_offset, + packed_profile_len, + packed_profile_len_hex: format!("0x{packed_profile_len:03x}"), + packed_profile_block, + ascii_runs, + }) +} + +pub(in crate::inspect::smp) fn parse_classic_packed_profile_block( + bytes: &[u8], + packed_profile_offset: usize, + packed_profile_len: usize, +) -> Option { + let block_end = packed_profile_offset.checked_add(packed_profile_len)?; + if block_end > bytes.len() || packed_profile_len != 0x108 { + return None; + } + + let leading_word_0 = read_u32_at(bytes, packed_profile_offset)?; + let trailing_zero_word_count_after_leading_word = (1..4) + .take_while(|index| { + read_u32_at(bytes, packed_profile_offset + (index * 4)).is_some_and(|word| word == 0) + }) + .count(); + let map_path_offset = 0x13; + let display_name_offset = 0x46; + let stable_nonzero_word_offsets = [0x00usize, 0x10, 0x78, 0x7c, 0x84, 0x88]; + let stable_nonzero_words = stable_nonzero_word_offsets + .iter() + .filter_map(|relative_offset| { + let value = read_u32_at(bytes, packed_profile_offset + relative_offset)?; + if value == 0 { + return None; + } + + Some(SmpPackedProfileWordLane { + relative_offset: *relative_offset, + relative_offset_hex: format!("0x{relative_offset:02x}"), + value, + value_hex: format!("0x{value:08x}"), + }) + }) + .collect::>(); + + Some(SmpClassicPackedProfileBlock { + relative_len: packed_profile_len, + relative_len_hex: format!("0x{packed_profile_len:03x}"), + leading_word_0, + leading_word_0_hex: format!("0x{leading_word_0:08x}"), + trailing_zero_word_count_after_leading_word, + map_path_offset, + map_path: read_c_string_in_range(bytes, packed_profile_offset + map_path_offset, block_end), + display_name_offset, + display_name: read_c_string_in_range( + bytes, + packed_profile_offset + display_name_offset, + block_end, + ), + profile_byte_0x77: bytes[packed_profile_offset + 0x77], + profile_byte_0x77_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x77]), + profile_byte_0x82: bytes[packed_profile_offset + 0x82], + profile_byte_0x82_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x82]), + profile_byte_0x97: bytes[packed_profile_offset + 0x97], + profile_byte_0x97_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x97]), + profile_byte_0xc5: bytes[packed_profile_offset + 0xc5], + profile_byte_0xc5_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0xc5]), + stable_nonzero_words, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/profiles/mod.rs b/crates/rrt-runtime/src/inspect/smp/profiles/mod.rs new file mode 100644 index 0000000..972f5bc --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/profiles/mod.rs @@ -0,0 +1,28 @@ +use super::bundle::{ + SmpContainerProfile, SmpHeaderVariantProbe, SmpRt3105PostSpanBridgeProbe, + SmpRt3105SaveBridgePayloadProbe, SmpRuntimePostSpanProbe, SmpRuntimeTrailerBlock, + collect_ascii_previews_in_range, find_c_string_with_suffix_in_range, + parse_grounded_progress_hit_offset, read_c_string_in_range, +}; +use super::special_conditions::{ + classify_name_table_footer_progress_alignment, matches_candidate_availability_table_header, +}; +use crate::inspect::smp::*; + +mod classic; +mod model; +mod name_table; +mod named_locomotive; +mod rt3_105; + +pub use model::*; + +pub(in crate::inspect::smp) use classic::parse_classic_rehydrate_profile_probe; +pub(in crate::inspect::smp) use name_table::parse_rt3_105_save_name_table_probe; +#[cfg(test)] +pub(super) use named_locomotive::RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE; +pub(in crate::inspect::smp) use named_locomotive::parse_rt3_105_save_named_locomotive_availability_probe; +pub(in crate::inspect::smp) use rt3_105::{ + parse_rt3_105_packed_profile_probe, parse_rt3_105_post_span_bridge_probe, + parse_rt3_105_save_bridge_payload_probe, +}; diff --git a/crates/rrt-runtime/src/inspect/smp/profiles/model.rs b/crates/rrt-runtime/src/inspect/smp/profiles/model.rs new file mode 100644 index 0000000..2227350 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/profiles/model.rs @@ -0,0 +1,145 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::common::SmpAsciiPreview; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRt3105SaveNameTableProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub semantic_alignment: Vec, + pub header_offset: usize, + pub header_word_0: u32, + pub header_word_0_hex: String, + pub header_word_1: u32, + pub header_word_1_hex: String, + pub header_word_2: u32, + pub header_word_2_hex: String, + pub entry_stride: usize, + pub entry_stride_hex: String, + pub header_prefix_word_count: usize, + pub observed_entry_capacity: usize, + pub observed_entry_count: usize, + pub zero_trailer_entry_count: usize, + pub nonzero_trailer_entry_count: usize, + pub distinct_trailer_words: Vec, + pub distinct_trailer_hex_words: Vec, + pub zero_trailer_entry_names: Vec, + pub entries_offset: usize, + pub entries_end_offset: usize, + pub trailing_footer_hex: String, + pub footer_progress_word_0: u32, + pub footer_progress_word_0_hex: String, + pub footer_progress_word_1: u32, + pub footer_progress_word_1_hex: String, + pub footer_trailing_byte: u8, + pub footer_trailing_byte_hex: String, + pub footer_grounded_alignments: Vec, + pub entries: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRt3105SaveNamedLocomotiveAvailabilityProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub semantic_alignment: Vec, + pub entries_offset: usize, + pub entry_stride: usize, + pub entry_stride_hex: String, + pub observed_entry_count: usize, + pub zero_availability_count: usize, + pub zero_availability_names: Vec, + pub entries_end_offset: usize, + pub entries: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRt3105SaveNameTableEntry { + pub index: usize, + pub offset: usize, + pub text: String, + pub availability_dword: u32, + pub availability_dword_hex: String, + pub trailer_word: u32, + pub trailer_word_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpClassicRehydrateProfileProbe { + pub profile_family: String, + pub progress_32dc_offset: usize, + pub progress_3714_offset: usize, + pub progress_3715_offset: usize, + pub packed_profile_offset: usize, + pub packed_profile_len: usize, + pub packed_profile_len_hex: String, + pub packed_profile_block: SmpClassicPackedProfileBlock, + pub ascii_runs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpClassicPackedProfileBlock { + pub relative_len: usize, + pub relative_len_hex: String, + pub leading_word_0: u32, + pub leading_word_0_hex: String, + pub trailing_zero_word_count_after_leading_word: usize, + pub map_path_offset: usize, + pub map_path: Option, + pub display_name_offset: usize, + pub display_name: Option, + pub profile_byte_0x77: u8, + pub profile_byte_0x77_hex: String, + pub profile_byte_0x82: u8, + pub profile_byte_0x82_hex: String, + pub profile_byte_0x97: u8, + pub profile_byte_0x97_hex: String, + pub profile_byte_0xc5: u8, + pub profile_byte_0xc5_hex: String, + pub stable_nonzero_words: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRt3105PackedProfileProbe { + pub profile_family: String, + pub packed_profile_offset: usize, + pub packed_profile_len: usize, + pub packed_profile_len_hex: String, + pub packed_profile_block: SmpRt3105PackedProfileBlock, + pub ascii_runs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRt3105PackedProfileBlock { + pub relative_len: usize, + pub relative_len_hex: String, + pub leading_word_0: u32, + pub leading_word_0_hex: String, + pub trailing_zero_word_count_after_leading_word: usize, + pub header_flag_word_3: u32, + pub header_flag_word_3_hex: String, + pub map_path_offset: usize, + pub map_path: Option, + pub display_name_offset: usize, + pub display_name: Option, + pub profile_byte_0x77: u8, + pub profile_byte_0x77_hex: String, + pub profile_byte_0x82: u8, + pub profile_byte_0x82_hex: String, + pub profile_byte_0x97: u8, + pub profile_byte_0x97_hex: String, + pub profile_byte_0xc5: u8, + pub profile_byte_0xc5_hex: String, + pub stable_nonzero_words: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPackedProfileWordLane { + pub relative_offset: usize, + pub relative_offset_hex: String, + pub value: u32, + pub value_hex: String, +} diff --git a/crates/rrt-runtime/src/inspect/smp/profiles/name_table.rs b/crates/rrt-runtime/src/inspect/smp/profiles/name_table.rs new file mode 100644 index 0000000..6e99bba --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/profiles/name_table.rs @@ -0,0 +1,178 @@ +use crate::inspect::smp::profiles::*; + +pub(in crate::inspect::smp) fn parse_rt3_105_save_name_table_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + bridge_payload_probe: Option<&SmpRt3105SaveBridgePayloadProbe>, +) -> Option { + let ( + profile_family, + source_kind, + header_offset, + entries_offset, + block_end_offset, + mut evidence, + ) = if let Some(payload) = bridge_payload_probe { + ( + payload.profile_family.clone(), + "save-bridge-secondary-block".to_string(), + payload.secondary_block_offset + 0x354, + payload.secondary_block_offset + 0x3b5, + payload.secondary_block_end_offset, + vec![ + "common-save bridge payload branch".to_string(), + format!( + "secondary block span 0x{:x}..0x{:x}", + payload.secondary_block_offset, payload.secondary_block_end_offset + ), + ], + ) + } else { + let profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let extension = file_extension_hint.unwrap_or(""); + let source_kind = match extension { + "gmp" => "map-fixed-catalog-range", + "gms" => "save-fixed-catalog-range", + "gmx" => "sandbox-fixed-catalog-range", + _ => "fixed-catalog-range", + } + .to_string(); + ( + profile_family, + source_kind, + 0x6a70, + 0x6ad1, + 0x73c0, + vec![ + "fixed catalog range branch".to_string(), + "using observed shared 1.05 candidate-availability table offsets".to_string(), + ], + ) + }; + let entry_stride = 0x22usize; + if block_end_offset > bytes.len() { + return None; + } + if !matches_candidate_availability_table_header(bytes, header_offset) { + return None; + } + let observed_entry_capacity = read_u32_at(bytes, header_offset + 0x1c)? as usize; + let observed_entry_count = read_u32_at(bytes, header_offset + 0x20)? as usize; + let entries_len = observed_entry_count.checked_mul(entry_stride)?; + let entries_end_offset = entries_offset.checked_add(entries_len)?; + if observed_entry_count == 0 || observed_entry_capacity < observed_entry_count { + return None; + } + if entries_end_offset > block_end_offset || entries_end_offset > bytes.len() { + return None; + } + + let mut entries = Vec::with_capacity(observed_entry_count); + 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); + let text = std::str::from_utf8(&chunk[..nul_index]).ok()?.to_string(); + let trailer_word = read_u32_at(bytes, offset + entry_stride - 4)?; + entries.push(SmpRt3105SaveNameTableEntry { + index, + offset, + text, + availability_dword: trailer_word, + availability_dword_hex: format!("0x{trailer_word:08x}"), + trailer_word, + trailer_word_hex: format!("0x{trailer_word:08x}"), + }); + } + + let zero_trailer_entry_names = entries + .iter() + .filter(|entry| entry.trailer_word == 0) + .map(|entry| entry.text.clone()) + .collect::>(); + let zero_trailer_entry_count = zero_trailer_entry_names.len(); + let nonzero_trailer_entry_count = entries.len().saturating_sub(zero_trailer_entry_count); + let mut distinct_trailer_words = entries + .iter() + .map(|entry| entry.trailer_word) + .collect::>(); + distinct_trailer_words.sort_unstable(); + distinct_trailer_words.dedup(); + let distinct_trailer_hex_words = distinct_trailer_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect::>(); + let trailing_footer_hex = hex_encode(&bytes[entries_end_offset..block_end_offset]); + let footer = &bytes[entries_end_offset..block_end_offset]; + if footer.len() != 9 { + return None; + } + let footer_progress_word_0 = u32::from_le_bytes([footer[0], footer[1], footer[2], footer[3]]); + let footer_progress_word_1 = u32::from_le_bytes([footer[4], footer[5], footer[6], footer[7]]); + let footer_trailing_byte = footer[8]; + let mut footer_grounded_alignments = Vec::new(); + for value in [footer_progress_word_0, footer_progress_word_1] { + if let Some(alignment) = classify_name_table_footer_progress_alignment(value) { + footer_grounded_alignments.push(alignment.to_string()); + } + } + evidence.extend([ + format!("header offset 0x{header_offset:08x}"), + format!("entries offset 0x{entries_offset:08x}"), + format!("entry stride 0x{entry_stride:x}"), + format!("observed entry capacity {}", observed_entry_capacity), + format!("observed entry count {}", observed_entry_count), + format!("zero-trailer entries {}", zero_trailer_entry_count), + format!( + "trailing footer {} bytes after last entry", + block_end_offset - entries_end_offset + ), + ]); + let semantic_alignment = vec![ + "Matches the grounded scenario-side named candidate-availability table shape under 0x00437743.".to_string(), + "Entry layout matches 0x00434ea0/0x00434f20: name slot at +0x00..+0x1d and availability dword at +0x1e.".to_string(), + "The shared map/save range suggests this catalog is bundled in source map content and later mirrored into scenario state [state+0x66b2].".to_string(), + ]; + + Some(SmpRt3105SaveNameTableProbe { + profile_family, + source_kind, + semantic_family: "scenario-named-candidate-availability-table".to_string(), + semantic_alignment, + header_offset, + header_word_0: read_u32_at(bytes, header_offset)?, + header_word_0_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset)?), + header_word_1: read_u32_at(bytes, header_offset + 4)?, + header_word_1_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset + 4)?), + header_word_2: read_u32_at(bytes, header_offset + 8)?, + header_word_2_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset + 8)?), + entry_stride, + entry_stride_hex: format!("0x{entry_stride:x}"), + header_prefix_word_count: 11, + observed_entry_capacity, + observed_entry_count, + zero_trailer_entry_count, + nonzero_trailer_entry_count, + distinct_trailer_words, + distinct_trailer_hex_words, + zero_trailer_entry_names, + entries_offset, + entries_end_offset, + trailing_footer_hex, + footer_progress_word_0, + footer_progress_word_0_hex: format!("0x{footer_progress_word_0:08x}"), + footer_progress_word_1, + footer_progress_word_1_hex: format!("0x{footer_progress_word_1:08x}"), + footer_trailing_byte, + footer_trailing_byte_hex: format!("0x{footer_trailing_byte:02x}"), + footer_grounded_alignments, + entries, + evidence, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/profiles/named_locomotive.rs b/crates/rrt-runtime/src/inspect/smp/profiles/named_locomotive.rs new file mode 100644 index 0000000..21e854f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/profiles/named_locomotive.rs @@ -0,0 +1,143 @@ +use crate::inspect::smp::profiles::*; + +pub(in crate::inspect::smp) const RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE: usize = 0x41; +pub(in crate::inspect::smp) const RT3_105_SAVE_NAMED_LOCOMOTIVE_MIN_ENTRY_COUNT: usize = 8; +pub(in crate::inspect::smp) const RT3_105_SAVE_NAMED_LOCOMOTIVE_MAX_SEARCH_SPAN: usize = 0x4000; + +pub(in crate::inspect::smp) fn parse_rt3_105_save_named_locomotive_availability_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>, +) -> Option { + let packed_profile_probe = packed_profile_probe?; + let extension = file_extension_hint.unwrap_or(""); + let profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| packed_profile_probe.profile_family.clone()); + if !matches!(extension, "gms" | "gmx") || !profile_family.contains("save-container") { + return None; + } + + let search_start = packed_profile_probe + .packed_profile_offset + .checked_add(packed_profile_probe.packed_profile_len)?; + let search_end = search_start + .checked_add(RT3_105_SAVE_NAMED_LOCOMOTIVE_MAX_SEARCH_SPAN) + .map(|end| end.min(bytes.len())) + .unwrap_or(bytes.len()); + if search_end <= search_start + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE { + return None; + } + + let mut best_start = None; + let mut best_entries = Vec::new(); + for candidate_start in search_start..search_end { + let entries = parse_direct_named_locomotive_entries(bytes, candidate_start, search_end); + if entries.len() > best_entries.len() { + best_entries = entries; + best_start = Some(candidate_start); + } + } + + if best_entries.len() < RT3_105_SAVE_NAMED_LOCOMOTIVE_MIN_ENTRY_COUNT { + return None; + } + + let entries_offset = best_start?; + let entries_end_offset = entries_offset + .checked_add(best_entries.len() * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE)?; + let zero_availability_names = best_entries + .iter() + .filter(|entry| entry.availability_dword == 0) + .map(|entry| entry.text.clone()) + .collect::>(); + let zero_availability_count = zero_availability_names.len(); + let source_kind = match extension { + "gms" => "save-direct-locomotive-row-run", + "gmx" => "sandbox-direct-locomotive-row-run", + _ => "direct-locomotive-row-run", + } + .to_string(); + + let observed_entry_count = best_entries.len(); + + Some(SmpRt3105SaveNamedLocomotiveAvailabilityProbe { + profile_family, + source_kind, + semantic_family: "scenario-named-locomotive-availability-table".to_string(), + semantic_alignment: vec![ + "Matches the grounded `.smp` save-side locomotive-name-plus-dword row family restored into scenario state [world+0x66b6].".to_string(), + "Entry layout is one availability dword at +0x00 followed by one fixed-width locomotive name buffer at +0x04..+0x40.".to_string(), + "The recovered row order is treated conservatively as the live locomotive ordinal order later used by locomotives-page descriptor lowering.".to_string(), + ], + entries_offset, + entry_stride: RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE, + entry_stride_hex: format!("0x{:x}", RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE), + observed_entry_count, + zero_availability_count, + zero_availability_names, + entries_end_offset, + entries: best_entries, + evidence: vec![ + format!("search span 0x{search_start:08x}..0x{search_end:08x}"), + format!("entries offset 0x{entries_offset:08x}"), + format!( + "entry stride 0x{:x}", + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE + ), + format!("observed entry count {observed_entry_count}"), + ], + }) +} + +pub(in crate::inspect::smp) fn parse_direct_named_locomotive_entries( + bytes: &[u8], + start_offset: usize, + search_end: usize, +) -> Vec { + let mut entries = Vec::new(); + let mut offset = start_offset; + while offset + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE <= bytes.len() && offset < search_end + { + let record = &bytes[offset..offset + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE]; + let Some(nul_index) = record[4..].iter().position(|byte| *byte == 0) else { + break; + }; + let name_bytes = &record[4..4 + nul_index]; + if name_bytes.is_empty() { + break; + } + let Ok(text) = std::str::from_utf8(name_bytes) else { + break; + }; + if !is_probable_named_locomotive_label(text) { + break; + } + if record[4 + nul_index + 1..].iter().any(|byte| *byte != 0) { + break; + } + + let availability_dword = u32::from_le_bytes([record[0], record[1], record[2], record[3]]); + entries.push(SmpRt3105SaveNameTableEntry { + index: entries.len(), + 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}"), + }); + offset += RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE; + } + entries +} + +pub(in crate::inspect::smp) fn is_probable_named_locomotive_label(text: &str) -> bool { + if text.is_empty() || text.len() > 40 { + return false; + } + text.bytes().all(|byte| { + byte.is_ascii_alphanumeric() || matches!(byte, b' ' | b'-' | b'/' | b'(' | b')' | b'.') + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/profiles/rt3_105.rs b/crates/rrt-runtime/src/inspect/smp/profiles/rt3_105.rs new file mode 100644 index 0000000..6afc063 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/profiles/rt3_105.rs @@ -0,0 +1,320 @@ +use crate::inspect::smp::profiles::*; + +pub(in crate::inspect::smp) fn parse_rt3_105_packed_profile_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + header_variant_probe: Option<&SmpHeaderVariantProbe>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + let profile_family = if container_profile.is_some_and(|profile| { + matches!( + profile.profile_family.as_str(), + "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ) + }) { + container_profile + .expect("checked above") + .profile_family + .clone() + } else if file_extension_hint == Some("gms") + && header_variant_probe.is_some_and(|probe| { + matches!( + probe.variant_family.as_str(), + "rt3-105-common-header-v1" + | "rt3-105-scenario-save-header-v1" + | "rt3-105-alt-save-header-v1" + | "rt3-map-header-family" + ) + }) + { + "rt3-105-save-analog-block-inferred".to_string() + } else { + return None; + }; + + if file_extension_hint != Some("gms") { + return None; + } + + let map_path_offset = find_c_string_with_suffix_in_range(bytes, 0x7000, 0x9000, ".gmp")?; + let packed_profile_offset = map_path_offset.checked_sub(0x10)?; + let packed_profile_len = 0x108usize; + let block_end = packed_profile_offset.checked_add(packed_profile_len)?; + if block_end > bytes.len() { + return None; + } + + let packed_profile_block = + parse_rt3_105_packed_profile_block(bytes, packed_profile_offset, packed_profile_len)?; + let ascii_runs = collect_ascii_previews_in_range(bytes, packed_profile_offset, block_end, 4); + + Some(SmpRt3105PackedProfileProbe { + profile_family, + packed_profile_offset, + packed_profile_len, + packed_profile_len_hex: format!("0x{packed_profile_len:03x}"), + packed_profile_block, + ascii_runs, + }) +} + +pub(in crate::inspect::smp) fn parse_rt3_105_post_span_bridge_probe( + runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>, + runtime_post_span_probe: Option<&SmpRuntimePostSpanProbe>, + rt3_105_packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>, +) -> Option { + let trailer = runtime_trailer_block?; + let post_span = runtime_post_span_probe?; + let packed_profile = rt3_105_packed_profile_probe?; + let supported = matches!( + trailer.profile_family.as_str(), + "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + | "rt3-105-save-analog-block-inferred" + ); + if !supported || trailer.profile_family != post_span.profile_family { + return None; + } + + let next_candidate_high_u16_words = post_span + .header_candidates + .first() + .map(|candidate| candidate.high_u16_words.clone()) + .unwrap_or_default(); + let next_candidate_high_hex_words = next_candidate_high_u16_words + .iter() + .map(|word| format!("0x{word:04x}")) + .collect::>(); + let next_candidate_offset = post_span.next_aligned_candidate_offset; + let next_candidate_delta_from_span_target = + next_candidate_offset.and_then(|offset| offset.checked_sub(post_span.span_target_offset)); + let packed_profile_delta_from_span_target = packed_profile + .packed_profile_offset + .checked_sub(post_span.span_target_offset)?; + let next_candidate_delta_from_packed_profile = next_candidate_offset + .map(|offset| offset as i64 - packed_profile.packed_profile_offset as i64); + + let mut bridge_evidence = vec![ + format!("profile family {}", trailer.profile_family), + format!("selector high {}", trailer.selector_high_hex), + format!("descriptor high {}", trailer.descriptor_high_hex), + format!( + "packed profile sits +0x{packed_profile_delta_from_span_target:x} from span target" + ), + ]; + if let Some(delta) = next_candidate_delta_from_span_target { + bridge_evidence.push(format!("next candidate sits +0x{delta:x} from span target")); + } + if let Some(delta) = next_candidate_delta_from_packed_profile { + bridge_evidence.push(format!( + "next candidate is {delta:+#x} relative to packed profile" + )); + } + + let bridge_family = match ( + trailer.selector_high_u16, + trailer.descriptor_high_u16, + next_candidate_high_u16_words.as_slice(), + ) { + (0x7110, 0x7801 | 0x7401, [0x6200, 0x0000, 0xfff7, 0x5515, ..]) => { + bridge_evidence.push(format!( + "selector/descriptor pair 0x7110 -> 0x{:04x}", + trailer.descriptor_high_u16 + )); + bridge_evidence.push( + "next candidate begins with high-16 lanes 0x6200/0x0000/0xfff7/0x5515" + .to_string(), + ); + "rt3-105-save-post-span-bridge-v1" + } + (0x54cd, 0x5901, [0x1500, 0x0100, 0x4100, 0x0200, ..]) => { + bridge_evidence.push("selector/descriptor pair 0x54cd -> 0x5901".to_string()); + bridge_evidence.push( + "next candidate begins with high-16 lanes 0x1500/0x0100/0x4100/0x0200" + .to_string(), + ); + "rt3-105-alt-save-post-span-bridge-v1" + } + (0x0001, 0x0186, [0x0186, 0x0006, 0x0006, 0x0001, ..]) => { + bridge_evidence.push("selector/descriptor pair 0x0001 -> 0x0186".to_string()); + bridge_evidence.push( + "next candidate remains in the local cycle neighborhood with 0x0186/0x0006/0x0006/0x0001" + .to_string(), + ); + "rt3-105-scenario-post-span-bridge-v1" + } + _ => "unknown", + } + .to_string(); + + Some(SmpRt3105PostSpanBridgeProbe { + profile_family: trailer.profile_family.clone(), + bridge_family, + bridge_evidence, + span_target_offset: post_span.span_target_offset, + next_candidate_offset, + next_candidate_delta_from_span_target, + packed_profile_offset: packed_profile.packed_profile_offset, + packed_profile_delta_from_span_target, + next_candidate_delta_from_packed_profile, + selector_high_u16: trailer.selector_high_u16, + selector_high_hex: trailer.selector_high_hex.clone(), + descriptor_high_u16: trailer.descriptor_high_u16, + descriptor_high_hex: trailer.descriptor_high_hex.clone(), + next_candidate_high_u16_words, + next_candidate_high_hex_words, + }) +} + +pub(in crate::inspect::smp) fn parse_rt3_105_save_bridge_payload_probe( + bytes: &[u8], + bridge_probe: Option<&SmpRt3105PostSpanBridgeProbe>, +) -> Option { + let bridge = bridge_probe?; + if bridge.bridge_family != "rt3-105-save-post-span-bridge-v1" { + return None; + } + + let primary_block_offset = bridge.next_candidate_offset?; + let primary_block_word_count = 8usize; + let primary_words = read_u32_window(bytes, primary_block_offset, primary_block_word_count); + if primary_words.len() < primary_block_word_count { + return None; + } + + let secondary_block_delta_from_primary = 0x1808usize; + let secondary_block_offset = primary_block_offset + secondary_block_delta_from_primary; + let secondary_block_end_offset = bridge.packed_profile_offset; + let secondary_block_len = secondary_block_end_offset.checked_sub(secondary_block_offset)?; + let secondary_preview_word_count = 32usize; + let secondary_words = + read_u32_window(bytes, secondary_block_offset, secondary_preview_word_count); + if secondary_words.len() < secondary_preview_word_count { + return None; + } + + let primary_signature_matches = primary_words + == [ + 0x62000000, 0x00000000, 0xfff70000, 0x55150000, 0x55550000, 0x00000000, 0xfff70000, + 0x54550000, + ]; + let secondary_prefix_matches = secondary_words.starts_with(&[ + 0x00050000, 0x00050005, 0xfff70000, 0x54540000, 0x545400f9, 0x00f900f9, 0x00f94008, + 0x00001555, + ]); + + let mut evidence = vec![ + "bridge family rt3-105-save-post-span-bridge-v1".to_string(), + format!("primary block offset 0x{primary_block_offset:08x}"), + format!("secondary block offset 0x{secondary_block_offset:08x}"), + format!("secondary block delta from primary 0x{secondary_block_delta_from_primary:x}"), + format!("secondary block end offset 0x{secondary_block_end_offset:08x}"), + format!("secondary block span 0x{secondary_block_len:x} bytes"), + ]; + if primary_signature_matches { + evidence.push( + "primary 8-word bridge block matches the observed 0x6200/0xfff7/0x5515/0x5555 spine" + .to_string(), + ); + } + if secondary_prefix_matches { + evidence.push( + "secondary preview matches the observed 0x0005/0xfff7/0x5454 dense block prefix" + .to_string(), + ); + } + + Some(SmpRt3105SaveBridgePayloadProbe { + profile_family: bridge.profile_family.clone(), + bridge_family: bridge.bridge_family.clone(), + primary_block_offset, + primary_block_len: primary_block_word_count * 4, + primary_block_len_hex: format!("0x{:02x}", primary_block_word_count * 4), + primary_hex_words: primary_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + primary_words, + secondary_block_offset, + secondary_block_delta_from_primary, + secondary_block_delta_from_primary_hex: format!("0x{secondary_block_delta_from_primary:x}"), + secondary_block_end_offset, + secondary_block_len, + secondary_block_len_hex: format!("0x{secondary_block_len:x}"), + secondary_preview_word_count, + secondary_hex_words: secondary_words + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + secondary_words, + evidence, + }) +} + +pub(in crate::inspect::smp) fn parse_rt3_105_packed_profile_block( + bytes: &[u8], + packed_profile_offset: usize, + packed_profile_len: usize, +) -> Option { + let block_end = packed_profile_offset.checked_add(packed_profile_len)?; + if block_end > bytes.len() || packed_profile_len != 0x108 { + return None; + } + + let leading_word_0 = read_u32_at(bytes, packed_profile_offset)?; + let trailing_zero_word_count_after_leading_word = (1..4) + .take_while(|index| { + read_u32_at(bytes, packed_profile_offset + (index * 4)).is_some_and(|word| word == 0) + }) + .count(); + let header_flag_word_3 = read_u32_at(bytes, packed_profile_offset + 0x0c)?; + let map_path_offset = 0x10usize; + let display_name_offset = 0x43usize; + let stable_nonzero_word_offsets = [0x00usize, 0x0c, 0x78, 0x7c, 0x80, 0x84]; + let stable_nonzero_words = stable_nonzero_word_offsets + .iter() + .filter_map(|relative_offset| { + let value = read_u32_at(bytes, packed_profile_offset + relative_offset)?; + if value == 0 { + return None; + } + + Some(SmpPackedProfileWordLane { + relative_offset: *relative_offset, + relative_offset_hex: format!("0x{relative_offset:02x}"), + value, + value_hex: format!("0x{value:08x}"), + }) + }) + .collect::>(); + + Some(SmpRt3105PackedProfileBlock { + relative_len: packed_profile_len, + relative_len_hex: format!("0x{packed_profile_len:03x}"), + leading_word_0, + leading_word_0_hex: format!("0x{leading_word_0:08x}"), + trailing_zero_word_count_after_leading_word, + header_flag_word_3, + header_flag_word_3_hex: format!("0x{header_flag_word_3:08x}"), + map_path_offset, + map_path: read_c_string_in_range(bytes, packed_profile_offset + map_path_offset, block_end), + display_name_offset, + display_name: read_c_string_in_range( + bytes, + packed_profile_offset + display_name_offset, + block_end, + ), + profile_byte_0x77: bytes[packed_profile_offset + 0x77], + profile_byte_0x77_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x77]), + profile_byte_0x82: bytes[packed_profile_offset + 0x82], + profile_byte_0x82_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x82]), + profile_byte_0x97: bytes[packed_profile_offset + 0x97], + profile_byte_0x97_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x97]), + profile_byte_0xc5: bytes[packed_profile_offset + 0xc5], + profile_byte_0xc5_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0xc5]), + stable_nonzero_words, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/regions/compare.rs b/crates/rrt-runtime/src/inspect/smp/regions/compare.rs new file mode 100644 index 0000000..0f0a135 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/regions/compare.rs @@ -0,0 +1,139 @@ +use crate::inspect::smp::regions::*; +use std::collections::BTreeMap; + +pub fn compare_save_region_fixed_row_run_candidates( + left: &SmpSaveCompanyChairmanAnalysisReport, + right: &SmpSaveCompanyChairmanAnalysisReport, +) -> Option { + let left_probe = left.region_fixed_row_run_candidates.as_ref()?; + let right_probe = right.region_fixed_row_run_candidates.as_ref()?; + + let left_by_shape = left_probe + .candidates + .iter() + .enumerate() + .map(|(index, candidate)| (candidate.shape_signature.clone(), (index, candidate))) + .collect::>(); + let right_by_shape = right_probe + .candidates + .iter() + .enumerate() + .map(|(index, candidate)| (candidate.shape_signature.clone(), (index, candidate))) + .collect::>(); + + let mut shared_shape_matches = Vec::new(); + let mut shared_shape_family_matches = Vec::new(); + let mut left_only_shape_signatures = Vec::new(); + let mut right_only_shape_signatures = Vec::new(); + let mut left_only_shape_family_signatures = Vec::new(); + let mut right_only_shape_family_signatures = Vec::new(); + let left_family_by_shape = left_probe + .candidates + .iter() + .enumerate() + .map(|(index, candidate)| (candidate.shape_family_signature.clone(), (index, candidate))) + .collect::>(); + let right_family_by_shape = right_probe + .candidates + .iter() + .enumerate() + .map(|(index, candidate)| (candidate.shape_family_signature.clone(), (index, candidate))) + .collect::>(); + + for (shape_signature, (left_index, left_candidate)) in &left_by_shape { + if let Some((right_index, right_candidate)) = right_by_shape.get(shape_signature) { + shared_shape_matches.push(SmpSaveRegionFixedRowRunSharedShapeMatch { + shape_signature: shape_signature.clone(), + left_rank: left_index + 1, + left_rows_offset_hex: left_candidate.rows_offset_hex.clone(), + left_best_probable_density_lane_relative_offset_hex: left_candidate + .best_probable_density_lane_relative_offset_hex + .clone(), + right_rank: right_index + 1, + right_rows_offset_hex: right_candidate.rows_offset_hex.clone(), + right_best_probable_density_lane_relative_offset_hex: right_candidate + .best_probable_density_lane_relative_offset_hex + .clone(), + }); + } else { + left_only_shape_signatures.push(shape_signature.clone()); + } + } + + for shape_signature in right_by_shape.keys() { + if !left_by_shape.contains_key(shape_signature) { + right_only_shape_signatures.push(shape_signature.clone()); + } + } + + for (shape_family_signature, (left_index, left_candidate)) in &left_family_by_shape { + if let Some((right_index, right_candidate)) = + right_family_by_shape.get(shape_family_signature) + { + shared_shape_family_matches.push(SmpSaveRegionFixedRowRunSharedShapeMatch { + shape_signature: shape_family_signature.clone(), + left_rank: left_index + 1, + left_rows_offset_hex: left_candidate.rows_offset_hex.clone(), + left_best_probable_density_lane_relative_offset_hex: left_candidate + .best_probable_density_lane_relative_offset_hex + .clone(), + right_rank: right_index + 1, + right_rows_offset_hex: right_candidate.rows_offset_hex.clone(), + right_best_probable_density_lane_relative_offset_hex: right_candidate + .best_probable_density_lane_relative_offset_hex + .clone(), + }); + } else { + left_only_shape_family_signatures.push(shape_family_signature.clone()); + } + } + + for shape_family_signature in right_family_by_shape.keys() { + if !left_family_by_shape.contains_key(shape_family_signature) { + right_only_shape_family_signatures.push(shape_family_signature.clone()); + } + } + + Some(SmpSaveRegionFixedRowRunComparisonReport { + left_profile_family: left.profile_family.clone(), + right_profile_family: right.profile_family.clone(), + left_best_rows_offset_hex: left_probe + .candidates + .first() + .map(|candidate| candidate.rows_offset_hex.clone()), + right_best_rows_offset_hex: right_probe + .candidates + .first() + .map(|candidate| candidate.rows_offset_hex.clone()), + left_best_shape_signature: left_probe + .candidates + .first() + .map(|candidate| candidate.shape_signature.clone()), + right_best_shape_signature: right_probe + .candidates + .first() + .map(|candidate| candidate.shape_signature.clone()), + left_best_shape_family_signature: left_probe + .candidates + .first() + .map(|candidate| candidate.shape_family_signature.clone()), + right_best_shape_family_signature: right_probe + .candidates + .first() + .map(|candidate| candidate.shape_family_signature.clone()), + shared_shape_matches, + shared_shape_family_matches, + left_only_shape_signatures, + right_only_shape_signatures, + left_only_shape_family_signatures, + right_only_shape_family_signatures, + evidence: vec![ + format!( + "comparison keys the pre-region-header fixed-row candidates by derived lane-shape fingerprint instead of raw offset, because current grounded saves do not keep the same top rows_offset across files ({:?} vs {:?})", + left_probe.candidates.first().map(|candidate| candidate.rows_offset_hex.as_str()), + right_probe.candidates.first().map(|candidate| candidate.rows_offset_hex.as_str()) + ), + "shared shape matches mean two saves surfaced at least one candidate with the same exact probable-f32/small-unsigned/partial-zero/trailing-byte profile, while shared shape-family matches allow mild count drift inside the same dense lane family".to_string(), + ], + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/regions/fixed_rows.rs b/crates/rrt-runtime/src/inspect/smp/regions/fixed_rows.rs new file mode 100644 index 0000000..094a3cb --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/regions/fixed_rows.rs @@ -0,0 +1,305 @@ +use crate::inspect::smp::regions::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn parse_save_region_fixed_row_run_candidate_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + region_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + if file_extension_hint != Some("gms") { + return None; + } + let profile = container_profile?; + let region_header_probe = region_header_probe?; + let target_row_count = region_header_probe.live_record_count as usize; + if target_row_count == 0 { + return None; + } + let scan_end_offset = region_header_probe.metadata_tag_offset; + let row_span_len = target_row_count.checked_mul(SAVE_REGION_FIXED_ROW_STRIDE)?; + let scan_bytes = bytes.get(..scan_end_offset)?; + let mut candidates = find_u32_le_offsets(scan_bytes, region_header_probe.live_record_count) + .into_iter() + .filter_map(|count_offset| { + let rows_offset = count_offset.checked_add(4)?; + let rows_end_offset = rows_offset.checked_add(row_span_len)?; + if rows_end_offset > scan_end_offset { + return None; + } + let rows_bytes = bytes.get(rows_offset..rows_end_offset)?; + let mut dword_lane_summaries = + Vec::with_capacity(SAVE_REGION_FIXED_ROW_DWORD_LANE_COUNT); + let mut best_probable_density_lane = None::<(usize, usize)>; + for lane_index in 0..SAVE_REGION_FIXED_ROW_DWORD_LANE_COUNT { + let relative_offset = lane_index * 4; + let mut zero_count = 0usize; + let mut nonzero_count = 0usize; + let mut probable_normal_f32_count = 0usize; + let mut small_unsigned_count = 0usize; + let mut distinct_values = BTreeSet::new(); + let mut sample_values_hex = Vec::new(); + for row_index in 0..target_row_count { + let row_offset = row_index * SAVE_REGION_FIXED_ROW_STRIDE + relative_offset; + let raw_u32 = read_u32_at(rows_bytes, row_offset)?; + if raw_u32 == 0 { + zero_count += 1; + } else { + nonzero_count += 1; + } + if probable_normal_f32_string(raw_u32).is_some() { + probable_normal_f32_count += 1; + } + if raw_u32 <= 1024 { + small_unsigned_count += 1; + } + if distinct_values.insert(raw_u32) && sample_values_hex.len() < 6 { + sample_values_hex.push(format!("0x{raw_u32:08x}")); + } + } + if best_probable_density_lane + .is_none_or(|(_, best_count)| probable_normal_f32_count > best_count) + { + best_probable_density_lane = Some((relative_offset, probable_normal_f32_count)); + } + dword_lane_summaries.push(SmpSaveFixedRowRunDwordLaneSummary { + relative_offset, + relative_offset_hex: format!("0x{relative_offset:x}"), + zero_count, + nonzero_count, + distinct_value_count: distinct_values.len(), + probable_normal_f32_count, + small_unsigned_count, + sample_values_hex, + }); + } + let mut trailing_values = BTreeSet::new(); + let mut trailing_byte_zero_count = 0usize; + let mut trailing_byte_nonzero_count = 0usize; + let mut trailing_byte_sample_values_hex = Vec::new(); + for row_index in 0..target_row_count { + let value = *rows_bytes.get(row_index * SAVE_REGION_FIXED_ROW_STRIDE + 0x28)?; + if value == 0 { + trailing_byte_zero_count += 1; + } else { + trailing_byte_nonzero_count += 1; + } + if trailing_values.insert(value) && trailing_byte_sample_values_hex.len() < 8 { + trailing_byte_sample_values_hex.push(format!("0x{value:02x}")); + } + } + let shape_signature = build_save_region_fixed_row_run_candidate_shape_signature( + &dword_lane_summaries, + trailing_byte_zero_count, + trailing_values.len(), + target_row_count, + ); + let shape_family_signature = + build_save_region_fixed_row_run_candidate_shape_family_signature( + &dword_lane_summaries, + trailing_byte_zero_count, + trailing_values.len(), + target_row_count, + ); + Some(SmpSaveRegionFixedRowRunCandidate { + count_offset, + count_offset_hex: format!("0x{count_offset:x}"), + row_count: target_row_count, + row_stride: SAVE_REGION_FIXED_ROW_STRIDE, + row_stride_hex: format!("0x{:x}", SAVE_REGION_FIXED_ROW_STRIDE), + rows_offset, + rows_offset_hex: format!("0x{rows_offset:x}"), + rows_end_offset, + rows_end_offset_hex: format!("0x{rows_end_offset:x}"), + distance_to_region_metadata_tag: scan_end_offset.saturating_sub(rows_end_offset), + distance_to_region_metadata_tag_hex: format!( + "0x{:x}", + scan_end_offset.saturating_sub(rows_end_offset) + ), + dword_lane_summaries, + shape_signature, + shape_family_signature, + trailing_byte_zero_count, + trailing_byte_nonzero_count, + trailing_byte_distinct_value_count: trailing_values.len(), + trailing_byte_sample_values_hex, + best_probable_density_lane_relative_offset_hex: best_probable_density_lane + .filter(|(_, count)| *count != 0) + .map(|(relative_offset, _)| format!("0x{relative_offset:x}")), + }) + }) + .collect::>(); + candidates.sort_by_key(|candidate| { + ( + Reverse( + candidate + .dword_lane_summaries + .iter() + .map(|summary| summary.probable_normal_f32_count) + .max() + .unwrap_or_default(), + ), + candidate.distance_to_region_metadata_tag, + candidate.count_offset, + ) + }); + candidates.truncate(SAVE_REGION_FIXED_ROW_CANDIDATE_PROBE_LIMIT); + let candidate_count = candidates.len(); + let best_candidate_offset_hex = candidates + .first() + .map(|candidate| candidate.rows_offset_hex.clone()); + Some(SmpSaveRegionFixedRowRunCandidateProbe { + profile_family: profile.profile_family.clone(), + source_kind: "save-region-fixed-row-run-candidates".to_string(), + semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), + target_row_count, + target_row_stride: SAVE_REGION_FIXED_ROW_STRIDE, + target_row_stride_hex: format!("0x{:x}", SAVE_REGION_FIXED_ROW_STRIDE), + scan_start_offset: 0, + scan_start_offset_hex: "0x0".to_string(), + scan_end_offset, + scan_end_offset_hex: format!("0x{scan_end_offset:x}"), + candidates, + evidence: vec![ + format!( + "candidate scan looks for pre-region-header counted runs keyed to the grounded live region count {} with fixed row stride 0x{:x}", + target_row_count, SAVE_REGION_FIXED_ROW_STRIDE + ), + format!( + "current scan range ends at region metadata tag offset 0x{:x}, because the atlas restore order places the fixed rows before the tagged 0x5209/0x520a/0x520b region collection", + scan_end_offset + ), + format!( + "kept {} highest-signal candidates after sorting by probable-f32 lane density and proximity to the region metadata tag; best candidate rows offset is {:?}", + candidate_count, best_candidate_offset_hex + ), + ], + }) +} + +pub(in crate::inspect::smp) fn build_save_region_fixed_row_run_candidate_shape_signature( + dword_lane_summaries: &[SmpSaveFixedRowRunDwordLaneSummary], + trailing_byte_zero_count: usize, + trailing_byte_distinct_value_count: usize, + row_count: usize, +) -> String { + fn pick_lane_terms( + summaries: &[SmpSaveFixedRowRunDwordLaneSummary], + score: F, + include: P, + row_count: usize, + max_terms: usize, + ) -> Vec + where + F: Fn(&SmpSaveFixedRowRunDwordLaneSummary) -> usize, + P: Fn(&SmpSaveFixedRowRunDwordLaneSummary) -> bool, + { + let high_signal_threshold = row_count.saturating_mul(3) / 4; + let mut picked = summaries + .iter() + .filter(|summary| include(summary)) + .filter(|summary| score(summary) >= high_signal_threshold) + .map(|summary| { + ( + summary.relative_offset, + format!("{}:{}", summary.relative_offset_hex, score(summary)), + ) + }) + .collect::>(); + if picked.is_empty() { + picked = summaries + .iter() + .filter(|summary| include(summary)) + .filter_map(|summary| { + let value = score(summary); + (value != 0).then(|| { + ( + summary.relative_offset, + format!("{}:{}", summary.relative_offset_hex, value), + ) + }) + }) + .collect::>(); + picked.sort_by_key(|(relative_offset, term)| { + let value = term + .split(':') + .nth(1) + .and_then(|part| part.parse::().ok()) + .unwrap_or_default(); + (Reverse(value), *relative_offset) + }); + picked.truncate(max_terms); + } + picked.into_iter().map(|(_, term)| term).collect() + } + + let probable_terms = pick_lane_terms( + dword_lane_summaries, + |summary| summary.probable_normal_f32_count, + |_| true, + row_count, + 3, + ); + let small_terms = pick_lane_terms( + dword_lane_summaries, + |summary| summary.small_unsigned_count, + |summary| summary.nonzero_count != 0, + row_count, + 2, + ); + let zero_terms = pick_lane_terms( + dword_lane_summaries, + |summary| summary.zero_count, + |summary| summary.zero_count != row_count, + row_count, + 2, + ); + + format!( + "pf32=[{}]|small=[{}]|zero=[{}]|trail={}/{}", + probable_terms.join(","), + small_terms.join(","), + zero_terms.join(","), + trailing_byte_zero_count, + trailing_byte_distinct_value_count + ) +} + +pub(in crate::inspect::smp) fn build_save_region_fixed_row_run_candidate_shape_family_signature( + dword_lane_summaries: &[SmpSaveFixedRowRunDwordLaneSummary], + trailing_byte_zero_count: usize, + trailing_byte_distinct_value_count: usize, + row_count: usize, +) -> String { + let dense_pf32_offsets = dword_lane_summaries + .iter() + .filter(|summary| summary.probable_normal_f32_count >= row_count.saturating_mul(3) / 4) + .map(|summary| summary.relative_offset_hex.clone()) + .collect::>(); + let partial_zero_offsets = dword_lane_summaries + .iter() + .filter(|summary| { + summary.zero_count != 0 + && summary.zero_count != row_count + && summary.zero_count >= row_count.saturating_mul(5) / 100 + }) + .map(|summary| summary.relative_offset_hex.clone()) + .collect::>(); + let small_nonzero_offsets = dword_lane_summaries + .iter() + .filter(|summary| { + summary.nonzero_count != 0 + && summary.small_unsigned_count >= row_count.saturating_mul(8) / 100 + }) + .map(|summary| summary.relative_offset_hex.clone()) + .collect::>(); + + format!( + "dense_pf32=[{}]|small_nonzero=[{}]|partial_zero=[{}]|trail_bucket={}/{}", + dense_pf32_offsets.join(","), + small_nonzero_offsets.join(","), + partial_zero_offsets.join(","), + trailing_byte_zero_count / 8, + trailing_byte_distinct_value_count / 8 + ) +} diff --git a/crates/rrt-runtime/src/inspect/smp/regions/headers.rs b/crates/rrt-runtime/src/inspect/smp/regions/headers.rs new file mode 100644 index 0000000..3921174 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/regions/headers.rs @@ -0,0 +1,205 @@ +use crate::inspect::smp::regions::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn parse_save_company_collection_header_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + parse_save_tagged_collection_header_probe( + bytes, + file_extension_hint, + container_profile, + 0x000061a9, + 0x000061aa, + 0x000061ab, + "save-company-tagged-header-counts", + "scenario-save-company-header-counts", + |header| { + header.direct_collection_flag == 1 + && header.live_id_bound >= 1 + && header.live_id_bound <= 0x20 + && header.live_record_count <= header.live_id_bound + && header.direct_record_stride >= 0x1000 + }, + vec![ + "save-side company collection uses tagged header family 0x61a9/0x61aa/0x61ab".to_string(), + "package-save per-company callback is currently grounded as a no-op stub, so this probe only claims header-level collection counts, not per-company payload".to_string(), + ], + ) +} + +pub(in crate::inspect::smp) fn parse_save_chairman_profile_collection_header_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + parse_save_tagged_collection_header_probe( + bytes, + file_extension_hint, + container_profile, + 0x00005209, + 0x0000520a, + 0x0000520b, + "save-chairman-profile-tagged-header-counts", + "scenario-save-chairman-profile-header-counts", + |header| { + header.direct_collection_flag == 1 + && header.live_id_bound >= 1 + && header.live_id_bound <= 0x20 + && header.live_record_count <= header.live_id_bound + && header.direct_record_stride >= 0x800 + && header.direct_record_stride <= 0x2000 + }, + vec![ + "save-side chairman/profile collection uses tagged header family 0x5209/0x520a/0x520b".to_string(), + "the direct-record chairman/profile family is the large-stride tagged collection with embedded name and biography payload, not the smaller train-side 0x5209 family".to_string(), + ], + ) +} + +pub(in crate::inspect::smp) fn parse_save_train_collection_header_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + parse_save_tagged_collection_header_probe( + bytes, + file_extension_hint, + container_profile, + 0x00005209, + 0x0000520a, + 0x0000520b, + "save-train-tagged-header-counts", + "scenario-save-train-header-counts", + |header| { + header.direct_collection_flag == 1 + && header.direct_record_stride >= 0x100 + && header.direct_record_stride <= 0x400 + && header.live_id_bound >= 0x10 + && header.live_id_bound <= 0x100 + && header.live_record_count >= 1 + && header.live_record_count <= header.live_id_bound + }, + vec![ + "save-side live train collection shares tagged header family 0x5209/0x520a/0x520b with other indexed direct-record bundles".to_string(), + "the grounded train-side candidate is the smaller direct-record family with stride 0x1d5 whose metadata payload carries Train N labels, distinct from the larger chairman/profile family and the non-direct region family".to_string(), + ], + ) +} + +pub(in crate::inspect::smp) fn parse_save_train_collection_directory_probe( + bytes: &[u8], + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + let header_probe = header_probe?; + if header_probe.source_kind != "save-train-tagged-header-counts" { + return None; + } + let metadata_payload = + bytes.get(header_probe.metadata_tag_offset + 4..header_probe.records_tag_offset)?; + let directory_root_byte_offset = + SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX.checked_mul(4)?; + let live_record_count = header_probe.live_record_count as usize; + let directory_len_dwords = + live_record_count.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT)?; + let directory_len_bytes = directory_len_dwords.checked_mul(4)?; + let directory_bytes = metadata_payload.get( + directory_root_byte_offset..directory_root_byte_offset.checked_add(directory_len_bytes)?, + )?; + let mut entries = Vec::with_capacity(live_record_count); + for index in 0..live_record_count { + let entry_offset = + index.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT * 4)?; + let payload_relative_offset = read_u32_at(directory_bytes, entry_offset)?; + let previous_live_entry_id = read_u32_at(directory_bytes, entry_offset + 4)?; + let next_live_entry_id = read_u32_at(directory_bytes, entry_offset + 8)?; + entries.push(SmpSaveTrainCollectionDirectoryEntryProbe { + live_entry_id: (index + 1) as u32, + payload_relative_offset, + payload_relative_offset_hex: format!("0x{payload_relative_offset:08x}"), + payload_absolute_offset: header_probe + .metadata_tag_offset + .checked_add(4)? + .checked_add(payload_relative_offset as usize)?, + previous_live_entry_id, + previous_live_entry_id_hex: format!("0x{previous_live_entry_id:08x}"), + next_live_entry_id, + next_live_entry_id_hex: format!("0x{next_live_entry_id:08x}"), + }); + } + let chain_head_live_entry_id = entries + .iter() + .find(|entry| entry.previous_live_entry_id == 0) + .map(|entry| entry.live_entry_id); + let chain_tail_live_entry_id = entries + .iter() + .find(|entry| entry.next_live_entry_id == 0) + .map(|entry| entry.live_entry_id); + let monotonic_offsets = entries + .windows(2) + .all(|window| window[0].payload_relative_offset < window[1].payload_relative_offset); + Some(SmpSaveTrainCollectionDirectoryProbe { + profile_family: header_probe.profile_family.clone(), + source_kind: "save-train-live-directory".to_string(), + semantic_family: "scenario-save-train-live-directory".to_string(), + metadata_tag_offset: header_probe.metadata_tag_offset, + records_tag_offset: header_probe.records_tag_offset, + close_tag_offset: header_probe.close_tag_offset, + directory_root_dword_index: SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX, + directory_entry_dword_count: SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT, + live_record_count: header_probe.live_record_count, + live_id_bound: header_probe.live_id_bound, + chain_head_live_entry_id, + chain_tail_live_entry_id, + entries, + evidence: vec![ + "save-side train metadata payload exposes a live-entry directory immediately after the first 16 dwords, with payload-relative offsets pointing into the later records span".to_string(), + format!( + "train live directory decodes {} triplets of (payload_relative_offset, prev_live_entry_id, next_live_entry_id) from metadata dword index {}", + header_probe.live_record_count, + SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX + ), + format!( + "decoded directory preserves a head/tail chain {:?}->{:?} and monotonic payload offsets={monotonic_offsets}", + chain_head_live_entry_id, chain_tail_live_entry_id + ), + ], + }) +} + +pub(in crate::inspect::smp) fn parse_save_region_collection_header_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + let probe = parse_save_tagged_collection_header_probe( + bytes, + file_extension_hint, + container_profile, + 0x00005209, + 0x0000520a, + 0x0000520b, + "save-region-tagged-header-counts", + "scenario-save-region-header-counts", + |header| { + header.direct_collection_flag == 0 + && header.direct_record_stride == 0x06 + && header.live_id_bound >= 0x80 + && header.live_id_bound <= 0x200 + && header.live_record_count >= 0x80 + && header.live_record_count <= header.live_id_bound + }, + vec![ + "save-side live region collection shares tagged header family 0x5209/0x520a/0x520b with trains and chairman profiles, but uses the larger non-direct indexed family".to_string(), + "the grounded region-side candidate is the non-direct 0x5209 family with live_id_bound/count in the 0x96/0x91 range and Marker09-style default stems in the records span, distinct from the smaller direct train family".to_string(), + ], + )?; + let records_preview = bytes + .get(probe.records_tag_offset + 4..probe.close_tag_offset) + .unwrap_or(&[]); + records_preview + .windows("Marker09".len()) + .any(|window| window == b"Marker09") + .then_some(probe) +} diff --git a/crates/rrt-runtime/src/inspect/smp/regions/mod.rs b/crates/rrt-runtime/src/inspect/smp/regions/mod.rs new file mode 100644 index 0000000..2d66fb0 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/regions/mod.rs @@ -0,0 +1,27 @@ +use super::bundle::SmpContainerProfile; +use super::world::SmpSaveCompanyChairmanAnalysisReport; +use crate::inspect::smp::*; + +mod compare; +mod fixed_rows; +mod headers; +mod model; +mod queued_notice; +mod triplets; + +pub use super::common::{ + SmpSaveRegionProfileCollectionProbe, SmpSaveRegionProfileEntryProbe, + SmpSaveTaggedCollectionHeaderProbe, SmpSaveTrainCollectionDirectoryEntryProbe, + SmpSaveTrainCollectionDirectoryProbe, SmpSaveUnclassifiedTaggedCollectionHeaderProbe, +}; +pub use model::*; + +pub use compare::compare_save_region_fixed_row_run_candidates; +pub(in crate::inspect::smp) use fixed_rows::parse_save_region_fixed_row_run_candidate_probe; +pub(in crate::inspect::smp) use headers::{ + parse_save_chairman_profile_collection_header_probe, + parse_save_company_collection_header_probe, parse_save_region_collection_header_probe, + parse_save_train_collection_directory_probe, parse_save_train_collection_header_probe, +}; +pub(in crate::inspect::smp) use queued_notice::parse_save_region_queued_notice_record_probe; +pub(in crate::inspect::smp) use triplets::parse_save_region_record_triplet_probe; diff --git a/crates/rrt-runtime/src/inspect/smp/regions/model.rs b/crates/rrt-runtime/src/inspect/smp/regions/model.rs new file mode 100644 index 0000000..5f7b49d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/regions/model.rs @@ -0,0 +1,225 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::common::{SmpSaveDwordCandidate, SmpSaveRegionProfileCollectionProbe}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveRegionRecordTripletEntryProbe { + pub record_index: usize, + pub name: String, + pub record_payload_relative_offset: usize, + pub record_payload_relative_offset_hex: String, + pub name_tag_relative_offset: usize, + pub policy_tag_relative_offset: usize, + pub profile_tag_relative_offset: usize, + pub pre_name_prefix_len: usize, + #[serde(default)] + pub pre_name_prefix_hex_bytes: Vec, + #[serde(default)] + pub pre_name_prefix_dword_candidates: Vec, + pub policy_chunk_len: usize, + pub profile_chunk_len: usize, + pub policy_leading_f32_0: f32, + pub policy_leading_f32_1: f32, + pub policy_leading_f32_2: f32, + #[serde(default)] + pub policy_reserved_dwords: Vec, + #[serde(default)] + pub policy_reserved_dword_candidates: Vec, + pub policy_trailing_word: u16, + pub policy_trailing_word_hex: String, + #[serde(default)] + pub profile_collection: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveRegionRecordTripletProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub records_tag_offset: usize, + pub close_tag_offset: usize, + pub record_count: usize, + #[serde(default)] + pub entries: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpLoadedRegionProfileEntry { + pub entry_index: usize, + pub name: String, + pub trailing_weight_f32: f32, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpLoadedRegionProfileCollection { + pub direct_collection_flag: u32, + pub entry_stride: u32, + pub live_id_bound: u32, + pub live_record_count: u32, + pub trailing_padding_len: usize, + #[serde(default)] + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpLoadedRegionEntry { + pub record_index: usize, + pub name: String, + pub pre_name_prefix_len: usize, + pub policy_leading_f32_0: f32, + pub policy_leading_f32_1: f32, + pub policy_leading_f32_2: f32, + #[serde(default)] + pub policy_reserved_dwords: Vec, + pub policy_trailing_word: u16, + pub policy_trailing_word_hex: String, + #[serde(default)] + pub profile_collection: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpLoadedRegionCollection { + pub source_kind: String, + pub semantic_family: String, + pub observed_entry_count: usize, + #[serde(default)] + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedRegionFixedRowRunSummary { + pub source_kind: String, + pub semantic_family: String, + pub target_row_count: usize, + pub target_row_stride: usize, + pub target_row_stride_hex: String, + #[serde(default)] + pub candidates: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveRegionQueuedNoticeRecordEntryProbe { + pub node_base_offset: usize, + pub payload_seed_offset: usize, + pub next_link_raw: u32, + pub next_link_raw_hex: String, + pub payload_seed_dword: u32, + pub payload_seed_dword_hex: String, + pub kind: u32, + pub kind_hex: String, + pub promotion_latch_dword: u32, + pub promotion_latch_dword_hex: String, + pub region_id: u32, + pub region_id_hex: String, + pub amount: u32, + pub amount_hex: String, + pub trailing_sentinel_i32_0: i32, + pub trailing_sentinel_i32_0_hex: String, + pub trailing_sentinel_i32_1: i32, + pub trailing_sentinel_i32_1_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveRegionQueuedNoticeRecordProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub payload_seed_dword: u32, + pub payload_seed_dword_hex: String, + #[serde(default)] + pub entries: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveFixedRowRunDwordLaneSummary { + pub relative_offset: usize, + pub relative_offset_hex: String, + pub zero_count: usize, + pub nonzero_count: usize, + pub distinct_value_count: usize, + pub probable_normal_f32_count: usize, + pub small_unsigned_count: usize, + #[serde(default)] + pub sample_values_hex: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveRegionFixedRowRunCandidate { + pub count_offset: usize, + pub count_offset_hex: String, + pub row_count: usize, + pub row_stride: usize, + pub row_stride_hex: String, + pub rows_offset: usize, + pub rows_offset_hex: String, + pub rows_end_offset: usize, + pub rows_end_offset_hex: String, + pub distance_to_region_metadata_tag: usize, + pub distance_to_region_metadata_tag_hex: String, + #[serde(default)] + pub dword_lane_summaries: Vec, + pub shape_signature: String, + pub shape_family_signature: String, + pub trailing_byte_zero_count: usize, + pub trailing_byte_nonzero_count: usize, + pub trailing_byte_distinct_value_count: usize, + #[serde(default)] + pub trailing_byte_sample_values_hex: Vec, + #[serde(default)] + pub best_probable_density_lane_relative_offset_hex: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveRegionFixedRowRunCandidateProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub target_row_count: usize, + pub target_row_stride: usize, + pub target_row_stride_hex: String, + pub scan_start_offset: usize, + pub scan_start_offset_hex: String, + pub scan_end_offset: usize, + pub scan_end_offset_hex: String, + #[serde(default)] + pub candidates: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveRegionFixedRowRunSharedShapeMatch { + pub shape_signature: String, + pub left_rank: usize, + pub left_rows_offset_hex: String, + pub left_best_probable_density_lane_relative_offset_hex: Option, + pub right_rank: usize, + pub right_rows_offset_hex: String, + pub right_best_probable_density_lane_relative_offset_hex: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveRegionFixedRowRunComparisonReport { + pub left_profile_family: String, + pub right_profile_family: String, + pub left_best_rows_offset_hex: Option, + pub right_best_rows_offset_hex: Option, + pub left_best_shape_signature: Option, + pub right_best_shape_signature: Option, + pub left_best_shape_family_signature: Option, + pub right_best_shape_family_signature: Option, + #[serde(default)] + pub shared_shape_matches: Vec, + #[serde(default)] + pub shared_shape_family_matches: Vec, + #[serde(default)] + pub left_only_shape_signatures: Vec, + #[serde(default)] + pub right_only_shape_signatures: Vec, + #[serde(default)] + pub left_only_shape_family_signatures: Vec, + #[serde(default)] + pub right_only_shape_family_signatures: Vec, + pub evidence: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/regions/queued_notice.rs b/crates/rrt-runtime/src/inspect/smp/regions/queued_notice.rs new file mode 100644 index 0000000..e1ccd71 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/regions/queued_notice.rs @@ -0,0 +1,77 @@ +use crate::inspect::smp::regions::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn parse_save_region_queued_notice_record_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + region_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + if file_extension_hint != Some("gms") { + return None; + } + let profile = container_profile?; + let max_region_id = region_header_probe + .map(|probe| probe.live_id_bound) + .unwrap_or(0x1000); + let entries = find_u32_le_offsets(bytes, SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED) + .into_iter() + .filter_map(|payload_seed_offset| { + let node_base_offset = payload_seed_offset.checked_sub(4)?; + let _node_bytes = bytes + .get(node_base_offset..node_base_offset + SAVE_REGION_QUEUED_NOTICE_NODE_LEN)?; + let next_link_raw = read_u32_at(bytes, node_base_offset)?; + let kind = read_u32_at(bytes, node_base_offset + 8)?; + let promotion_latch_dword = read_u32_at(bytes, node_base_offset + 12)?; + let region_id = read_u32_at(bytes, node_base_offset + 16)?; + let amount = read_u32_at(bytes, node_base_offset + 20)?; + let trailing_sentinel_i32_0 = read_i32_at(bytes, node_base_offset + 24)?; + let trailing_sentinel_i32_1 = read_i32_at(bytes, node_base_offset + 28)?; + if !(kind == SAVE_REGION_QUEUED_NOTICE_NODE_KIND + && promotion_latch_dword == 0 + && region_id >= 1 + && region_id <= max_region_id + && amount > 0 + && trailing_sentinel_i32_0 == -1 + && trailing_sentinel_i32_1 == -1) + { + return None; + } + Some(SmpSaveRegionQueuedNoticeRecordEntryProbe { + node_base_offset, + payload_seed_offset, + next_link_raw, + next_link_raw_hex: format!("0x{next_link_raw:08x}"), + payload_seed_dword: SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED, + payload_seed_dword_hex: format!("0x{SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED:08x}"), + kind, + kind_hex: format!("0x{kind:08x}"), + promotion_latch_dword, + promotion_latch_dword_hex: format!("0x{promotion_latch_dword:08x}"), + region_id, + region_id_hex: format!("0x{region_id:08x}"), + amount, + amount_hex: format!("0x{amount:08x}"), + trailing_sentinel_i32_0, + trailing_sentinel_i32_0_hex: format!("0x{:08x}", trailing_sentinel_i32_0 as u32), + trailing_sentinel_i32_1, + trailing_sentinel_i32_1_hex: format!("0x{:08x}", trailing_sentinel_i32_1 as u32), + }) + }) + .collect::>(); + if entries.is_empty() { + return None; + } + Some(SmpSaveRegionQueuedNoticeRecordProbe { + profile_family: profile.profile_family.clone(), + source_kind: "save-region-queued-notice-records".to_string(), + semantic_family: "scenario-save-region-queued-notice-records".to_string(), + payload_seed_dword: SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED, + payload_seed_dword_hex: format!("0x{SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED:08x}"), + entries, + evidence: vec![ + "save-side scan searches for the grounded region queued-notice payload seed 0x005c87a8 and validates the full 0x20-byte node shape from the atlas-backed queue owner".to_string(), + "accepted nodes require kind=7, promotion-latch dword=0, a bounded live region id, a positive amount, and trailing sentinel dwords -1/-1".to_string(), + ], + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/regions/triplets.rs b/crates/rrt-runtime/src/inspect/smp/regions/triplets.rs new file mode 100644 index 0000000..e04d216 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/regions/triplets.rs @@ -0,0 +1,263 @@ +use crate::inspect::smp::regions::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn parse_save_region_record_triplet_probe( + bytes: &[u8], + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + let header_probe = header_probe?; + if header_probe.source_kind != "save-region-tagged-header-counts" { + return None; + } + let records_payload = + bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?; + let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG); + let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG); + let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG); + let record_count = header_probe.live_record_count as usize; + if name_offsets.len() != record_count + || policy_offsets.len() != record_count + || profile_offsets.len() != record_count + { + return None; + } + let region_payload_start_offsets = bytes + .get(header_probe.metadata_tag_offset + 4..header_probe.records_tag_offset) + .and_then(|metadata_payload| { + let directory_root_byte_offset = + SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX.checked_mul(4)?; + let directory_len_dwords = + record_count.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT)?; + let directory_len_bytes = directory_len_dwords.checked_mul(4)?; + let directory_bytes = metadata_payload.get( + directory_root_byte_offset + ..directory_root_byte_offset.checked_add(directory_len_bytes)?, + )?; + let records_payload_absolute_offset = header_probe.records_tag_offset.checked_add(4)?; + (0..record_count) + .map(|index| { + let entry_offset = index + .checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT * 4)?; + let payload_relative_offset = + read_u32_at(directory_bytes, entry_offset)? as usize; + let payload_absolute_offset = header_probe + .metadata_tag_offset + .checked_add(4)? + .checked_add(payload_relative_offset)?; + let payload_start = + payload_absolute_offset.checked_sub(records_payload_absolute_offset)?; + (payload_start <= records_payload.len()).then_some(payload_start) + }) + .collect::>>() + }) + .unwrap_or_else(|| name_offsets.clone()); + let mut entries = Vec::with_capacity(record_count); + for index in 0..record_count { + let record_payload_relative_offset = region_payload_start_offsets[index]; + let name_tag_relative_offset = name_offsets[index]; + let policy_tag_relative_offset = policy_offsets[index]; + let profile_tag_relative_offset = profile_offsets[index]; + let next_record_relative_offset = name_offsets + .get(index + 1) + .copied() + .unwrap_or(records_payload.len()); + if record_payload_relative_offset > name_tag_relative_offset { + return None; + } + if !(name_tag_relative_offset < policy_tag_relative_offset + && policy_tag_relative_offset < profile_tag_relative_offset + && profile_tag_relative_offset < next_record_relative_offset) + { + return None; + } + let pre_name_prefix = + records_payload.get(record_payload_relative_offset..name_tag_relative_offset)?; + let pre_name_prefix_dword_candidates = build_region_record_prefix_dword_candidates( + record_payload_relative_offset, + pre_name_prefix, + ); + let name_payload = + records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?; + let name = parse_save_len_prefixed_ascii_name(name_payload)?; + let policy_chunk_len = + profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?; + if policy_chunk_len != 0x1a { + return None; + } + let policy_payload = + records_payload.get(policy_tag_relative_offset + 4..profile_tag_relative_offset)?; + let policy_leading_f32_0 = f32::from_bits(read_u32_at(policy_payload, 0)?); + let policy_leading_f32_1 = f32::from_bits(read_u32_at(policy_payload, 4)?); + let policy_leading_f32_2 = f32::from_bits(read_u32_at(policy_payload, 8)?); + let mut policy_reserved_dwords = Vec::with_capacity(3); + let mut policy_reserved_dword_candidates = Vec::with_capacity(3); + for dword_index in 0..3 { + let reserved_relative_offset = 12 + dword_index * 4; + let raw_u32 = read_u32_at(policy_payload, reserved_relative_offset)?; + policy_reserved_dwords.push(raw_u32); + let relative_offset = record_payload_relative_offset + + policy_tag_relative_offset + + 4 + + reserved_relative_offset; + policy_reserved_dword_candidates.push(SmpSaveDwordCandidate { + label: format!("policy_reserved_word_{}", dword_index + 1), + relative_offset, + relative_offset_hex: format!("0x{relative_offset:x}"), + raw_u32, + raw_u32_hex: format!("0x{raw_u32:08x}"), + value_i32: raw_u32 as i32, + value_f32: f32::from_bits(raw_u32), + }); + } + let policy_trailing_word = read_u16_at(policy_payload, 24)?; + let profile_chunk_len = + next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?; + let profile_payload = + records_payload.get(profile_tag_relative_offset + 4..next_record_relative_offset)?; + let profile_collection = parse_save_region_profile_collection_probe(profile_payload); + entries.push(SmpSaveRegionRecordTripletEntryProbe { + record_index: index, + name, + record_payload_relative_offset, + record_payload_relative_offset_hex: format!("0x{record_payload_relative_offset:x}"), + name_tag_relative_offset, + policy_tag_relative_offset, + profile_tag_relative_offset, + pre_name_prefix_len: pre_name_prefix.len(), + pre_name_prefix_hex_bytes: pre_name_prefix + .iter() + .map(|byte| format!("0x{byte:02x}")) + .collect(), + pre_name_prefix_dword_candidates, + policy_chunk_len, + profile_chunk_len, + policy_leading_f32_0, + policy_leading_f32_1, + policy_leading_f32_2, + policy_reserved_dwords, + policy_reserved_dword_candidates, + policy_trailing_word, + policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"), + profile_collection, + }); + } + let zero_trailing_padding_record_count = entries + .iter() + .filter(|entry| { + entry + .profile_collection + .as_ref() + .is_some_and(|collection| collection.trailing_padding_len == 0) + }) + .count(); + let records_with_nonzero_pre_name_prefix = entries + .iter() + .filter(|entry| entry.pre_name_prefix_len != 0) + .count(); + let records_with_prefix_dword_candidates = entries + .iter() + .filter(|entry| !entry.pre_name_prefix_dword_candidates.is_empty()) + .count(); + let records_with_any_nonzero_policy_reserved_dword = entries + .iter() + .filter(|entry| { + entry + .policy_reserved_dwords + .iter() + .any(|raw_u32| *raw_u32 != 0) + }) + .count(); + let policy_reserved_nonzero_counts = (0..3) + .map(|dword_index| { + entries + .iter() + .filter(|entry| entry.policy_reserved_dwords[dword_index] != 0) + .count() + }) + .collect::>(); + let unique_nonzero_policy_reserved_triplets = entries + .iter() + .filter_map(|entry| { + let triplet = [ + entry.policy_reserved_dwords[0], + entry.policy_reserved_dwords[1], + entry.policy_reserved_dwords[2], + ]; + triplet + .iter() + .any(|raw_u32| *raw_u32 != 0) + .then_some(triplet) + }) + .collect::>() + .into_iter() + .collect::>(); + let unique_pre_name_prefix_lens = entries + .iter() + .map(|entry| entry.pre_name_prefix_len) + .collect::>() + .into_iter() + .collect::>(); + Some(SmpSaveRegionRecordTripletProbe { + profile_family: header_probe.profile_family.clone(), + source_kind: "save-region-record-triplets".to_string(), + semantic_family: "scenario-save-region-record-triplets".to_string(), + records_tag_offset: header_probe.records_tag_offset, + close_tag_offset: header_probe.close_tag_offset, + record_count, + entries, + evidence: vec![ + "save-side region records in the non-direct Marker09 family are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the records span".to_string(), + format!( + "decoded {} region record triplets with one len-prefixed name chunk, one fixed policy chunk, and one trailing profile payload chunk per record", + record_count + ), + "each fixed 0x55f2 policy chunk currently decodes as three leading f32 lanes, three reserved dwords, and one trailing u16 word".to_string(), + "the trailing 0x55f3 payload also carries an embedded direct profile collection with fixed 0x22-byte rows on grounded saves".to_string(), + format!( + "live-entry directory now also grounds the actual 0x520a payload starts: {} of {} records currently have nonzero bytes before the first 0x55f1 tag, with unique pre-name prefix lengths {:?}", + records_with_nonzero_pre_name_prefix, + record_count, + unique_pre_name_prefix_lens + ), + format!( + "structured pre-name prefix dword candidates are currently present on {} of {} decoded region records", + records_with_prefix_dword_candidates, + record_count + ), + format!( + "fixed 0x55f2 policy reserved dwords are nonzero on {} of {} decoded region records, with per-word nonzero counts {:?} and unique nonzero triplets {:?}", + records_with_any_nonzero_policy_reserved_dword, + record_count, + policy_reserved_nonzero_counts, + unique_nonzero_policy_reserved_triplets + ), + format!( + "on grounded saves the 0x55f3 payload is fully consumed by that embedded profile collection: all {} decoded records currently have zero trailing padding beyond the direct profile rows", + zero_trailing_padding_record_count + ), + ], + }) +} + +pub(in crate::inspect::smp) fn build_region_record_prefix_dword_candidates( + record_payload_relative_offset: usize, + prefix_bytes: &[u8], +) -> Vec { + prefix_bytes + .chunks_exact(4) + .enumerate() + .map(|(index, chunk)| { + let raw_u32 = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + let relative_offset = record_payload_relative_offset + index * 4; + SmpSaveDwordCandidate { + label: format!("pre_name_prefix_word_{}", index + 1), + relative_offset, + relative_offset_hex: format!("0x{relative_offset:x}"), + raw_u32, + raw_u32_hex: format!("0x{raw_u32:08x}"), + value_i32: raw_u32 as i32, + value_f32: f32::from_bits(raw_u32), + } + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/save_load/assembly.rs b/crates/rrt-runtime/src/inspect/smp/save_load/assembly.rs new file mode 100644 index 0000000..a8fb37a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/save_load/assembly.rs @@ -0,0 +1,257 @@ +use crate::inspect::smp::save_load::*; + +pub(super) struct LoadedSaveSliceParts { + pub(super) profile: Option, + pub(super) candidate_availability_table: Option, + pub(super) named_locomotive_availability_table: + Option, + pub(super) locomotive_catalog: Option, + pub(super) cargo_catalog: Option, + pub(super) world_issue_37_state: Option, + pub(super) world_economic_tuning_state: Option, + pub(super) world_finance_neighborhood_state: Option, + pub(super) world_locomotive_policy_state: Option, + pub(super) company_roster: Option, + pub(super) chairman_profile_table: Option, + pub(super) region_collection: Option, + pub(super) region_fixed_row_run_summary: Option, + pub(super) placed_structure_collection: Option, + pub(super) placed_structure_dynamic_side_buffer_probe: + Option, + pub(super) placed_structure_dynamic_side_buffer_summary: + Option, + pub(super) special_conditions_table: Option, + pub(super) event_runtime_collection: Option, +} + +pub(super) fn derive_loaded_save_slice_parts(report: &SmpInspectionReport) -> LoadedSaveSliceParts { + let profile = if let Some(probe) = &report.classic_rehydrate_profile_probe { + Some(SmpLoadedProfile { + profile_kind: "classic-rehydrate-profile".to_string(), + profile_family: probe.profile_family.clone(), + packed_profile_offset: probe.packed_profile_offset, + packed_profile_len: probe.packed_profile_len, + packed_profile_len_hex: probe.packed_profile_len_hex.clone(), + leading_word_0: probe.packed_profile_block.leading_word_0, + leading_word_0_hex: probe.packed_profile_block.leading_word_0_hex.clone(), + header_flag_word_3: None, + header_flag_word_3_hex: None, + map_path: probe.packed_profile_block.map_path.clone(), + display_name: probe.packed_profile_block.display_name.clone(), + profile_byte_0x77: probe.packed_profile_block.profile_byte_0x77, + profile_byte_0x77_hex: probe.packed_profile_block.profile_byte_0x77_hex.clone(), + profile_byte_0x82: probe.packed_profile_block.profile_byte_0x82, + profile_byte_0x82_hex: probe.packed_profile_block.profile_byte_0x82_hex.clone(), + profile_byte_0x97: probe.packed_profile_block.profile_byte_0x97, + profile_byte_0x97_hex: probe.packed_profile_block.profile_byte_0x97_hex.clone(), + profile_byte_0xc5: probe.packed_profile_block.profile_byte_0xc5, + profile_byte_0xc5_hex: probe.packed_profile_block.profile_byte_0xc5_hex.clone(), + }) + } else { + report + .rt3_105_packed_profile_probe + .as_ref() + .map(|probe| SmpLoadedProfile { + profile_kind: "rt3-105-packed-profile".to_string(), + profile_family: probe.profile_family.clone(), + packed_profile_offset: probe.packed_profile_offset, + packed_profile_len: probe.packed_profile_len, + packed_profile_len_hex: probe.packed_profile_len_hex.clone(), + leading_word_0: probe.packed_profile_block.leading_word_0, + leading_word_0_hex: probe.packed_profile_block.leading_word_0_hex.clone(), + header_flag_word_3: Some(probe.packed_profile_block.header_flag_word_3), + header_flag_word_3_hex: Some( + probe.packed_profile_block.header_flag_word_3_hex.clone(), + ), + map_path: probe.packed_profile_block.map_path.clone(), + display_name: probe.packed_profile_block.display_name.clone(), + profile_byte_0x77: probe.packed_profile_block.profile_byte_0x77, + profile_byte_0x77_hex: probe.packed_profile_block.profile_byte_0x77_hex.clone(), + profile_byte_0x82: probe.packed_profile_block.profile_byte_0x82, + profile_byte_0x82_hex: probe.packed_profile_block.profile_byte_0x82_hex.clone(), + profile_byte_0x97: probe.packed_profile_block.profile_byte_0x97, + profile_byte_0x97_hex: probe.packed_profile_block.profile_byte_0x97_hex.clone(), + profile_byte_0xc5: probe.packed_profile_block.profile_byte_0xc5, + profile_byte_0xc5_hex: probe.packed_profile_block.profile_byte_0xc5_hex.clone(), + }) + }; + let candidate_availability_table = report.rt3_105_save_name_table_probe.as_ref().map(|probe| { + SmpLoadedCandidateAvailabilityTable { + source_kind: probe.source_kind.clone(), + semantic_family: probe.semantic_family.clone(), + header_offset: probe.header_offset, + entries_offset: probe.entries_offset, + entries_end_offset: probe.entries_end_offset, + observed_entry_count: probe.observed_entry_count, + zero_availability_count: probe.zero_trailer_entry_count, + zero_availability_names: probe.zero_trailer_entry_names.clone(), + footer_progress_hex_words: vec![ + probe.footer_progress_word_0_hex.clone(), + probe.footer_progress_word_1_hex.clone(), + ], + entries: probe.entries.clone(), + } + }); + let named_locomotive_availability_table = report + .rt3_105_save_named_locomotive_availability_probe + .as_ref() + .map(|probe| SmpLoadedNamedLocomotiveAvailabilityTable { + source_kind: probe.source_kind.clone(), + semantic_family: probe.semantic_family.clone(), + header_offset: None, + entries_offset: Some(probe.entries_offset), + entries_end_offset: Some(probe.entries_end_offset), + observed_entry_count: probe.observed_entry_count, + zero_availability_count: probe.zero_availability_count, + zero_availability_names: probe.zero_availability_names.clone(), + entries: probe.entries.clone(), + }); + let locomotive_catalog = named_locomotive_availability_table + .as_ref() + .and_then(derive_locomotive_catalog_from_named_availability_table); + let cargo_catalog = report + .recipe_book_summary_probe + .as_ref() + .and_then(derive_cargo_catalog_from_recipe_book_probe); + let world_issue_37_state = report + .save_world_issue_37_probe + .as_ref() + .map(derive_loaded_world_issue_37_state_from_probe); + let world_economic_tuning_state = report + .save_world_economic_tuning_probe + .as_ref() + .map(derive_loaded_world_economic_tuning_state_from_probe); + let world_finance_neighborhood_state = report + .save_world_finance_neighborhood_probe + .as_ref() + .map(derive_loaded_world_finance_neighborhood_state_from_probe); + let world_locomotive_policy_state = derive_loaded_world_locomotive_policy_state_from_probes( + report.post_text_field_neighborhood_probe.as_ref(), + report.locomotive_policy_neighborhood_probe.as_ref(), + ); + let company_roster = report.save_company_roster_probe.clone().or_else(|| { + report + .save_world_selection_context_probe + .as_ref() + .and_then(|probe| { + derive_selection_only_company_roster_from_save_world_probe( + probe, + report.save_company_collection_header_probe.as_ref(), + ) + }) + }); + let chairman_profile_table = report + .save_chairman_profile_table_probe + .clone() + .or_else(|| { + report + .save_world_selection_context_probe + .as_ref() + .and_then(|probe| { + derive_selection_only_chairman_profile_table_from_save_world_probe( + probe, + report + .save_chairman_profile_collection_header_probe + .as_ref(), + ) + }) + }); + let region_collection = report + .save_region_record_triplet_probe + .as_ref() + .map(derive_loaded_region_collection_from_probe); + let region_fixed_row_run_summary = report + .save_region_fixed_row_run_candidate_probe + .as_ref() + .map(derive_loaded_region_fixed_row_run_summary_from_probe); + let placed_structure_collection = report + .save_placed_structure_record_triplet_probe + .as_ref() + .map(derive_loaded_placed_structure_collection_from_probe); + let special_conditions_table = + report + .special_conditions_probe + .as_ref() + .map(|probe| SmpLoadedSpecialConditionsTable { + source_kind: probe.source_kind.clone(), + table_offset: probe.table_offset, + table_len: probe.table_len, + enabled_visible_count: probe.enabled_visible_count, + enabled_visible_labels: probe.enabled_visible_labels.clone(), + entries: probe.entries.clone(), + }); + let placed_structure_dynamic_side_buffer_probe = report + .save_placed_structure_dynamic_side_buffer_probe + .clone(); + let placed_structure_dynamic_side_buffer_alignment = report + .save_placed_structure_dynamic_side_buffer_probe + .as_ref() + .zip(report.save_placed_structure_record_triplet_probe.as_ref()) + .map(|(side_buffer, triplets)| { + summarize_placed_structure_dynamic_side_buffer_alignment(side_buffer, triplets) + }); + let placed_structure_dynamic_side_buffer_summary = report + .save_placed_structure_dynamic_side_buffer_probe + .as_ref() + .map(|probe| { + derive_loaded_placed_structure_dynamic_side_buffer_summary( + probe, + placed_structure_dynamic_side_buffer_alignment.as_ref(), + ) + }); + + LoadedSaveSliceParts { + profile, + candidate_availability_table, + named_locomotive_availability_table, + locomotive_catalog, + cargo_catalog, + world_issue_37_state, + world_economic_tuning_state, + world_finance_neighborhood_state, + world_locomotive_policy_state, + company_roster, + chairman_profile_table, + region_collection, + region_fixed_row_run_summary, + placed_structure_collection, + placed_structure_dynamic_side_buffer_probe, + placed_structure_dynamic_side_buffer_summary, + special_conditions_table, + event_runtime_collection: report.event_runtime_collection_summary.clone(), + } +} + +pub(super) fn finish_loaded_save_slice( + summary: &SmpSaveLoadSummary, + parts: LoadedSaveSliceParts, + notes: Vec, +) -> SmpLoadedSaveSlice { + SmpLoadedSaveSlice { + file_extension_hint: summary.file_extension_hint.clone(), + container_profile_family: summary.container_profile_family.clone(), + mechanism_family: summary.mechanism_family.clone(), + mechanism_confidence: summary.mechanism_confidence.clone(), + trailer_family: summary.trailer_family.clone(), + bridge_family: summary.bridge_family.clone(), + profile: parts.profile, + candidate_availability_table: parts.candidate_availability_table, + named_locomotive_availability_table: parts.named_locomotive_availability_table, + locomotive_catalog: parts.locomotive_catalog, + cargo_catalog: parts.cargo_catalog, + world_issue_37_state: parts.world_issue_37_state, + world_economic_tuning_state: parts.world_economic_tuning_state, + world_finance_neighborhood_state: parts.world_finance_neighborhood_state, + world_locomotive_policy_state: parts.world_locomotive_policy_state, + company_roster: parts.company_roster, + chairman_profile_table: parts.chairman_profile_table, + region_collection: parts.region_collection, + region_fixed_row_run_summary: parts.region_fixed_row_run_summary, + placed_structure_collection: parts.placed_structure_collection, + placed_structure_dynamic_side_buffer_summary: parts + .placed_structure_dynamic_side_buffer_summary, + special_conditions_table: parts.special_conditions_table, + event_runtime_collection: parts.event_runtime_collection, + notes, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/save_load/entrypoints.rs b/crates/rrt-runtime/src/inspect/smp/save_load/entrypoints.rs new file mode 100644 index 0000000..2013919 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/save_load/entrypoints.rs @@ -0,0 +1,23 @@ +use std::path::Path; + +use super::assembly::{derive_loaded_save_slice_parts, finish_loaded_save_slice}; +use super::notes::build_loaded_save_slice_notes; +use crate::inspect::smp::save_load::*; + +pub fn load_save_slice_file(path: &Path) -> Result> { + let inspection = inspect_smp_file(path)?; + load_save_slice_from_report(&inspection) + .map_err(|err| -> Box { err.into() }) +} + +pub fn load_save_slice_from_report( + report: &SmpInspectionReport, +) -> Result { + let summary = report + .save_load_summary + .as_ref() + .ok_or_else(|| "inspection did not expose a recognizable save-load summary".to_string())?; + let parts = derive_loaded_save_slice_parts(report); + let notes = build_loaded_save_slice_notes(report, summary, &parts); + Ok(finish_loaded_save_slice(summary, parts, notes)) +} diff --git a/crates/rrt-runtime/src/inspect/smp/save_load/loaded_regions.rs b/crates/rrt-runtime/src/inspect/smp/save_load/loaded_regions.rs new file mode 100644 index 0000000..70759c4 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/save_load/loaded_regions.rs @@ -0,0 +1,57 @@ +use crate::inspect::smp::save_load::*; + +pub(in crate::inspect::smp) fn derive_loaded_region_collection_from_probe( + probe: &SmpSaveRegionRecordTripletProbe, +) -> SmpLoadedRegionCollection { + SmpLoadedRegionCollection { + source_kind: probe.source_kind.clone(), + semantic_family: "scenario-save-region-triplet-collection".to_string(), + observed_entry_count: probe.record_count, + entries: probe + .entries + .iter() + .map(|entry| SmpLoadedRegionEntry { + record_index: entry.record_index, + name: entry.name.clone(), + pre_name_prefix_len: entry.pre_name_prefix_len, + policy_leading_f32_0: entry.policy_leading_f32_0, + policy_leading_f32_1: entry.policy_leading_f32_1, + policy_leading_f32_2: entry.policy_leading_f32_2, + policy_reserved_dwords: entry.policy_reserved_dwords.clone(), + policy_trailing_word: entry.policy_trailing_word, + policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(), + profile_collection: entry.profile_collection.as_ref().map(|collection| { + SmpLoadedRegionProfileCollection { + direct_collection_flag: collection.direct_collection_flag, + entry_stride: collection.entry_stride, + live_id_bound: collection.live_id_bound, + live_record_count: collection.live_record_count, + trailing_padding_len: collection.trailing_padding_len, + entries: collection + .entries + .iter() + .map(|entry| SmpLoadedRegionProfileEntry { + entry_index: entry.entry_index, + name: entry.name.clone(), + trailing_weight_f32: entry.trailing_weight_f32, + }) + .collect(), + } + }), + }) + .collect(), + } +} + +pub(in crate::inspect::smp) fn derive_loaded_region_fixed_row_run_summary_from_probe( + probe: &SmpSaveRegionFixedRowRunCandidateProbe, +) -> SmpLoadedRegionFixedRowRunSummary { + SmpLoadedRegionFixedRowRunSummary { + source_kind: probe.source_kind.clone(), + semantic_family: "scenario-save-region-fixed-row-run-summary".to_string(), + target_row_count: probe.target_row_count, + target_row_stride: probe.target_row_stride, + target_row_stride_hex: probe.target_row_stride_hex.clone(), + candidates: probe.candidates.clone(), + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/save_load/mod.rs b/crates/rrt-runtime/src/inspect/smp/save_load/mod.rs new file mode 100644 index 0000000..9579cf2 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/save_load/mod.rs @@ -0,0 +1,23 @@ +use super::bundle::*; +use super::events::*; +use super::profiles::*; +use super::regions::*; +use super::special_conditions::*; +use super::structures::*; +use super::world::*; + +mod assembly; +mod entrypoints; +mod loaded_regions; +mod model; +mod notes; +mod summary; + +pub use model::*; + +pub use entrypoints::{load_save_slice_file, load_save_slice_from_report}; +pub(in crate::inspect::smp) use loaded_regions::{ + derive_loaded_region_collection_from_probe, + derive_loaded_region_fixed_row_run_summary_from_probe, +}; +pub(in crate::inspect::smp) use summary::build_save_load_summary; diff --git a/crates/rrt-runtime/src/inspect/smp/save_load/model.rs b/crates/rrt-runtime/src/inspect/smp/save_load/model.rs new file mode 100644 index 0000000..cfdf037 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/save_load/model.rs @@ -0,0 +1,190 @@ +use serde::{Deserialize, Serialize}; + +use crate::event::targets::RuntimeCargoClass; +use crate::inspect::smp::events::SmpLoadedEventRuntimeCollectionSummary; +use crate::inspect::smp::profiles::SmpRt3105SaveNameTableEntry; +use crate::inspect::smp::regions::{SmpLoadedRegionCollection, SmpLoadedRegionFixedRowRunSummary}; +use crate::inspect::smp::special_conditions::SmpLoadedSpecialConditionsTable; +use crate::inspect::smp::structures::{ + SmpLoadedPlacedStructureCollection, SmpLoadedPlacedStructureDynamicSideBufferSummary, +}; +use crate::inspect::smp::world::{ + SmpLoadedChairmanProfileTable, SmpLoadedCompanyRoster, SmpLoadedWorldEconomicTuningState, + SmpLoadedWorldFinanceNeighborhoodState, SmpLoadedWorldIssue37State, + SmpLoadedWorldLocomotivePolicyState, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveLoadCandidateTableSummary { + pub source_kind: String, + pub semantic_family: String, + pub observed_entry_count: usize, + pub zero_availability_count: usize, + pub zero_availability_names: Vec, + pub footer_progress_hex_words: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveLoadSummary { + pub file_extension_hint: Option, + pub container_profile_family: Option, + pub mechanism_family: String, + pub mechanism_confidence: String, + pub packed_profile_kind: Option, + pub packed_profile_family: Option, + pub packed_profile_offset: Option, + pub packed_profile_len: Option, + pub map_path: Option, + pub display_name: Option, + pub profile_byte_0x77: Option, + pub profile_byte_0x77_hex: Option, + pub profile_byte_0x82: Option, + pub profile_byte_0x82_hex: Option, + pub profile_byte_0x97: Option, + pub profile_byte_0x97_hex: Option, + pub profile_byte_0xc5: Option, + pub profile_byte_0xc5_hex: Option, + pub trailer_family: Option, + pub bridge_family: Option, + pub candidate_table: Option, + pub notes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedProfile { + pub profile_kind: String, + pub profile_family: String, + pub packed_profile_offset: usize, + pub packed_profile_len: usize, + pub packed_profile_len_hex: String, + pub leading_word_0: u32, + pub leading_word_0_hex: String, + pub header_flag_word_3: Option, + pub header_flag_word_3_hex: Option, + pub map_path: Option, + pub display_name: Option, + pub profile_byte_0x77: u8, + pub profile_byte_0x77_hex: String, + pub profile_byte_0x82: u8, + pub profile_byte_0x82_hex: String, + pub profile_byte_0x97: u8, + pub profile_byte_0x97_hex: String, + pub profile_byte_0xc5: u8, + pub profile_byte_0xc5_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedCandidateAvailabilityTable { + pub source_kind: String, + pub semantic_family: String, + pub header_offset: usize, + pub entries_offset: usize, + pub entries_end_offset: usize, + pub observed_entry_count: usize, + pub zero_availability_count: usize, + pub zero_availability_names: Vec, + pub footer_progress_hex_words: Vec, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedNamedLocomotiveAvailabilityTable { + pub source_kind: String, + pub semantic_family: String, + #[serde(default)] + pub header_offset: Option, + #[serde(default)] + pub entries_offset: Option, + #[serde(default)] + pub entries_end_offset: Option, + pub observed_entry_count: usize, + pub zero_availability_count: usize, + pub zero_availability_names: Vec, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedLocomotiveCatalogEntry { + pub locomotive_id: u32, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedLocomotiveCatalog { + pub source_kind: String, + pub semantic_family: String, + #[serde(default)] + pub entries_offset: Option, + pub observed_entry_count: usize, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedCargoCatalogEntry { + pub slot_id: u32, + pub label: String, + #[serde(default)] + pub cargo_class: RuntimeCargoClass, + pub book_index: usize, + pub max_annual_production_word: u32, + pub mode_word: u32, + pub runtime_import_branch_kind: String, + pub annual_amount_word: u32, + pub supplied_cargo_token_word: u32, + #[serde(default)] + pub supplied_cargo_token_probable_high16_ascii_stem: Option, + pub demanded_cargo_token_word: u32, + #[serde(default)] + pub demanded_cargo_token_probable_high16_ascii_stem: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedCargoCatalog { + pub source_kind: String, + pub semantic_family: String, + #[serde(default)] + pub root_offset: Option, + pub observed_entry_count: usize, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpLoadedSaveSlice { + pub file_extension_hint: Option, + pub container_profile_family: Option, + pub mechanism_family: String, + pub mechanism_confidence: String, + pub trailer_family: Option, + pub bridge_family: Option, + pub profile: Option, + pub candidate_availability_table: Option, + pub named_locomotive_availability_table: Option, + #[serde(default)] + pub locomotive_catalog: Option, + #[serde(default)] + pub cargo_catalog: Option, + #[serde(default)] + pub world_issue_37_state: Option, + #[serde(default)] + pub world_economic_tuning_state: Option, + #[serde(default)] + pub world_finance_neighborhood_state: Option, + #[serde(default)] + pub world_locomotive_policy_state: Option, + #[serde(default)] + pub company_roster: Option, + #[serde(default)] + pub chairman_profile_table: Option, + #[serde(default)] + pub region_collection: Option, + #[serde(default)] + pub region_fixed_row_run_summary: Option, + #[serde(default)] + pub placed_structure_collection: Option, + #[serde(default)] + pub placed_structure_dynamic_side_buffer_summary: + Option, + pub special_conditions_table: Option, + pub event_runtime_collection: Option, + pub notes: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/save_load/notes.rs b/crates/rrt-runtime/src/inspect/smp/save_load/notes.rs new file mode 100644 index 0000000..b7dd4be --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/save_load/notes.rs @@ -0,0 +1,315 @@ +use super::assembly::LoadedSaveSliceParts; +use crate::inspect::smp::save_load::*; + +pub(super) fn build_loaded_save_slice_notes( + report: &SmpInspectionReport, + summary: &SmpSaveLoadSummary, + parts: &LoadedSaveSliceParts, +) -> Vec { + let mut notes = summary.notes.clone(); + + if let Some(probe) = &report.save_world_selection_context_probe { + notes.push(format!( + "Raw save fixed world block exposes selected_company_id={} at file offset 0x{:x}.", + probe.selected_company_id, probe.selected_company_id_offset + )); + notes.push(format!( + "Raw save fixed world block exposes selected_chairman_profile_id={} at file offset 0x{:x}.", + probe.selected_chairman_profile_id, probe.selected_chairman_profile_id_offset + )); + notes.push(format!( + "Raw save fixed world block also exposes {} chairman slot selector bytes at file offset 0x{:x} and campaign_override_flag={} at file offset 0x{:x}.", + probe.chairman_slot_selectors.len(), + probe.chairman_slot_selector_offset, + probe.campaign_override_flag, + probe.campaign_override_flag_offset + )); + if report.save_company_roster_probe.is_none() + || report.save_chairman_profile_table_probe.is_none() + { + notes.push( + "Raw save inspection still does not reconstruct every company_roster or chairman_profile_table scalar lane; the grounded package-save path prefers direct-record reconstruction where it can and falls back to selection/header-only context otherwise." + .to_string(), + ); + } + } + if let Some(probe) = &report.save_world_issue_37_probe { + notes.push(format!( + "Raw save fixed world block also exposes the grounded issue-0x37 pair: value={} at file offset 0x{:x} and companion multiplier {:.6} at file offset 0x{:x}.", + probe.issue_value_lane.value_i32, + probe.payload_offset + probe.issue_value_lane.relative_offset, + probe.multiplier_lane.value_f32, + probe.payload_offset + probe.multiplier_lane.relative_offset + )); + } + if let Some(probe) = &report.save_world_economic_tuning_probe { + notes.push(format!( + "Raw save fixed world block also exposes the six-lane economic tuning float band at file offset 0x{:x} (mirror lane at 0x{:x}).", + probe.tuning_lanes + .first() + .map(|lane| probe.payload_offset + lane.relative_offset) + .unwrap_or(probe.payload_offset), + probe.payload_offset + probe.mirror_lane.relative_offset + )); + notes.push( + "Current atlas evidence treats that fixed six-float world tuning band as the editor economic-cost family, not as the company-governance issue table behind investor confidence." + .to_string(), + ); + } + if let Some(probe) = &report.save_company_collection_header_probe { + notes.push(format!( + "Raw save tagged company header reports live_record_count={} and live_id_bound={} at file offsets 0x{:x}/0x{:x}/0x{:x}.", + probe.live_record_count, + probe.live_id_bound, + probe.metadata_tag_offset, + probe.records_tag_offset, + probe.close_tag_offset + )); + } + if let Some(probe) = &report.save_chairman_profile_collection_header_probe { + notes.push(format!( + "Raw save tagged chairman/profile header reports live_record_count={} and live_id_bound={} at file offsets 0x{:x}/0x{:x}/0x{:x}.", + probe.live_record_count, + probe.live_id_bound, + probe.metadata_tag_offset, + probe.records_tag_offset, + probe.close_tag_offset + )); + } + if let Some(probe) = &report.save_train_collection_header_probe { + notes.push(format!( + "Raw save tagged train header reports live_record_count={} and live_id_bound={} with direct_record_stride=0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", + probe.live_record_count, + probe.live_id_bound, + probe.direct_record_stride, + probe.metadata_tag_offset, + probe.records_tag_offset, + probe.close_tag_offset + )); + } + if let Some(probe) = &report.save_train_collection_directory_probe { + notes.push(format!( + "Raw save tagged train metadata also exposes a live-entry directory with {} entries rooted at metadata dword {} (head={:?}, tail={:?}).", + probe.entries.len(), + probe.directory_root_dword_index, + probe.chain_head_live_entry_id, + probe.chain_tail_live_entry_id + )); + } + if let Some(probe) = &report.save_region_collection_header_probe { + notes.push(format!( + "Raw save tagged region header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", + probe.live_record_count, + probe.live_id_bound, + probe.direct_record_stride, + probe.metadata_tag_offset, + probe.records_tag_offset, + probe.close_tag_offset + )); + } + if let Some(probe) = &report.save_region_record_triplet_probe { + notes.push(format!( + "Raw save tagged region records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets in the records span; first name={:?}, first policy lanes=({:.3}, {:.3}, {:.3}), trailing_word={}, first profile collection count={:?}, first profile collection trailing_padding_len={:?}.", + probe.record_count, + probe.entries.first().map(|entry| entry.name.as_str()), + probe.entries + .first() + .map(|entry| entry.policy_leading_f32_0) + .unwrap_or_default(), + probe.entries + .first() + .map(|entry| entry.policy_leading_f32_1) + .unwrap_or_default(), + probe.entries + .first() + .map(|entry| entry.policy_leading_f32_2) + .unwrap_or_default(), + probe.entries + .first() + .map(|entry| entry.policy_trailing_word_hex.as_str()) + .unwrap_or("0x0000"), + probe.entries.first().and_then(|entry| { + entry.profile_collection.as_ref().map(|collection| collection.live_record_count) + }), + probe.entries.first().and_then(|entry| { + entry.profile_collection + .as_ref() + .map(|collection| collection.trailing_padding_len) + }) + )); + } + if let Some(collection) = &parts.region_collection { + let total_profile_rows = collection + .entries + .iter() + .map(|entry| { + entry + .profile_collection + .as_ref() + .map(|collection| collection.entries.len()) + .unwrap_or_default() + }) + .sum::(); + let nonzero_prefix_count = collection + .entries + .iter() + .filter(|entry| entry.pre_name_prefix_len != 0) + .count(); + let nonzero_reserved_count = collection + .entries + .iter() + .filter(|entry| entry.policy_reserved_dwords.iter().any(|raw| *raw != 0)) + .count(); + notes.push(format!( + "Save-slice projection now carries {} loaded region triplet rows as first-class context, with {} embedded profile rows, {} rows with nonzero pre-name prefixes, and {} rows with nonzero reserved policy dwords.", + collection.observed_entry_count, + total_profile_rows, + nonzero_prefix_count, + nonzero_reserved_count + )); + } + if let Some(summary) = &parts.region_fixed_row_run_summary { + notes.push(format!( + "Save-slice projection now also carries the region fixed-row run summary with {} candidate row bands at target stride {}, best rows offset {:?}, and best shape signature {:?}.", + summary.candidates.len(), + summary.target_row_stride_hex, + summary + .candidates + .first() + .map(|candidate| candidate.rows_offset_hex.as_str()), + summary + .candidates + .first() + .map(|candidate| candidate.shape_signature.as_str()) + )); + } + if let Some(probe) = &report.save_placed_structure_collection_header_probe { + notes.push(format!( + "Raw save tagged placed-structure header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", + probe.live_record_count, + probe.live_id_bound, + probe.direct_record_stride, + probe.metadata_tag_offset, + probe.records_tag_offset, + probe.close_tag_offset + )); + } + if let Some(probe) = &report.save_placed_structure_record_triplet_probe { + notes.push(format!( + "Raw save tagged placed-structure records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets; first stems={:?}/{:?}, first policy lanes=({:.3}, {:.3}, {:.3}, {:.3}, {:.3}), first footer payload={}, first footer status kind={:?}.", + probe.record_count, + probe.entries.first().map(|entry| entry.primary_name.as_str()), + probe.entries.first().map(|entry| entry.secondary_name.as_str()), + probe.entries.first().map(|entry| entry.policy_f32_lane_0).unwrap_or_default(), + probe.entries.first().map(|entry| entry.policy_f32_lane_1).unwrap_or_default(), + probe.entries.first().map(|entry| entry.policy_f32_lane_2).unwrap_or_default(), + probe.entries.first().map(|entry| entry.policy_f32_lane_3).unwrap_or_default(), + probe.entries.first().map(|entry| entry.policy_f32_lane_4).unwrap_or_default(), + probe.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000"), + probe.entries.first().map(|entry| entry.profile_status_kind.as_str()) + )); + } + if let Some(collection) = &parts.placed_structure_collection { + let farm_growth_stage_count = collection + .entries + .iter() + .filter(|entry| entry.farm_growth_stage_index.is_some()) + .count(); + let opaque_status_count = collection + .entries + .iter() + .filter(|entry| entry.profile_status_kind != "unset") + .count(); + notes.push(format!( + "Save-slice projection now carries {} loaded placed-structure triplet rows as first-class context, with {} farm growth-stage rows and {} non-default footer-status rows.", + collection.observed_entry_count, farm_growth_stage_count, opaque_status_count + )); + } + if let Some(probe) = &parts.placed_structure_dynamic_side_buffer_probe { + let dominant_pattern = probe.compact_prefix_pattern_summaries.first(); + let payload_envelope_summary = probe.payload_envelope_summary.as_ref(); + let short_profile_flag_pair_summary = payload_envelope_summary + .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()); + notes.push(format!( + "Raw save also exposes the separate placed-structure dynamic-side-buffer candidate 0x38a5/0x38a6/0x38a7: live_record_count={}, owner-shared 0x38a6 dword={} at relative offset 0x{:x}, first compact prefix=({},{},{}), first embedded names={:?}/{:?}/{:?}, embedded 0x55f1 row count={}, rows with tertiary 0x55f1 string={}, unique compact prefix patterns={}, 0x55f3-leading rows={}, complete 0x55f1/0x55f2/0x55f3 envelopes={}, dominant 0x55f2 chunk len=0x{:x} x{}, dominant 0x55f3 span=0x{:x} x{}, dominant short 0x55f3 flag pair={}/{} x{}, dominant compact pattern={}/{}/{} x{}.", + probe.live_record_count, + probe.owner_shared_dword_hex, + probe.owner_shared_dword_relative_offset, + probe.prefix_leading_dword_hex, + probe.prefix_trailing_word_hex, + probe.prefix_separator_byte_hex, + probe.first_embedded_primary_name.as_deref(), + probe.first_embedded_secondary_name.as_deref(), + probe.first_embedded_tertiary_name.as_deref(), + probe.embedded_name_tag_count, + probe.decoded_embedded_name_row_with_tertiary_name_count, + probe.unique_compact_prefix_pattern_count, + probe.prefix_leading_dword_matching_embedded_profile_tag_count, + payload_envelope_summary + .map(|summary| summary.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope) + .unwrap_or_default(), + payload_envelope_summary + .and_then(|summary| summary.dominant_policy_chunk_len) + .unwrap_or_default(), + payload_envelope_summary + .map(|summary| summary.dominant_policy_chunk_len_count) + .unwrap_or_default(), + payload_envelope_summary + .and_then(|summary| summary.dominant_profile_chunk_len) + .unwrap_or_default(), + payload_envelope_summary + .map(|summary| summary.dominant_profile_chunk_len_count) + .unwrap_or_default(), + short_profile_flag_pair_summary + .and_then(|summary| summary.dominant_flag_pair.as_ref()) + .map(|pair| pair.first_flag_byte_hex.as_str()) + .unwrap_or("0x00"), + short_profile_flag_pair_summary + .and_then(|summary| summary.dominant_flag_pair.as_ref()) + .map(|pair| pair.second_flag_byte_hex.as_str()) + .unwrap_or("0x00"), + short_profile_flag_pair_summary + .and_then(|summary| summary.dominant_flag_pair.as_ref()) + .map(|pair| pair.count) + .unwrap_or_default(), + dominant_pattern + .map(|pattern| pattern.prefix_leading_dword_hex.as_str()) + .unwrap_or("0x00000000"), + dominant_pattern + .map(|pattern| pattern.prefix_trailing_word_hex.as_str()) + .unwrap_or("0x0000"), + dominant_pattern + .map(|pattern| pattern.prefix_separator_byte_hex.as_str()) + .unwrap_or("0x00"), + dominant_pattern.map(|pattern| pattern.count).unwrap_or_default() + )); + if probe.owner_shared_dword_matches_first_compact_prefix_leading_dword { + notes.push( + "Direct disassembly now shows 0x00493be0 consuming one shared 0x38a6 owner-local dword before iterating records; the first compact-prefix leading dword currently reuses that same lane." + .to_string(), + ); + } + } + if let Some(summary) = &parts.placed_structure_dynamic_side_buffer_summary { + notes.push(format!( + "Save-slice projection now also carries the placed-structure dynamic side-buffer summary with {} decoded name rows, {} unique name pairs, and {} overlapping triplet name pairs.", + summary.decoded_embedded_name_row_count, + summary.unique_embedded_name_pair_count, + summary.triplet_alignment_overlap_count + )); + } + if let Some(roster) = &report.save_company_roster_probe { + notes.push(format!( + "Raw save inspection reconstructed {} company direct records from the tagged company collection.", + roster.entries.len() + )); + } + if let Some(table) = &report.save_chairman_profile_table_probe { + notes.push(format!( + "Raw save inspection reconstructed {} chairman/profile direct records from the tagged chairman collection.", + table.entries.len() + )); + } + + notes +} diff --git a/crates/rrt-runtime/src/inspect/smp/save_load/summary.rs b/crates/rrt-runtime/src/inspect/smp/save_load/summary.rs new file mode 100644 index 0000000..39076ee --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/save_load/summary.rs @@ -0,0 +1,175 @@ +use crate::inspect::smp::save_load::*; + +pub(in crate::inspect::smp) fn build_save_load_summary( + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>, + rt3_105_post_span_bridge_probe: Option<&SmpRt3105PostSpanBridgeProbe>, + classic_rehydrate_profile_probe: Option<&SmpClassicRehydrateProfileProbe>, + rt3_105_packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>, + rt3_105_save_name_table_probe: Option<&SmpRt3105SaveNameTableProbe>, +) -> Option { + let file_extension_hint = file_extension_hint.map(str::to_string); + let container_profile_family = container_profile.map(|profile| profile.profile_family.clone()); + let trailer_family = runtime_trailer_block.map(|trailer| trailer.trailer_family.clone()); + let bridge_family = rt3_105_post_span_bridge_probe.map(|bridge| bridge.bridge_family.clone()); + let candidate_table = + rt3_105_save_name_table_probe.map(|probe| SmpSaveLoadCandidateTableSummary { + source_kind: probe.source_kind.clone(), + semantic_family: probe.semantic_family.clone(), + observed_entry_count: probe.observed_entry_count, + zero_availability_count: probe.zero_trailer_entry_count, + zero_availability_names: probe.zero_trailer_entry_names.clone(), + footer_progress_hex_words: vec![ + probe.footer_progress_word_0_hex.clone(), + probe.footer_progress_word_1_hex.clone(), + ], + }); + + if let Some(probe) = classic_rehydrate_profile_probe { + let block = &probe.packed_profile_block; + let mut notes = vec![ + "Classic save load reaches the grounded late rehydrate band 0x32dc -> 0x3714 -> 0x3715." + .to_string(), + "The file exposes one exact 0x108 packed-profile block between progress ids 0x3714 and 0x3715." + .to_string(), + ]; + if let Some(map_path) = &block.map_path { + notes.push(format!("Packed profile map path: {map_path}")); + } + if let Some(display_name) = &block.display_name { + notes.push(format!("Packed profile display name: {display_name}")); + } + + return Some(SmpSaveLoadSummary { + file_extension_hint, + container_profile_family, + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + packed_profile_kind: Some("classic-rehydrate-profile".to_string()), + packed_profile_family: Some(probe.profile_family.clone()), + packed_profile_offset: Some(probe.packed_profile_offset), + packed_profile_len: Some(probe.packed_profile_len), + map_path: block.map_path.clone(), + display_name: block.display_name.clone(), + profile_byte_0x77: Some(block.profile_byte_0x77), + profile_byte_0x77_hex: Some(block.profile_byte_0x77_hex.clone()), + profile_byte_0x82: Some(block.profile_byte_0x82), + profile_byte_0x82_hex: Some(block.profile_byte_0x82_hex.clone()), + profile_byte_0x97: Some(block.profile_byte_0x97), + profile_byte_0x97_hex: Some(block.profile_byte_0x97_hex.clone()), + profile_byte_0xc5: Some(block.profile_byte_0xc5), + profile_byte_0xc5_hex: Some(block.profile_byte_0xc5_hex.clone()), + trailer_family, + bridge_family: None, + candidate_table, + notes, + }); + } + + if let Some(probe) = rt3_105_packed_profile_probe { + let block = &probe.packed_profile_block; + let mechanism_family = rt3_105_post_span_bridge_probe + .map(|bridge| bridge.bridge_family.clone()) + .unwrap_or_else(|| match probe.profile_family.as_str() { + "rt3-105-scenario-save-container-v1" => { + "rt3-105-scenario-save-profile-analog-v1".to_string() + } + "rt3-105-alt-save-container-v1" => "rt3-105-alt-save-profile-analog-v1".to_string(), + _ => "rt3-105-save-profile-analog-v1".to_string(), + }); + let mechanism_confidence = if rt3_105_post_span_bridge_probe.is_some() { + "mixed" + } else { + "inferred" + } + .to_string(); + let mut notes = Vec::new(); + if let Some(bridge) = rt3_105_post_span_bridge_probe { + notes.push(format!( + "RT3 1.05 save branch uses {} with selector/descriptor {} -> {}.", + bridge.bridge_family, bridge.selector_high_hex, bridge.descriptor_high_hex + )); + } else { + notes.push( + "RT3 1.05 save exposes a packed-profile analogue, but the upstream load bridge is not resolved for this branch." + .to_string(), + ); + } + if let Some(map_path) = &block.map_path { + notes.push(format!("Packed profile map path: {map_path}")); + } + if let Some(display_name) = &block.display_name { + notes.push(format!("Packed profile display name: {display_name}")); + } + if let Some(table) = &candidate_table { + notes.push(format!( + "Candidate table source {} carries {} entries with {} zero-availability overrides.", + table.source_kind, table.observed_entry_count, table.zero_availability_count + )); + } + + return Some(SmpSaveLoadSummary { + file_extension_hint, + container_profile_family, + mechanism_family, + mechanism_confidence, + packed_profile_kind: Some("rt3-105-packed-profile".to_string()), + packed_profile_family: Some(probe.profile_family.clone()), + packed_profile_offset: Some(probe.packed_profile_offset), + packed_profile_len: Some(probe.packed_profile_len), + map_path: block.map_path.clone(), + display_name: block.display_name.clone(), + profile_byte_0x77: Some(block.profile_byte_0x77), + profile_byte_0x77_hex: Some(block.profile_byte_0x77_hex.clone()), + profile_byte_0x82: Some(block.profile_byte_0x82), + profile_byte_0x82_hex: Some(block.profile_byte_0x82_hex.clone()), + profile_byte_0x97: Some(block.profile_byte_0x97), + profile_byte_0x97_hex: Some(block.profile_byte_0x97_hex.clone()), + profile_byte_0xc5: Some(block.profile_byte_0xc5), + profile_byte_0xc5_hex: Some(block.profile_byte_0xc5_hex.clone()), + trailer_family, + bridge_family, + candidate_table, + notes, + }); + } + + if let Some(table) = candidate_table { + return Some(SmpSaveLoadSummary { + file_extension_hint, + container_profile_family, + mechanism_family: "rt3-105-candidate-catalog-source-v1".to_string(), + mechanism_confidence: "mixed".to_string(), + packed_profile_kind: None, + packed_profile_family: None, + packed_profile_offset: None, + packed_profile_len: None, + map_path: None, + display_name: None, + profile_byte_0x77: None, + profile_byte_0x77_hex: None, + profile_byte_0x82: None, + profile_byte_0x82_hex: None, + profile_byte_0x97: None, + profile_byte_0x97_hex: None, + profile_byte_0xc5: None, + profile_byte_0xc5_hex: None, + trailer_family, + bridge_family, + notes: vec![ + format!( + "The file carries the shared 1.05 candidate table source block through {}.", + table.source_kind + ), + format!( + "The table exposes {} named entries with {} zero-availability overrides.", + table.observed_entry_count, table.zero_availability_count + ), + ], + candidate_table: Some(table), + }); + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/entries.rs b/crates/rrt-runtime/src/inspect/smp/services/company/entries.rs new file mode 100644 index 0000000..3caf76f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/entries.rs @@ -0,0 +1,148 @@ +use crate::inspect::smp::services::*; + +pub(super) fn build_periodic_company_service_entries( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + analysis + .company_entries + .iter() + .map(|entry| { + let mut branches = Vec::new(); + branches.push(build_service_trace_branch_status( + "route_preference_override", + if entry.preferred_locomotive_engine_type_raw_u8 == 2 { + "grounded_electric_override_candidate" + } else { + "grounded_non_electric_or_inactive_override_candidate" + }, + &[ + "company periodic side-latch trio", + "world route-preference byte", + "preferred locomotive engine-type lane", + ], + &[], + &[ + "0x004019e0 periodic company outer owner", + "0x004078a0 preferred-locomotive chooser", + "0x0041d550 locomotive-era and engine-type approval gate", + ], + &["This probe keeps the outer owner at the save-owned input level; the concrete runtime reader/apply/restore seam is already grounded separately."], + )); + branches.push(build_service_trace_branch_status( + "annual_finance_policy", + "runnable_from_grounded_owner_state", + &[ + "company market/cache owner state", + "periodic side-latches", + "world issue/timing owner state", + "derived annual-finance readers", + ], + &[], + &[ + "0x004019e0 periodic company outer owner", + "0x00401c50 annual finance-policy owner", + ], + &["The shellless annual-finance helper is already rehosted on top of runtime-owned state."], + )); + branches.push(build_service_trace_branch_status( + "city_connection_announcement", + "blocked_missing_region_and_infrastructure_asset_owner_seams", + &[ + "company periodic side-latches", + "route-preference override seam", + "annual-finance sequencing owner", + ], + &[ + "region pending/completion/one-shot/severity lanes", + "stable region id or class discriminator", + "placed-structure or infrastructure-asset consumer mapping", + ], + &[ + "0x004019e0 periodic company outer owner", + "0x00406050 city-connection bonus/news owner", + "0x00420030 / 0x00420280 city-connection peer probes", + "0x0047efe0 placed-structure linked-company resolver", + ], + &["Current atlas evidence places this branch above both the region pending-bonus lane and infrastructure/placed-structure consumers."], + )); + branches.push(build_service_trace_branch_status( + "linked_transit_roster_maintenance", + "blocked_missing_site_cache_input_owner_mapping", + &[ + "company linked-transit latch", + "company linked-transit route-anchor tuple", + "company linked-transit peer-cache refresh absolute counter [company+0x0d3e]", + "company linked-transit autoroute-score refresh absolute counter [company+0x0d3a]", + "route-preference override seam", + ], + &[ + "placed-structure site-cache input owners beneath 0x004093d0 / 0x00407bd0", + "persisted site-side inputs behind 0x00408280 / 0x00408380", + ], + &[ + "0x004019e0 periodic company outer owner", + "0x00409720 timed linked-transit cache-service wrapper", + "0x004093d0 linked-transit site-peer cache rebuild", + "0x00407bd0 linked-transit autoroute site-score cache rebuild", + "0x00408280 linked-transit ranked-site chooser", + "0x00408380 linked-transit staged autoroute-entry builder", + "0x00408f70 linked-transit aggregate site-score pressure helper", + "0x00409770 linked-transit autoroute append/rotate owner", + "0x00409830 linked-transit add-train/news owner", + "0x00409950 linked-transit train-roster balancer", + ], + &["The save side now grounds the timed cache-owner seams, ranked-site chooser, staged route builder, and train-side follow-ons; the remaining blocker is the placed-structure site-cache input ownership beneath those bounded consumers, not the train-side strip itself."], + )); + branches.push(build_service_trace_branch_status( + "industry_acquisition_side_branch", + "blocked_missing_near-city_owner_mapping", + &["periodic service outer owner", "annual-finance ordering"], + &[ + "near-city industry candidate owner seam", + "city or region peer linkage", + ], + &[ + "0x004019e0 periodic company outer owner", + "0x004014b0 near-city industry acquisition and news owner", + "0x004010f0 near-city acquisition region scorer", + "0x00405920 same-company industry proximity aggregator", + "0x0041f6e0 center-cell token gate", + "0x0042b2d0 packed-u16 contains-key predicate", + "0x0047de00 linked-region resolver feeding 0x0040c990", + "0x004801a0 linked-transit route-anchor reachability gate", + "0x00425b90 pending-bonus/company-state gate", + "0x0040d360 region type/class filter", + "0x0040d540 weighted region-to-company proximity scorer", + "0x0040f6d0 subtype-1 placed-structure constructor", + "0x00481390 / 0x00480210 subtype-1 linked-site allocation and constructor", + "0x00444690 late world bring-up caller of 0x004133b0", + "0x004133b0 placed-structure local-runtime replay owner", + "0x0040e450 queued site-id cloned local-runtime replay helper", + "0x0040ee10 live-site position/scalar refresh helper", + "0x00480710 linked-site runtime side-buffer and route-entry-anchor refresh owner", + "0x0040df27 / 0x0040e00a / 0x0040edf6 linked-site side-refresh callers", + "0x004160aa non-bring-up runtime caller of 0x0040ee10", + "0x0048abc0 / 0x00493cf0 route-entry-anchor rebind and synthesis strip", + "0x0047dda0 linked-peer route-entry-anchor validator", + "0x00420030 / 0x00420280 peer-site boolean/selector pair", + "0x00406050 city-connection bonus/news sibling owner", + ], + &["Direct disassembly now shows this branch scanning the live placed-structure collection at 0x0062b26c for the best current acquisition target, rejecting sites whose owner field [site+0x276] is already nonzero, reusing the center-cell token gate 0x0041f6e0 -> 0x0042b2d0, reusing the linked-region status branch 0x0047de00 -> 0x0040c990, checking candidate reachability through 0x004801a0, consulting the placed-structure peer-site boolean/selector pair 0x00420030 / 0x00420280 over 0x006cec20, scoring candidate sites against company proximity and age through 0x0040d540 and 0x0040cac0, and then committing the chosen site through 0x004269b0. The peer-site selector seam itself is now grounded through the local helper strip: 0x0040cd70 seeds [site+0x3cc/+0x3d0] from 0x62b2fc / 0x62b268, 0x0040ceab and 0x0040d1a1 reach the save-backed 0x0045c150 / 0x0045c310 owner directly, that owner fills [owner+0x23e/+0x242] from tagged payload 0x5dc1, and 0x0045c36e then feeds [owner+0x23e] through 0x00456100 -> 0x00455b70 -> 0x0052edf0 into the live backing-record selector [site+0x04]. The cached tri-lane is no longer a restore-only mystery either: 0x0040d450 and 0x00410b30..0x004118f4 now bound the live writer family above the shared 0x00412560 candidate/admissibility gate, 0x0040fb70 is the small wrapper into that gate, and direct callers now separate acquisition-adjacent 0x0040d540 users like 0x00401633 / 0x0044b81a from broader sibling sweeps such as 0x004b4052 / 0x004b46ec / 0x004b70f5 / 0x004b7979. The remaining linked-site field work is now about which persisted site/peer lanes are actually sufficient for shellless acquisition and city-connection behavior, not about who owns [site+0x04] or whether the tri-lane has live producers."], + )); + SmpPeriodicCompanyServiceTraceEntry { + company_id: entry.company_id, + name: entry.name.clone(), + active: entry.active, + linked_chairman_profile_id: entry.linked_chairman_profile_id, + preferred_locomotive_engine_type_raw_u8: entry.preferred_locomotive_engine_type_raw_u8, + city_connection_latch: entry.city_connection_latch, + linked_transit_latch: entry.linked_transit_latch, + linked_transit_autoroute_site_score_cache_refresh_absolute_counter: entry + .linked_transit_autoroute_site_score_cache_refresh_absolute_counter, + linked_transit_site_peer_cache_refresh_absolute_counter: entry + .linked_transit_site_peer_cache_refresh_absolute_counter, + branches, + } + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/entrypoints.rs b/crates/rrt-runtime/src/inspect/smp/services/company/entrypoints.rs new file mode 100644 index 0000000..19356a0 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/entrypoints.rs @@ -0,0 +1,22 @@ +use std::path::Path; + +use crate::inspect::smp::bundle::inspect_smp_file; +use crate::inspect::smp::services::{ + SmpPeriodicCompanyServiceTraceReport, build_periodic_company_service_trace_report, +}; +use crate::inspect::smp::world::inspect_save_company_and_chairman_analysis_file; + +pub fn inspect_save_periodic_company_service_trace_file( + path: &Path, +) -> Result> { + let inspection = inspect_smp_file(path)?; + let analysis = inspect_save_company_and_chairman_analysis_file(path)?; + let mut trace = build_periodic_company_service_trace_report(&analysis); + let _ = inspection; + if trace.region_record_body_present || trace.placed_structure_record_body_present { + trace.notes.push( + "The current blockers are no longer collection identity; they are missing higher-layer consumer semantics for the region and infrastructure/placed-structure owner seams.".to_string(), + ); + } + Ok(trace) +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/company/mod.rs new file mode 100644 index 0000000..2cd14e5 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/mod.rs @@ -0,0 +1,9 @@ +mod entries; +mod entrypoints; +mod near_city_acquisition; +mod notes; +mod peer_site; +mod status; + +pub use entrypoints::inspect_save_periodic_company_service_trace_file; +pub(in crate::inspect::smp) use status::build_periodic_company_service_trace_report; diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/gaps.rs b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/gaps.rs new file mode 100644 index 0000000..148abf1 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/gaps.rs @@ -0,0 +1,14 @@ +pub(super) fn build_near_city_owner_gaps_and_region_statuses() -> (Vec, Vec) { + ( + vec![ + "non-transport persisted source family outside the currently bounded direct allocator/finalize/store families and tagged 0x36b1/0x36b2/0x36b3 load path, feeding the tuple-to-live projection of placed-structure owner-company field [site+0x276] for the acquisition-side owner-present gate; the live meaning is grounded through 0x0047efe0, the owner family is bounded under 0x004134d0 / 0x0040f6d0 plus shared finalize helper 0x0040ef10, loader tuple field [+0x0c] is known to seed [site+0x276] through 0x0046f073 / 0x004707ff, and the runtime now carries a best-effort owner projection while the exact ordinary non-transport restore caller remains open".to_string(), + "exact persisted inputs and shellless service semantics for the now-grounded live cached tri-lane writer family over [site+0x310/+0x338/+0x360], especially 0x0040d450 and 0x00410b30..0x004118f4 above 0x00412560; the runtime now carries a best-effort tri-lane projection from save-side side-buffer and fixed-row signatures while the exact ordinary formula is still open".to_string(), + ], + vec![ + "[site+0x276] owner-present gate: consumed directly by 0x004014b0, the live owner-company meaning is grounded through 0x0047efe0, the write family is bounded under 0x004134d0 / 0x0040f6d0 plus shared finalize helper 0x0040ef10, loader tuple field [+0x0c] is known to seed that lane through 0x0046f073 / 0x004707ff, the tagged 0x36b1/0x36b2/0x36b3 collection loader is ruled down, and the runtime now carries a best-effort projected owner lane while the exact ordinary restore caller remains open".to_string(), + "[site+0x2a4] placed-structure id lane: peer-chain helpers already ground this as the record's own site id, constructor-side 0x00480210 seeds it for new linked-site rows, and 0x004269b0 resolves the chosen site id back through 0x0062b26c before mutating [site+0x276], so this lane is reconstructible from collection identity for the 0x004014b0 commit path".to_string(), + "[site+0x310/+0x338/+0x360] cached tri-lane: exact delta reader grounded at 0x0040cac0, deferred additive accumulator/reset helper grounded at 0x0040c9a0, direct live producers now grounded at 0x0040d450 plus the broader 0x00410b30..0x004118f4 candidate-processing loop above 0x00412560, and the runtime now carries a best-effort save-derived tri-lane projection while the exact service formula remains open".to_string(), + "placed-structure subtype filter: 0x0040d360 is the exact test [candidate+0x32] == 4, the owning subtype byte is already bounded under 0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0, and direct local binary inspection now grounds stream-load callback 0x0040ce60 as the restore-side bridge into [site+0x3cc/+0x3d0]".to_string(), + ], + ) +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/hypotheses.rs b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/hypotheses.rs new file mode 100644 index 0000000..dcdda60 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/hypotheses.rs @@ -0,0 +1,109 @@ +use crate::inspect::smp::services::*; + +pub(super) fn build_near_city_projection_hypotheses() -> Vec { + vec![ + SmpServiceConsumerHypothesis { + label: "site_owner_replay_from_post_load_refresh_self_id_reconstructible".to_string(), + status: "ordinary_replay_and_stream_load_ruled_down_tuple_finalize_positive_path_grounded".to_string(), + candidate_consumers: vec![ + "0x00444690 late world bring-up caller".to_string(), + "0x004133b0 placed-structure local-runtime replay owner".to_string(), + "0x0040ee10 live-site position/scalar refresh helper".to_string(), + "0x00480710 linked-site runtime side-buffer and route-entry-anchor refresh owner".to_string(), + "0x004134d0 allocator plus direct site constructor 0x0040f6d0".to_string(), + "0x00403ef3 / 0x00404489 create-side callers of shared finalize helper 0x0040ef10".to_string(), + "0x0046f073 / 0x004707ff data-driven loader callers of shared finalize helper 0x0040ef10".to_string(), + "0x004269b0 acquisition commit owner resolving live site rows by id".to_string(), + ], + evidence: vec![ + "[site+0x276] live owner-company meaning is grounded through 0x0047efe0 / 0x0040d210".to_string(), + "[site+0x2a4] self-id meaning is grounded through 0x0041f7e0 / 0x0041f810 / 0x0041f850".to_string(), + "0x004269b0 resolves the chosen site id through placed-structure collection 0x0062b26c before mutating the live row, so [site+0x2a4] is reconstructible from collection identity rather than a separate serializer-owned selector".to_string(), + "0x00444690 -> 0x004133b0 -> 0x0040ee10 is the current checked-in ordinary bring-up replay family above live placed structures".to_string(), + "the ordinary restore-side staging order is tighter now too: world bring-up calls 0x00413280 first for tagged 0x36b1/0x36b2/0x36b3 stream load at 0x00444467, refreshes dynamic side buffers through 0x00481210 at 0x004444d8, and only later enters 0x004133b0 at 0x00444690 for queued local-runtime replay plus 0x0040ee10".to_string(), + "0x004133b0 rebuilds cloned local-runtime records through 0x0040e450 and 0x0040ee10 only republishes local position/scalar triplets before 0x0040e360".to_string(), + "the ordinary bring-up strip stays separate from the constructor/finalize family too: after 0x00444690 -> 0x004133b0 the world restore continues through later route-entry/grid/tagged refresh owners rather than re-entering 0x004134d0 / 0x0040f6d0 / 0x0040ef10 for already-restored rows".to_string(), + "[site+0x27a] is now bounded as a live signed scalar accumulator rather than a second owner-identity mystery: base constructor 0x00421200 zeros it at 0x0042125d, create-side initializer 0x0040f6d0 zeros it again at 0x0040f793, station-detail apply 0x0040dfec accumulates into it before 0x0040d1f0 / 0x00480710, acquisition commit stores it at 0x004269e4, acquisition clear negates and clears it through 0x00426a44..0x00426a90, and acquisition delta helper 0x00426ad8 accumulates into it again".to_string(), + "direct local replay-strip inspection now splits that family more precisely: bounded 0x0040ee10 itself only reads cached source lane [site+0x3cc], and the bounded 0x00480710 neighborhood is working from [site+0x04], [site+0x08], and [site+0x3cc]; the broader immediate continuation 0x0040e360..0x0040edf6 still consumes [site+0x2a8], [site+0x2a4], and [site+0x276] around 0x0040d230 / 0x0040d1f0 / 0x00480710 / 0x00426b10 / 0x00455860, but in the checked range those [site+0x276] uses are still reads/queries rather than a direct rehydrating store".to_string(), + "direct constructor control-flow now shows 0x004134d0 allocating a new placed-structure row through 0x00518900 and then calling 0x0040f6d0, which seeds [site+0x2a4], clears broad row state, copies the display name bytes, and writes [site+0x276] from an incoming argument before any later service logic runs".to_string(), + "0x0040f6d0 immediately zeroes [site+0x2a8/+0x272/+0x27a/+0x29e], stamps [site+0x3d4/+0x3d5], and seeds further local caches, which makes it a create-side initializer rather than a replay-only refresh".to_string(), + "shared finalize helper 0x0040ef10 now has create-side callers 0x00403ef3 / 0x00404489 and data-driven callers 0x0046f073 / 0x004707ff; the latter feed it from tuple-backed loads after 0x0040eba0 / 0x0052eb90 rather than from the checked-in local replay strip".to_string(), + "the loader-side dataflow is narrower now too: 0x0046efbf is the paired constructor call and 0x0046f073 / 0x004707ff are the paired finalize calls in the tuple-backed data path; 0x0046efbf and 0x0047074b both reach 0x004134d0 first, then 0x0046f073 / 0x004707ff push tuple fields [+0x00/+0x04/+0x0c] into 0x0040ef10, that helper reads arg3 into ebx at 0x0040ef1c, and the paired write at 0x0040f5d4 stores ebx into [site+0x276] while 0x0040f5da stores the computed companion word into [site+0x27a]".to_string(), + "the outer owner above 0x0046efbf / 0x0046f073 / 0x0047074b / 0x004707ff is now classified too: atlas-backed recovery ties those calls to multiplayer transport selector-0x13 body 0x004706b0, which attempts the placed-structure apply path through 0x004197e0 / 0x004134d0 / 0x0040eba0 / 0x0052eb90 / 0x0040ef10 rather than ordinary save-load restore".to_string(), + "the neighboring builder strip 0x00472b40 is classified too now: atlas-backed recovery ties it to multiplayer transport selector-0x72, the heavier counted live-world apply path over 0x0062b2fc / 0x0062b26c / 0x0062bae0, and its inner calls 0x00472bef / 0x00472d03 reach 0x004134d0 from counted transport records rather than ordinary save-load restore".to_string(), + "another surviving 0x004134d0 caller is bounded away from persisted restore too: 0x00422bb4 pushes one live 0x0062b2fc record plus local args and literal flags 1/0 into 0x004134d0, then returns the created row id through an out-param rather than re-entering the tuple-backed finalize path".to_string(), + "the remaining 0x00508fd1 / 0x005098eb strip is bounded away from persisted restore too: 0x00508fd1 stores the new row id in [this+0x7c], immediately configures the live row through vtable slot +0x58 plus 0x00507cf0, and 0x005098eb later re-enters 0x0040ef10 with arg3 forced to zero, so this family is another live controller path rather than the missing persisted owner seam".to_string(), + "the adjacent 0x00473c20 family is bounded away too: it drains queued site ids and coordinate pairs from scratch band 0x006ce808..0x006ce988, re-enters 0x0040eba0 at 0x00473c98 for each live row, and then clears the queued id slot, so it is a local post-create refresh path rather than the missing persisted owner seam".to_string(), + "the remaining direct [site+0x276] store census is bounded away too: 0x0042128d is broad zero-init in the 0x00421430 constructor neighborhood, 0x00422305 computes a live score/category lane before publishing event 0x7, 0x004269c9/0x00426a2a are acquisition commit and clear helpers, and 0x004282a9 / 0x004300d6 are bulk owner-transfer writes rather than ordinary restored-row replay".to_string(), + "the paired collection-side serializer 0x00413440 is bounded away too: it opens tagged families 0x36b1 / 0x36b2 / 0x36b3 on the save side, routes each live record through per-entry vtable slot +0x44, and keeps that seam on the already-grounded triplet payload/load-save strip rather than the missing [site+0x276] replay owner".to_string(), + "the actual broader restore-side stream-load owner remains 0x00413280: it opens tagged 0x36b1 / 0x36b2 / 0x36b3 on the load side, dispatches per-entry vtable slot +0x40, and currently only grounds the cached-source bridge 0x0040ce60 -> 0x0040cd70 / 0x0045c150 rather than a direct [site+0x276] republisher".to_string(), + "direct local control flow now rules the ordinary bring-up owner down even further: 0x00444690 immediately calls 0x004133b0 and then continues into later grid/world refresh owners without re-entering 0x004134d0 / 0x0040f6d0 / 0x0040ef10 for already-restored rows".to_string(), + "inside 0x0040ef10 the [site+0x276] write at 0x0040f047 only clears owner-company under a world-flag branch, while the paired [site+0x276]/[site+0x27a] write at 0x0040f5d4 follows a 0x00436590 event/scalar path and is not the generic post-load republisher".to_string(), + "direct local writer census now shows the grounded [site+0x276] write side clustering under live mutation families such as 0x004269b0 / 0x00426a10, the create-side 0x0040ef10 / 0x0040f6d0 strip, and the bulk reassignment families 0x00426dce..0x00426ea1 and 0x00430040..0x004300d6 rather than under the known replay strip".to_string(), + "direct local control-flow reconstruction now shows those same writer families hanging under the 0x00431b20 opcode dispatcher over 0x0061039c: opcodes 0x04..0x07 dispatch to 0x00430040, opcodes 0x08/0x10..0x13 dispatch to 0x00426d60, and opcodes 0x0d/0x16 dispatch to 0x0042fc90".to_string(), + "0x0042fc90 itself iterates the live placed-structure collection 0x0062b26c, filters rows through 0x0040c990 plus optional company match [site+0x276], and then dispatches row vtable slot +0x70, which keeps that branch on the live application side rather than replay".to_string(), + "the trigger-kind field itself is now bounded as an ordinary loaded per-event lane rather than a startup-only special class: restore-side loader 0x00433130 repopulates live event collection 0x0062be18 from packed chunk family 0x4e21/0x4e22, and the event-detail editor strip 0x004d90ba..0x004d91ed writes [event+0x7ef] across the full 0x00..0x0a range through controls 0x4e98..0x4ea2, including kind 8 at 0x004d91b3".to_string(), + "that keeps 0x00444d92 -> 0x00432f40(kind 8) on the ordinary loaded runtime-effect pipeline too: world bring-up is servicing pre-existing rows from 0x0062be18 rather than a one-off startup-only record class synthesized outside the collection".to_string(), + "the event-detail editor family now ties that trigger-kind field to the ordinary runtime-effect builders too: selected-event control family 0x004db02a / 0x004db1b8..0x004db309 mirrors current [event+0x7ef] back into controls 0x4e98..0x4ea2 under root control 0x4e84, while editor-side builder 0x004db9e5..0x004db9f1 allocates a runtime-effect row from compact payload into 0x0062be18 through 0x00432ea0 before rebinding the selected event id".to_string(), + "bundle-side inspection now grounds the ordinary startup collection further too: the non-direct 0x4e99/0x4e9a/0x4e9b runtime-event collection decodes as a compact serializer family recovered from 0x00433060/0x00430d70 plus the paired 0x00433130/0x0042db20 load path rather than an opaque raw blob, and sampled maps such as War Effort/British Isles/Germany/Texas Tea now decode their compact rows into actual condition/grouped summaries instead of signature-only parity".to_string(), + "the adjacent control-lane owner is bounded too now: nearby helper 0x0042e050 copies text bands plus [event+0x7ee..0x80f] between live runtime-event rows, which separates full event-control cloning from the narrower compact row-body loader 0x0042db20".to_string(), + ], + blockers: vec![ + "current atlas evidence now grounds one tuple-backed owner path too: loader tuple field [+0x0c] reaches [site+0x276] through 0x0046f073 / 0x004707ff -> 0x0040ef10, but the classified 0x004707ff caller belongs to multiplayer transport selector-0x13 rather than ordinary save-load restore, so a non-transport persisted source family is still needed for shellless acquisition".to_string(), + "the explicit store census now also rules down the remaining obvious non-transport writes, so the missing ordinary restored-row owner seam likely sits outside the currently bounded direct allocator/finalize/store families".to_string(), + "the paired collection-side triplet serializer 0x00413440 is ruled down too, so the missing ordinary restored-row owner seam likely sits outside the currently bounded direct allocator/finalize/store families and the tagged 0x36b1/0x36b2/0x36b3 load-save strip".to_string(), + "the load-side stream owner 0x00413280 is ruled down to cached-source/candidate replay through vtable slot +0x40 and 0x0040ce60, so the missing ordinary restored-row owner seam still sits beyond the current stream-load bridge too".to_string(), + "the ordinary bring-up caller 0x00444690 is ruled down too: it just enters 0x004133b0 and then proceeds into later refresh owners, so the positive [site+0x276] write side at 0x0040ef10 remains a tuple/create path rather than the checked ordinary restore path".to_string(), + "the checked ordinary restore ordering is ruled down too: 0x00413280 stream load, 0x00481210 dynamic side-buffer refresh, and 0x004133b0 local-runtime replay all sit on the bring-up strip without re-entering 0x004134d0 / 0x0040f6d0 / 0x0040ef10 for already-restored rows".to_string(), + "the grouped opcode dispatcher 0x00431b20 is still not a tagged restore owner, but the remaining uncertainty is now narrower than compact row framing too: restore-side 0x00433130 with 0x0042db20 reloads compact row bodies into ordinary live event rows in 0x0062be18, nearby 0x0042e050 is the separate full-event copy owner for [event+0x7ee..0x80f], the event-detail editor exposes [event+0x7ef] across 0x00..0x0a including kind 8, and sampled map bundles now decode into concrete grouped descriptors, so the open question is which serialized/live rows feed trigger kind 8 into that control lane and which of those loaded rows can actually reach the placed-structure mutation opcodes under 0x00431b20".to_string(), + ], + }, + SmpServiceConsumerHypothesis { + label: "site_cached_tri_lane_payload_or_restore_owner".to_string(), + status: "checked_in_save_seams_ruled_down_live_scoring_family_grounded_exact_semantics_open".to_string(), + candidate_consumers: vec![ + "0x36b1/0x36b2/0x36b3 placed-structure triplet owner".to_string(), + "0x00455fc0 shared tagged payload loader".to_string(), + "0x00444690 -> 0x004133b0 replay strip".to_string(), + "0x0040d450 owner-company-aware local scorer writing [site+0x310]".to_string(), + "0x00410b30..0x004118f4 candidate-processing loop writing [site+0x310/+0x338/+0x360] after 0x00412560".to_string(), + ], + evidence: vec![ + "0x0040cac0 is only the exact raw delta reader over [site+0x310/+0x338/+0x360]".to_string(), + "direct local binary inspection now shows 0x0040c9a0 as the deferred additive accumulator over [site+0x310/+0x338/+0x360], folding that tri-lane into [site+0x2b4/+0x2b8/+0x2bc], mirroring the nine-dword side array rooted at [site+0x2e4], and then clearing the tri-lane".to_string(), + "direct local caller census now shows 0x0040c9a0 only under the broad live-collection sweep 0x0040a3a1..0x0040a4d3, while 0x0040cac0 stays under weighted scoring or evaluation families such as 0x0040fcc0..0x0040fe28 and 0x00422c62..0x00422d3c".to_string(), + "direct local binary inspection now shows concrete live producers too: 0x0040d4aa/0x0040d4b0 add into [site+0x310], 0x0041114a7/0x004111572 add into [site+0x310], 0x0041114b7/0x004111582 add into [site+0x338], and 0x0041118aa/0x0041118f4 add into [site+0x360]".to_string(), + "0x0040d450 is a small owner-company-aware producer over [site+0x276], 0x00455810/0x00455800/0x0044ad60, and 0x00436590 ids 0x66/0x68 that writes directly into [site+0x310]".to_string(), + "0x00410b30..0x004118f4 is a broader candidate-processing loop walking 0xbc-stride rows, gating them through 0x00412560, and then accumulating stack temporaries plus direct writes into [site+0x310/+0x338/+0x360]".to_string(), + "0x00412560 itself is a shared candidate/admissibility gate over candidate-row fields [+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44], world date/flags via 0x006cec78, and the candidate table 0x0062ba8c".to_string(), + "current checked-in save owners still do not serialize those lanes directly, which rules down the known save seams while leaving a later restore family open".to_string(), + "0x00481430 -> 0x0047d8e0 repopulates the dynamic side-buffer route-entry list, three byte arrays, five proximity buckets, and trailing scratch band from stream without claiming the tri-lane".to_string(), + ], + blockers: vec![ + "no checked-in triplet or side-buffer payload field is tied directly to the tri-lane, and the remaining open question is now the exact service semantics and persisted inputs of the grounded live scorer family rather than the existence of runtime writers".to_string(), + ], + }, + SmpServiceConsumerHypothesis { + label: "cached_source_candidate_id_to_subtype_projection".to_string(), + status: "grounded_stream_load_callback_0x40ce60".to_string(), + candidate_consumers: vec![ + "0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70".to_string(), + "0x005c8c50 +0x40 stream-load callback 0x0040ce60".to_string(), + "0x0040cd70 cached source/candidate resolver seeding [site+0x3cc/+0x3d0]".to_string(), + "0x0040cee0 cached candidate resolver through 0x0062b268".to_string(), + "0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0 aux-candidate load chain".to_string(), + ], + evidence: vec![ + "[site+0x04] backing-record selector owner is grounded, but the stronger checked-in bridge is now [site+0x3cc/+0x3d0]".to_string(), + "direct local binary inspection now shows the placed-structure stream-load path 0x00413280 dispatching per-entry vtable slot +0x40 on the 0x005c8c50 specialization table, and that slot resolves to 0x0040ce60".to_string(), + "0x0040ce60 canonicalizes the Radio Station stem and then re-enters 0x0040cd70 plus 0x0045c150 on stream load".to_string(), + "0x0040cd70 rebuilds cached source id [site+0x3cc] and candidate/profile id [site+0x3d0]".to_string(), + "0x0040cee0 resolves cached candidate id [site+0x3d0] back into the live candidate pool 0x0062b268".to_string(), + "0x004138f0 already counts live placed structures by cached candidate id [site+0x3d0], confirming that lane as a real live selector bridge".to_string(), + "candidate subtype ownership is bounded under the aux-candidate load and stem-policy chain".to_string(), + "0x0040d360 only consumes the loaded candidate subtype byte [candidate+0x32] == 4".to_string(), + ], + blockers: Vec::new(), + }, + ] +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/input_fields.rs b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/input_fields.rs new file mode 100644 index 0000000..ef37d1a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/input_fields.rs @@ -0,0 +1,37 @@ +pub(super) fn build_near_city_input_fields() -> (Vec, Vec, Vec) { + ( + vec![ + "[site+0x276] owner-present gate".to_string(), + "placed-structure subject subtype gate [candidate+0x32] == 4 consumed through 0x0040d360" + .to_string(), + "[site+0x3d5] age/year delta lane".to_string(), + "[site+0x310/+0x338/+0x360] cached tri-lane sampled through 0x0040cac0".to_string(), + "[site+0x2a4] self placed-structure id lane later consumed through 0x004269b0" + .to_string(), + ], + vec![ + "center-cell token gate 0x0041f6e0 -> 0x0042b2d0 over the current region".to_string(), + "[site+0x04] live backing-record selector consumed by 0x0047efe0 / 0x0047fd50" + .to_string(), + "0x0047fd50 linked-peer candidate-class gate over [candidate+0x8c] accepting only 0/1/2" + .to_string(), + "[site+0x2a8] linked peer-site id consumed by 0x0040d1f0".to_string(), + "[peer+0x08] route-entry anchor id consumed by 0x0047dda0".to_string(), + "world-cell owner chain [cell+0xd4] and linked-site chain [cell+0xd6] republished by 0x00480710" + .to_string(), + "linked-region status branch 0x0047de00 -> 0x0040c990".to_string(), + ], + vec![ + "company stat-family reader 0x2329/0x0d through 0x0042a5d0".to_string(), + "save-native linked-transit route-anchor entry id [company+0x0d35] through 0x00401860" + .to_string(), + "save-native linked-transit route-anchor fallback counts [company+0x7664/+0x7668/+0x766c] through 0x00401860" + .to_string(), + "current chairman profile byte [profile+0x291] through 0x00426ef0".to_string(), + "company byte [company+0x5b] and indexed lane [company+0x67 + 12*0x0042a0e0()]" + .to_string(), + "company-root argument [company+0x00] passed into 0x0040d540 and 0x00455f60" + .to_string(), + ], + ) +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/mod.rs new file mode 100644 index 0000000..8b8772d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/mod.rs @@ -0,0 +1,73 @@ +use crate::inspect::smp::services::*; + +mod gaps; +mod hypotheses; +mod input_fields; +mod model; +mod projection_status; +mod tri_lane; + +use gaps::build_near_city_owner_gaps_and_region_statuses; +use hypotheses::build_near_city_projection_hypotheses; +use input_fields::build_near_city_input_fields; +pub(super) use model::NearCityAcquisitionTraceInputs; +use projection_status::build_near_city_projection_status_fields; +use tri_lane::build_near_city_tri_lane_fields; + +pub(super) fn build_near_city_acquisition_trace_inputs( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> NearCityAcquisitionTraceInputs { + let ( + near_city_acquisition_region_input_fields, + near_city_acquisition_peer_input_fields, + near_city_acquisition_company_input_fields, + ) = build_near_city_input_fields(); + let projection = build_near_city_projection_status_fields(); + let tri_lane = build_near_city_tri_lane_fields(analysis); + let near_city_acquisition_projection_hypotheses = build_near_city_projection_hypotheses(); + let (near_city_acquisition_remaining_owner_gaps, near_city_acquisition_region_lane_statuses) = + build_near_city_owner_gaps_and_region_statuses(); + + NearCityAcquisitionTraceInputs { + near_city_acquisition_region_input_fields, + near_city_acquisition_peer_input_fields, + near_city_acquisition_company_input_fields, + near_city_acquisition_shellless_readiness_status: projection + .near_city_acquisition_shellless_readiness_status, + near_city_acquisition_runtime_backed_input_families: projection + .near_city_acquisition_runtime_backed_input_families, + near_city_acquisition_site_owner_company_projection_status: projection + .near_city_acquisition_site_owner_company_projection_status, + near_city_acquisition_site_self_id_projection_status: projection + .near_city_acquisition_site_self_id_projection_status, + near_city_acquisition_site_cached_tri_lane_projection_status: projection + .near_city_acquisition_site_cached_tri_lane_projection_status, + near_city_acquisition_tri_lane_live_service_status: projection + .near_city_acquisition_tri_lane_live_service_status, + near_city_acquisition_candidate_subtype_projection_status: projection + .near_city_acquisition_candidate_subtype_projection_status, + near_city_acquisition_backing_record_projection_status: projection + .near_city_acquisition_backing_record_projection_status, + near_city_acquisition_nontransport_persisted_source_status: projection + .near_city_acquisition_nontransport_persisted_source_status, + near_city_acquisition_nontransport_persisted_source_candidates: projection + .near_city_acquisition_nontransport_persisted_source_candidates, + near_city_acquisition_tri_lane_save_shape_family_status: tri_lane + .near_city_acquisition_tri_lane_save_shape_family_status, + near_city_acquisition_tri_lane_save_shape_family_candidates: tri_lane + .near_city_acquisition_tri_lane_save_shape_family_candidates, + near_city_acquisition_tri_lane_live_owner_families: tri_lane + .near_city_acquisition_tri_lane_live_owner_families, + near_city_acquisition_tri_lane_candidate_gate_fields: tri_lane + .near_city_acquisition_tri_lane_candidate_gate_fields, + near_city_acquisition_tri_lane_runtime_writer_roles: tri_lane + .near_city_acquisition_tri_lane_runtime_writer_roles, + near_city_acquisition_tri_lane_direct_caller_families: tri_lane + .near_city_acquisition_tri_lane_direct_caller_families, + near_city_acquisition_tri_lane_formula_input_lanes: tri_lane + .near_city_acquisition_tri_lane_formula_input_lanes, + near_city_acquisition_projection_hypotheses, + near_city_acquisition_remaining_owner_gaps, + near_city_acquisition_region_lane_statuses, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/model.rs b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/model.rs new file mode 100644 index 0000000..c6d1d60 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/model.rs @@ -0,0 +1,31 @@ +use crate::inspect::smp::services::*; + +pub(in crate::inspect::smp) struct NearCityAcquisitionTraceInputs { + pub(in crate::inspect::smp) near_city_acquisition_region_input_fields: Vec, + pub(in crate::inspect::smp) near_city_acquisition_peer_input_fields: Vec, + pub(in crate::inspect::smp) near_city_acquisition_company_input_fields: Vec, + pub(in crate::inspect::smp) near_city_acquisition_shellless_readiness_status: String, + pub(in crate::inspect::smp) near_city_acquisition_runtime_backed_input_families: Vec, + pub(in crate::inspect::smp) near_city_acquisition_site_owner_company_projection_status: String, + pub(in crate::inspect::smp) near_city_acquisition_site_self_id_projection_status: String, + pub(in crate::inspect::smp) near_city_acquisition_site_cached_tri_lane_projection_status: + String, + pub(in crate::inspect::smp) near_city_acquisition_tri_lane_live_service_status: String, + pub(in crate::inspect::smp) near_city_acquisition_candidate_subtype_projection_status: String, + pub(in crate::inspect::smp) near_city_acquisition_backing_record_projection_status: String, + pub(in crate::inspect::smp) near_city_acquisition_nontransport_persisted_source_status: String, + pub(in crate::inspect::smp) near_city_acquisition_nontransport_persisted_source_candidates: + Vec, + pub(in crate::inspect::smp) near_city_acquisition_tri_lane_save_shape_family_status: String, + pub(in crate::inspect::smp) near_city_acquisition_tri_lane_save_shape_family_candidates: + Vec, + pub(in crate::inspect::smp) near_city_acquisition_tri_lane_live_owner_families: Vec, + pub(in crate::inspect::smp) near_city_acquisition_tri_lane_candidate_gate_fields: Vec, + pub(in crate::inspect::smp) near_city_acquisition_tri_lane_runtime_writer_roles: Vec, + pub(in crate::inspect::smp) near_city_acquisition_tri_lane_direct_caller_families: Vec, + pub(in crate::inspect::smp) near_city_acquisition_tri_lane_formula_input_lanes: Vec, + pub(in crate::inspect::smp) near_city_acquisition_projection_hypotheses: + Vec, + pub(in crate::inspect::smp) near_city_acquisition_remaining_owner_gaps: Vec, + pub(in crate::inspect::smp) near_city_acquisition_region_lane_statuses: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/projection_status.rs b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/projection_status.rs new file mode 100644 index 0000000..80c7fcd --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/projection_status.rs @@ -0,0 +1,97 @@ +pub(super) struct NearCityProjectionStatusFields { + pub(super) near_city_acquisition_shellless_readiness_status: String, + pub(super) near_city_acquisition_runtime_backed_input_families: Vec, + pub(super) near_city_acquisition_site_owner_company_projection_status: String, + pub(super) near_city_acquisition_site_self_id_projection_status: String, + pub(super) near_city_acquisition_site_cached_tri_lane_projection_status: String, + pub(super) near_city_acquisition_tri_lane_live_service_status: String, + pub(super) near_city_acquisition_candidate_subtype_projection_status: String, + pub(super) near_city_acquisition_backing_record_projection_status: String, + pub(super) near_city_acquisition_nontransport_persisted_source_status: String, + pub(super) near_city_acquisition_nontransport_persisted_source_candidates: Vec, +} + +pub(super) fn build_near_city_projection_status_fields() -> NearCityProjectionStatusFields { + NearCityProjectionStatusFields { + near_city_acquisition_shellless_readiness_status: + "peer_and_company_inputs_grounded_runtime_projection_and_periodic_service_present" + .to_string(), + near_city_acquisition_runtime_backed_input_families: vec![ + "peer-site restore subset [site+0x3cc/+0x3d0] plus tagged 0x5dc1 [owner+0x23e/+0x242]" + .to_string(), + "peer-site bring-up replay path reconstructing [site+0x04], [site+0x2a8], and [peer+0x08]" + .to_string(), + "linked-site post-load replay republishing world-cell owner and linked-site chains through 0x0042bbf0/0x0042bbb0 and 0x0042c9f0/0x0042c9a0" + .to_string(), + "placed-structure linked-company resolver 0x0047efe0 already grounds the live owner-company meaning of [site+0x276]" + .to_string(), + "placed-structure peer-chain helpers 0x0041f7e0 / 0x0041f810 / 0x0041f850 already ground [site+0x2a4] as the record's own placed-structure id lane" + .to_string(), + "0x004269b0 resolves the chosen site id back through live placed-structure collection 0x0062b26c before mutating [site+0x276], so the [site+0x2a4] self-id lane is reconstructible from collection identity once the chosen live row is known" + .to_string(), + "aux-candidate stream-load and stem-policy chain 0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0 already grounds the subtype byte consumed as [candidate+0x32]" + .to_string(), + "direct site constructor 0x004134d0 allocates through 0x00518900 and seeds broad row state through 0x0040f6d0, including [site+0x2a4], copied name bytes, [site+0x276], [site+0x3d4/+0x3d5], and cleared tri-lane-adjacent caches" + .to_string(), + "shared finalize helper 0x0040ef10 now has both create-side callers 0x00403ef3 / 0x00404489 and data-driven loader callers 0x0046f073 / 0x004707ff, so the [site+0x276] owner lane is grounded under constructor-plus-finalize families rather than only the post-load local replay strip" + .to_string(), + "data-driven loader callers 0x0046f073 / 0x004707ff push tuple fields [+0x00/+0x04/+0x0c] into 0x0040ef10, and that helper's third argument flows into ebx and then [site+0x276] at 0x0040f5d4" + .to_string(), + "at least one of those tuple-backed callers is now classified too: 0x004707ff sits under multiplayer transport selector-0x13 body 0x004706b0 rather than the ordinary save-load restore strip" + .to_string(), + "another tuple-backed 0x004134d0 family is classified too now: 0x00472b40 is the multiplayer transport selector-0x72 counted live-world apply path, and its inner builders 0x00472bef / 0x00472d03 reach 0x004134d0 from counted transport records rather than ordinary save-load restore" + .to_string(), + "non-transport caller 0x00422bb4 also reaches 0x004134d0, but it pushes live args plus literal flags 1/0 and returns the created row id through an out-param instead of feeding the tuple-backed finalize path" + .to_string(), + "the surviving 0x00508fd1 / 0x005098eb family is bounded away from persisted restore too: it caches the created site id in [this+0x7c], re-enters 0x0040eba0 with immediate coords, and later calls 0x0040ef10 with a hard zero third arg" + .to_string(), + "0x00473c20 is a separate live queue-drain family over scratch band 0x006ce808..0x006ce988: it iterates queued site ids and coordinate pairs, re-enters 0x0040eba0 at 0x00473c98, then clears each queued id, so it is a local post-create refresh path rather than a persisted replay owner" + .to_string(), + "the remaining direct [site+0x276] store census is bounded away from persisted replay too: 0x0042128d is broad zero-init in the 0x00421430 constructor neighborhood, 0x00422305 computes a live score/category lane before publishing 0x7, 0x004269c9/0x00426a2a are acquisition commit and clear helpers, and 0x004282a9/0x004300d6 are bulk owner-transfer writes" + .to_string(), + "the paired collection-side loader 0x00413440 is bounded away too: it owns the tagged 0x36b1/0x36b2/0x36b3 triplet load path, dispatches each live record through vtable slot +0x44, and keeps that seam on the already-grounded triplet payload rather than the missing [site+0x276] replay owner" + .to_string(), + "station-detail mutation path 0x0040dc40 already consumes [site+0x276], company stat-family 0x2329/0x0d, and candidate field [candidate+0x22], then commits linked-site side-state rebuild through 0x0040d1f0 / 0x00480710 / 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70" + .to_string(), + "city-connection direct-placement family 0x00402cb0 -> 0x00403ed5/0x0040446b -> 0x004134d0 -> 0x0040ef10 already grounds the shared allocator/finalize path for newly created site rows" + .to_string(), + "opcode-dispatch strip 0x00431b20 routes grouped 0x0061039c opcodes into the same live site-mutation helpers 0x00430040 / 0x00426d60 / 0x0042fc90 rather than the bring-up replay family" + .to_string(), + "direct local writer strip now grounds live cached-tri-lane producers too: 0x0040d450 writes [site+0x310] through owner-company-aware scoring, and the broader candidate-processing loop 0x00410b30..0x004118f4 writes [site+0x310/+0x338/+0x360] after gating rows through 0x00412560" + .to_string(), + "company stat-family lane 0x2329/0x0d already rehosted in runtime company state" + .to_string(), + "company market state now carries the save-native linked-transit route-anchor tuple [company+0x0d35] and [company+0x7664/+0x7668/+0x766c]" + .to_string(), + "chairman personality byte [profile+0x291] already reconstructed from raw save chairman rows" + .to_string(), + "company-root pointer and linked chairman/company save-native roster identity already imported" + .to_string(), + ], + near_city_acquisition_site_owner_company_projection_status: + "best_effort_runtime_projection_from_save_collections_and_company_latches" + .to_string(), + near_city_acquisition_site_self_id_projection_status: + "live_meaning_grounded_reconstructible_from_collection_identity".to_string(), + near_city_acquisition_site_cached_tri_lane_projection_status: + "best_effort_runtime_projection_from_side_buffer_and_region_row_signatures" + .to_string(), + near_city_acquisition_tri_lane_live_service_status: + "candidate_gate_grounded_runtime_projection_and_periodic_commit_present" + .to_string(), + near_city_acquisition_candidate_subtype_projection_status: + "cached_candidate_id_bridge_grounded_via_stream_load".to_string(), + near_city_acquisition_backing_record_projection_status: + "stream_load_callback_grounded_via_0x40ce60".to_string(), + near_city_acquisition_nontransport_persisted_source_status: + "best_effort_runtime_projection_from_company_market_and_name_pair_alignment" + .to_string(), + near_city_acquisition_nontransport_persisted_source_candidates: vec![ + "ordinary loaded runtime-effect lane 0x00444d92 -> 0x00432f40(kind 8) -> 0x004323a0 -> 0x00431b20".to_string(), + "non-direct runtime-event bundle 0x4e99/0x4e9a/0x4e9b decodes grouped placed-structure descriptors on checked maps".to_string(), + "restore-side loader 0x00433130 with 0x0042db20 repopulates ordinary live runtime-effect rows in 0x0062be18".to_string(), + "trigger-kind control lane [event+0x7ef] is editor-visible across 0x00..0x0a including kind 8".to_string(), + "remaining gap is which serialized/live rows feed trigger kind 8 into that lane and which loaded ordinary rows actually reach placed-structure mutation opcodes".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/tri_lane.rs b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/tri_lane.rs new file mode 100644 index 0000000..c441c60 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/near_city_acquisition/tri_lane.rs @@ -0,0 +1,86 @@ +use crate::inspect::smp::services::*; + +pub(super) struct NearCityTriLaneFields { + pub(super) near_city_acquisition_tri_lane_save_shape_family_status: String, + pub(super) near_city_acquisition_tri_lane_save_shape_family_candidates: + Vec, + pub(super) near_city_acquisition_tri_lane_live_owner_families: Vec, + pub(super) near_city_acquisition_tri_lane_candidate_gate_fields: Vec, + pub(super) near_city_acquisition_tri_lane_runtime_writer_roles: Vec, + pub(super) near_city_acquisition_tri_lane_direct_caller_families: Vec, + pub(super) near_city_acquisition_tri_lane_formula_input_lanes: Vec, +} + +pub(super) fn build_near_city_tri_lane_fields( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> NearCityTriLaneFields { + let near_city_acquisition_tri_lane_save_shape_family_candidates = + summarize_near_city_acquisition_tri_lane_save_shape_family_candidates(analysis); + let near_city_acquisition_tri_lane_save_shape_family_status = + if near_city_acquisition_tri_lane_save_shape_family_candidates.is_empty() { + "save_shape_family_probe_missing".to_string() + } else { + "save_shape_family_candidates_present_fixed_offset_ruled_down".to_string() + }; + + NearCityTriLaneFields { + near_city_acquisition_tri_lane_save_shape_family_status, + near_city_acquisition_tri_lane_save_shape_family_candidates, + near_city_acquisition_tri_lane_live_owner_families: vec![ + "0x0040d450 owner-company-aware local scorer producing [site+0x310]".to_string(), + "0x00410b30..0x004118f4 broader candidate-processing loop producing [site+0x310/+0x338/+0x360]" + .to_string(), + "0x00412560 shared candidate/admissibility gate above both scorer paths" + .to_string(), + "0x0040c9a0 deferred additive accumulator/reset folding the tri-lane into [site+0x2b4/+0x2b8/+0x2bc] and [site+0x2e4..]" + .to_string(), + "0x0040fcc0..0x0040fe28 and 0x00422c62..0x00422d3c weighted scoring/evaluation consumers reading 0x0040cac0" + .to_string(), + ], + near_city_acquisition_tri_lane_candidate_gate_fields: vec![ + "0x00412560 gates candidate rows using fields [+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44]" + .to_string(), + "0x00412560 consumes world date/flags through 0x006cec78".to_string(), + "0x00412560 resolves candidate rows from table 0x0062ba8c".to_string(), + "0x00412560 callers pass the placed-structure subject vtable slot +0x80 result plus owner-present flag from [site+0x246]" + .to_string(), + "direct callers of 0x00412560 are now bounded at 0x0040fb8d, 0x00410721, 0x00410b71, 0x00412620, and 0x004126d3" + .to_string(), + ], + near_city_acquisition_tri_lane_runtime_writer_roles: vec![ + "0x0040d450 adds one owner-company-aware local score component into [site+0x310]" + .to_string(), + "0x00410b30..0x004118f4 walks 0xbc-stride candidate rows and adds per-row score components into [site+0x310/+0x338/+0x360]" + .to_string(), + "0x0041114a7/0x004111572 add into [site+0x310] and 0x0041114b7/0x004111582 add into [site+0x338]" + .to_string(), + "0x0041118aa/0x0041118f4 add into [site+0x360]".to_string(), + "0x0040c9a0 later folds the tri-lane into [site+0x2b4/+0x2b8/+0x2bc] and clears the transient producer lanes" + .to_string(), + ], + near_city_acquisition_tri_lane_direct_caller_families: vec![ + "0x0040fb70 is a small wrapper passing one candidate row plus the subject vtable slot +0x80 result and owner-present flag into 0x00412560" + .to_string(), + "0x004b4052 / 0x004b46ec are collection-wide 0x0040fb70 callers iterating 0x0062b26c candidate rows" + .to_string(), + "0x00401633 is an acquisition-adjacent 0x0040d540 caller that immediately feeds company stat-family 0x2329/0x0d" + .to_string(), + "0x0044b81a is an owner-company-aware 0x0040d540 caller that also reaches 0x0040cb70 and 0x00436590 news/event id 0x65" + .to_string(), + "0x004b70f5 / 0x004b7979 are broader 0x0040d540 callers routing through 0x004337a0 and downstream 0x00540120 / 0x00518140 state consumers" + .to_string(), + ], + near_city_acquisition_tri_lane_formula_input_lanes: vec![ + "0x00412560 uses candidate-row time window [+0x20/+0x22], owner/absence booleans [+0x24/+0x28], list count [+0x2c], and membership list [+0x44]" + .to_string(), + "0x00412560 consumes world date and policy flags through 0x006cec78 fields [world+0x0d] and [world+0x4afb]" + .to_string(), + "0x0040d450 combines helper outputs 0x00455810 / 0x00455800 / 0x0044ad60 with owner-company lane [site+0x276] and world event sink 0x00436590 ids 0x66/0x68" + .to_string(), + "0x00410b30..0x004118f4 consumes candidate-row fields [+0x18/+0x1c/+0x2a/+0x2c/+0x44], subject latch [site+0x78c], and personality byte [site+0x391]" + .to_string(), + "0x00410b30..0x004118f4 also feeds world-side scalar [world+0x4caa], owner-company scalar [company+0x0d5d] through 0x0040d210, and the local cache bands [site+0x2e8], [site+0x310], [site+0x338], and [site+0x360]" + .to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/notes/acquisition_frontier.rs b/crates/rrt-runtime/src/inspect/smp/services/company/notes/acquisition_frontier.rs new file mode 100644 index 0000000..ed55dc5 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/notes/acquisition_frontier.rs @@ -0,0 +1,94 @@ +use super::super::near_city_acquisition::NearCityAcquisitionTraceInputs; + +pub(super) fn push_acquisition_frontier_notes( + notes: &mut Vec, + near_city: &NearCityAcquisitionTraceInputs, +) { + notes.push( + "Periodic company service trace is intentionally an outer-owner probe: it reports save-owned branch inputs and blocker seams without serializing the full projected runtime reader state.".to_string(), + ); + notes.push( + "Direct disassembly now narrows the acquisition-side sibling substantially: 0x004014b0 gates on the periodic outer owner, then scans the live placed-structure collection at 0x0062b26c, rejects sites whose owner field [site+0x276] is already nonzero, filters candidates through the subtype-4 predicate 0x0040d360, scores surviving sites against company linkage/age/proximity through 0x0040d540 and 0x0040cac0, and then commits the chosen site through 0x004269b0 before the shared news lane 0x004554e0 formats the headline.".to_string(), + ); + notes.push( + "The shellless acquisition frontier is narrower now too: the peer-site selector/linked-peer seam is grounded enough to plan around, the company/chairman side now includes the save-native route-anchor tuple used by 0x00401860, the live owner-company meaning of [site+0x276] is grounded through 0x0047efe0 / 0x0040d210, the self-id meaning of [site+0x2a4] is grounded through 0x0041f7e0 / 0x0041f810 / 0x0041f850, constructor-side 0x00480210 already seeds that self-id lane for new linked-site rows, 0x004269b0 resolves the chosen site id back through 0x0062b26c before mutating [site+0x276], the cached tri-lane now has grounded live producers at 0x0040d450 and 0x00410b30..0x004118f4 above 0x00412560, and the subtype owner strip is bounded under the aux-candidate load chain 0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0. The remaining blocker is the placed-structure-side restore or replay ownership for [site+0x276], the persisted inputs and exact shellless semantics of the live cached tri-lane scorer family, plus the projection from [site+0x04] back into the loaded candidate subtype row.".to_string(), + ); + notes.push( + "The acquisition-side consumer family is tighter now too. The checked-in station-detail action path 0x0040dc40 already consumes live owner company [site+0x276], candidate field [candidate+0x22], company stat-family 0x2329/0x0d, projected-cell validation 0x00417840 -> 0x004197e0, and compact-grid replay 0x004142c0/0x004142d0 before it commits the linked-site mutation through 0x0040d1f0 -> 0x00480710 -> 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70. That means these lanes are already grounded as live preconditions and mutation-side rebuild inputs, even though the save or replay owner that populates them for shellless acquisition is still the open question.".to_string(), + ); + notes.push( + "The create-side family is grounded separately too. City-connection direct placement already reaches the shared constructor/finalize strip 0x00402cb0 -> 0x00403ed5/0x0040446b -> 0x004134d0 -> 0x0040ef10, and the direct writer census now shows [site+0x276] writes clustering under create-side, acquisition-side, and bulk control-transfer families rather than under the known replay strip. So the remaining shellless gap is no longer 'how are new placed structures finalized?', '[site+0x2a4] mystery', or 'does the tri-lane even have live writers?'; it is specifically how restored existing rows regain the owner-company lane and which persisted inputs feed the grounded tri-lane scorer family before acquisition-style consumers run.".to_string(), + ); + notes.push( + "The tri-lane restore side is narrower now too. The checked-in dynamic side-buffer load owner 0x00481430 -> 0x0047d8e0 repopulates the route-entry list, three per-site byte arrays, five proximity buckets, and the trailing scratch band from stream, so that seam is no longer a plausible hidden owner for [site+0x310/+0x338/+0x360].".to_string(), + ); + notes.push( + "Direct local binary inspection now also gives the tri-lane a concrete live runtime role: 0x0040c9a0 folds [site+0x310/+0x338/+0x360] into the local scalar band [site+0x2b4/+0x2b8/+0x2bc], mirrors the nine-dword side array rooted at [site+0x2e4], and then clears the tri-lane. Caller census keeps that accumulator role narrow too: 0x0040c9a0 only appears under the broad live-collection sweep 0x0040a3a1..0x0040a4d3, while 0x0040cac0 stays under weighted scoring/evaluation families such as 0x0040fcc0..0x0040fe28 and 0x00422c62..0x00422d3c. The direct writer strip is grounded too: 0x0040d450 writes [site+0x310] through owner-company-aware scoring, and the broader 0x00410b30..0x004118f4 candidate-processing loop writes [site+0x310/+0x338/+0x360] after gating rows through 0x00412560.".to_string(), + ); + notes.push( + "Cross-save compare now tightens the save-native side too: `compare_save_region_fixed_row_run_candidates` keys the pre-region-header fixed-row bands by lane-shape fingerprint because grounded saves do not keep one stable top rows_offset, but they do retain shared shape-family matches. The tri-lane-adjacent save seam should therefore be treated as a stable row-family problem, not a fixed-offset problem." + .to_string(), + ); + if let Some(best_shape_family) = near_city + .near_city_acquisition_tri_lane_save_shape_family_candidates + .first() + { + notes.push(format!( + "The periodic-company trace now also carries the current top tri-lane-adjacent save row family: rank {} shape-family {} at rows {} with stride {} and probable density lane {:?}. That keeps the persisted side on a concrete save-native candidate family while the live tri-lane writers stay grounded separately.", + best_shape_family.rank, + best_shape_family.shape_family_signature, + best_shape_family.rows_offset_hex, + best_shape_family.row_stride_hex, + best_shape_family + .best_probable_density_lane_relative_offset_hex + .as_deref() + )); + } + notes.push( + "The periodic-company trace now also surfaces the strongest non-transport persisted source candidate for [site+0x276]: the ordinary loaded runtime-effect lane 0x00444d92 -> 0x00432f40(kind 8) -> 0x004323a0 -> 0x00431b20 above the non-direct 0x4e99/0x4e9a/0x4e9b bundle. That branch is no longer just a blocker note; the remaining question is the tighter control-lane mapping from loaded rows into trigger kind 8 and then into the placed-structure mutation opcodes." + .to_string(), + ); + notes.push( + "Installed-map cluster counts now sharpen that candidate lane too: the current rt3_105/maps corpus has 41 bundled maps, 38 of them with dispatch-strip rows, and 318 dispatch-strip records total, all still on the non-direct compact family with null trigger kind. The add-building subset inside that corpus is narrower still at 10 grouped occurrences across 7 recovered descriptor keys (Barracks, Bauxite Mine, FarmGrain, Furniture Factory, Logging Camp, Port01, Warehouse05), again all with null trigger kind. So the open question is no longer whether ordinary loaded rows can reach the 0x00431b20 strip at scale; it is specifically how any of those rows acquire or bypass the missing trigger-kind control lane." + .to_string(), + ); + notes.push( + "Direct local binary inspection now grounds the cached-candidate restore bridge too: the placed-structure stream-load owner 0x00413280 dispatches per-entry vtable slot +0x40 on the 0x005c8c50 specialization table, that slot resolves to 0x0040ce60, and 0x0040ce60 immediately re-enters 0x0040cd70 plus 0x0045c150. So the acquisition-side cached source/candidate bridge [site+0x3cc/+0x3d0] is no longer a generic restore mystery for stream-loaded rows; the remaining restored-row gaps are [site+0x276] and the deferred tri-lane.".to_string(), + ); + notes.push( + "Direct disassembly now tightens the remaining placed-structure lanes too: 0x0040cac0 is only the raw tri-lane delta reader over [site+0x310/+0x338/+0x360]; 0x0040d360 is only the exact subtype test [candidate+0x32] == 4; and the direct writer strip now shows [site+0x2a4] staying replay/constructor-owned while [site+0x310/+0x338/+0x360] is service-produced by the grounded 0x0040d450 / 0x00410b30..0x004118f4 family above 0x00412560.".to_string(), + ); + notes.push( + "That branch also reuses the same peer-site helper strip already bounded under the city-connection family: 0x0041f6e0 resolves the current center world-grid cell and checks one packed token through 0x0042b2d0, 0x0047de00 follows the linked region behind the candidate site into the status byte returned by 0x0040c990, and 0x004801a0 checks whether one candidate site is reachable from the cached company route anchor through 0x00401860 -> 0x0048e3c0.".to_string(), + ); + notes.push( + "Direct disassembly now closes the collection identity too: 0x00420030 is the boolean peer gate and 0x00420280 is the first-match selector over the live placed-structure / peer-site collection 0x006cec20, combining 0x0042b2d0, the optional linked-company filter through 0x0047efe0, the station-or-transit gate 0x0047fd50, and the linked-region status branch 0x0047de00 -> 0x0040c990.".to_string(), + ); + notes.push( + "The linked-transit sibling owner is tighter now too: timed wrapper 0x00409720 compares world counter [world+0x15] against the save-owned company refresh counters [company+0x0d3e] and [company+0x0d3a], reruns the shorter peer-cache rebuild 0x004093d0 on the tighter 0x7ff80 cadence, reruns the heavier autoroute score-cache rebuild 0x00407bd0 on the broader 0x31380 cadence, and then feeds the raw site-score total 0x00408f70 into roster balancer 0x00409950. That means the remaining blocker for shellless linked-transit maintenance is no longer the timing seam; it is the higher-layer placed-structure / infrastructure consumer mapping above those grounded cache owners.".to_string(), + ); + notes.push( + "The chooser-and-train strip is tighter now too: 0x00408280 only picks among company-owned linked-transit sites whose cache cells are already live, ranking them off site-cache lane [site+0x16] with one extra boost from local cache words [site+0x5c1/+0x5c5], while 0x00408380 either reuses one explicit site id or falls back to 0x00408280 before building a staged 0x33-byte route entry. Above that, 0x00409770 services the caches through 0x00409720, asks 0x00408380 for one staged entry, and then either appends or rotates it in-place, while 0x00409830 repeats the same builder twice before publishing the add-train/news side. So the remaining blocker is not the train-side consumer strip; it is the placed-structure-side owner seam that makes those site-cache lanes and ready bits trustworthy for shellless runs.".to_string(), + ); + notes.push( + "Those extra chooser-side local lanes are tighter now too: 0x00481910 first clears [site+0x5c1] across the live placed-structure collection and then repopulates it by resolving current site ids through 0x004a9340, while 0x004819b0 decrements one prior site's [site+0x5c1] and increments one new site's copy during later reassignment. 0x004a9340 itself is now bounded as a current-site-id resolver: it returns [this+0xa0] when present and otherwise falls back through 0x004b3360 before returning one dereferenced site id. The companion age lane [site+0x5c5] is stamped directly from world counter [world+0x15] at 0x004aee2b. 0x00408280 then uses [site+0x5c1] as an occupancy divisor/penalty and [site+0x5c5] as a zero-occupancy age bonus ladder, so those lanes are no longer anonymous chooser magic; they are grounded live cache inputs with bounded writer strips.".to_string(), + ); + notes.push( + "The per-company cache root is grounded now too: 0x00407780 allocates [site+0x5bd] as a 0x20-entry pointer table and seeds each entry with one zeroed 0x1a-byte company cache cell, while 0x004077e0 frees that same root and any nested cell payloads. That means the remaining linked-transit gap is no longer cache-root allocation identity; it is which persisted site-side inputs are sufficient to repopulate those per-company cells and the downstream score lanes before the bounded chooser/train strip runs shelllessly.".to_string(), + ); + notes.push( + "The per-company cache-cell layout is bounded now too: 0x004093d0 and 0x00407bd0 use bytes +0x00/+0x01 as the initial participation gates, dword +0x02 as the peer-row count, dword +0x06 as the peer-row pointer, dword +0x0a as the shorter peer-cache refresh stamp, and floats +0x0e/+0x12/+0x16 as the weighted/raw/final linked-transit score lanes. The candidate-table and route-entry-tracker owners are bounded above that too: 0x0062ba8c is constructed through 0x0041f4e0 -> 0x0041ede0 -> 0x0041e970, while 0x004a6360 / 0x004a6630 sit under owner-notify refresh 0x00494fb0. The remaining linked-transit gap is narrower again: subtype-4 follow-on 0x0040eba0 already republishes [site+0x2a4] through 0x004814c0 / 0x00481480 and world-cell chain helpers 0x0042c9f0 / 0x0042c9a0, and direct inspection of 0x0040ea96..0x0040eb65 shows that owner-company branch only consumes [site+0x276] rather than rehydrating it. That pushes the open question one level earlier to whichever restore or service owner feeds [site+0x276] and the live linked-peer rows before this replay continuation runs.".to_string(), + ); + notes.push( + "One nearby live helper strip is narrower now too: 0x004337a0 is exactly the raw selected-company getter over [world+0x21], used in the replay-side sibling around 0x0040e775 only to compare the current selection against [site+0x276]. The adjacent world-side helpers 0x00452d80 / 0x00452db0 / 0x00452fa0 are separate live selected-site or active service-state setters/dispatchers over [world+0x217d/+0x2181] gated by mode byte [world+0x2175]; they can clear or republish currently-selected site ids through 0x00413620 / 0x00413750, but they do not repopulate [site+0x276] for already-restored rows.".to_string(), + ); + notes.push( + "The base placed-structure load callback is narrower now too: local .rdata at 0x005cb4c0 shows that the shared base table, not the 0x005c8c50 specialization table, owns the 0x0045c150 / 0x0045b560 / 0x00455870 / 0x00455930 load-save quartet. Direct disassembly of 0x0045c150 -> 0x00455fc0 then shows that callback only reloads the generic 0x55f1/0x55f2/0x55f3 triplet/scalar bands and re-enters the same base triplet/scalar slots 0x00455870 / 0x00455930, so the missing placed-structure owner-company lane [site+0x276] still lies outside the checked-in base load path.".to_string(), + ); + notes.push( + "The remaining direct [site+0x276] writers are split more cleanly now too: 0x00421200 is the broad late-field constructor/reset zero-fill over the same 0x23a/0x23e/0x25a/0x25e/... row family and clears [+0x276] as part of that initialization; 0x00428270 is a collection-wide live owner remap over 0x0062b26c that rewrites [site+0x276] only for rows matching one caller-supplied old owner id; and 0x00422280 is a subtype-local synthetic scalar writer that buckets float lane [row+0x25e], stores one 100000 * rand(bucket) result into [+0x276], and immediately publishes localized-id 7. Those writes therefore sit under constructor/live mutation or subtype-local scalar families, not the missing restore-time owner-company replay seam.".to_string(), + ); + notes.push( + "Direct disassembly now closes the negative persistence side too: the direct 0x36b1 per-record callbacks serialize the shared base scalar triplets rooted at [this+0x206/+0x20a/+0x20e] plus the subordinate payload callback strip, while the 0x4a9d/0x4a3a/0x4a3b side-buffer owner only persists route-entry lists, three byte arrays, five proximity buckets, and the sampled-cell list. That means neither checked-in save owner seam currently persists the core peer-site identity fields [site+0x04], [site+0x2a8], or [peer+0x08] directly.".to_string(), + ); +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/notes/linked_transit_reference.rs b/crates/rrt-runtime/src/inspect/smp/services/company/notes/linked_transit_reference.rs new file mode 100644 index 0000000..c14169e --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/notes/linked_transit_reference.rs @@ -0,0 +1,192 @@ +use super::LinkedTransitTraceInputs; + +pub(super) fn build_linked_transit_trace_inputs() -> LinkedTransitTraceInputs { + LinkedTransitTraceInputs { + atlas_candidate_consumers: vec![ + "0x004019e0 periodic company outer service owner".to_string(), + "0x00406050 city-connection bonus/news owner".to_string(), + "0x00402cb0 city-connection direct-placement builder owner".to_string(), + "0x00409950 linked-transit train-roster balancer".to_string(), + "0x004014b0 near-city industry acquisition and news owner".to_string(), + "0x0040dc40 station-detail linked-site mutation validator/apply owner".to_string(), + "0x00401c50 annual finance-policy owner".to_string(), + "0x00420030 / 0x00420280 peer-site boolean/selector pair over 0x006cec20".to_string(), + "0x004093d0 / 0x00407bd0 linked-transit refresh tails".to_string(), + ], + known_bridge_helpers: vec![ + "0x004078a0 preferred-locomotive chooser feeding company byte 0x0d17".to_string(), + "0x0041d550 locomotive-era and engine-type approval gate over scenario opinion lanes" + .to_string(), + "0x004010f0 near-city acquisition region scorer over class-0 region entries".to_string(), + "0x00405920 same-company industry proximity aggregator over linked site peers" + .to_string(), + "0x0041f6e0 center-cell token gate feeding 0x0042b2d0 over the current region" + .to_string(), + "0x0042b2d0 packed-u16 contains-key predicate over the region token list".to_string(), + "0x0047de00 linked-region resolver feeding the candidate status branch 0x0040c990" + .to_string(), + "0x004801a0 linked-transit route-anchor reachability gate for one candidate site" + .to_string(), + "0x00425b90 pending-bonus/company-state gate over the [region+0x276] companion object" + .to_string(), + "0x0040cac0 placed-structure cached tri-lane delta sampler over [site+0x310/+0x338/+0x360]" + .to_string(), + "0x0040c9a0 deferred additive accumulator/reset folding [site+0x310/+0x338/+0x360] into [site+0x2b4/+0x2b8/+0x2bc] and the side array rooted at [site+0x2e4]" + .to_string(), + "0x0040d450 owner-company-aware local scorer writing directly into [site+0x310] via 0x00455810/0x00455800/0x0044ad60 and 0x00436590 ids 0x66/0x68" + .to_string(), + "0x0040fb70 small wrapper passing the subject vtable slot +0x80 result plus owner-present flag into 0x00412560" + .to_string(), + "0x00412560 shared candidate/admissibility gate over 0x0062ba8c candidate rows and world date/flags before the tri-lane scoring loop" + .to_string(), + "0x00410b30..0x004118f4 broader candidate-processing loop writing [site+0x310/+0x338/+0x360] after 0x00412560" + .to_string(), + "0x00401633 acquisition-adjacent 0x0040d540 caller immediately feeding company stat-family 0x2329/0x0d" + .to_string(), + "0x0044b81a owner-company-aware 0x0040d540 caller reaching 0x0040cb70 and 0x00436590 news/event id 0x65" + .to_string(), + "0x004b4052 / 0x004b46ec collection-wide 0x0040fb70 callers iterating 0x0062b26c candidate rows" + .to_string(), + "0x004b70f5 / 0x004b7979 broader 0x0040d540 callers routing through 0x004337a0 and downstream 0x00540120 / 0x00518140 state consumers" + .to_string(), + "0x0040d360 placed-structure subtype-4 predicate over [candidate+0x32]".to_string(), + "0x0040d540 weighted region-to-company proximity scorer with pending-bonus context" + .to_string(), + "0x0040f6d0 / 0x00481390 / 0x00480210 subtype-1 constructor family seeding linked record own id, anchor-site id, and linked peer id" + .to_string(), + "0x0040d210 owner-side placed-structure resolver from [site+0x276] through 0x0062be10" + .to_string(), + "0x0041f7e0 / 0x0041f810 / 0x0041f850 peer-chain helpers grounding [site+0x2a4] as the record's own site id" + .to_string(), + "0x00481390 / 0x00480210 subtype-1 linked-site allocation and constructor".to_string(), + "0x00444690 late world bring-up caller of 0x004133b0 placed-structure local-runtime replay" + .to_string(), + "0x004133b0 placed-structure local-runtime replay owner draining queued site ids through 0x0040e450 and sweeping live sites through 0x0040ee10".to_string(), + "0x0040e360..0x0040edf6 broader replay continuation consuming [site+0x2a8/+0x2a4/+0x276] around 0x0040d230 / 0x0040d1f0 / 0x00480710 / 0x00426b10 / 0x00455860" + .to_string(), + "0x0040e450 queued site-id cloned local-runtime replay helper".to_string(), + "0x0040ee10 live-site position/scalar refresh helper reaching 0x0040edf6 -> 0x00480710 and 0x0040e360".to_string(), + "0x00480710 linked-site runtime side-buffer and route-entry-anchor refresh owner" + .to_string(), + "0x0042bbf0 / 0x0042bbb0 world-cell owner-chain refresh over [cell+0xd4]".to_string(), + "0x0042c9f0 / 0x0042c9a0 world-cell linked-site-chain refresh over [cell+0xd6]" + .to_string(), + "0x0040df27 / 0x0040e00a / 0x0040edf6 concrete linked-site side-refresh callers of 0x00480710" + .to_string(), + "0x004160aa non-bring-up runtime caller of 0x0040ee10".to_string(), + "0x0048abc0 / 0x00493cf0 route-entry-anchor rebind and synthesis strip".to_string(), + "0x0047dda0 linked-peer route-entry-anchor validator".to_string(), + "0x00420030 / 0x00420280 peer-site boolean/selector pair over the placed-structure collection".to_string(), + "0x0047efe0 placed_structure_query_linked_company_id returning owner company id from [site+0x276]".to_string(), + "0x004269b0 acquisition commit owner resolving one site id through 0x0062b26c and then mutating [site+0x276]/[site+0x27a] on the chosen live row".to_string(), + "0x00426dce..0x00426ea1 bulk placed-structure owner-company reassignment over 0x0062b26c for non-subtype-4 rows matching the current company".to_string(), + "0x00430040..0x004300d6 filtered placed-structure owner-company overwrite over site classes 0x09/0x0b/0x0c".to_string(), + "0x00431b20 grouped opcode dispatcher over 0x0061039c routing opcodes 0x04..0x07 to 0x00430040, 0x08/0x10..0x13 to 0x00426d60, and 0x0d/0x16 to 0x0042fc90".to_string(), + "0x0042fc90 live placed-structure mutator iterating 0x0062b26c through 0x0040c990, optional owner-company match [site+0x276], and row vtable slot +0x70".to_string(), + "0x0047fd50 linked-peer candidate-class gate returning true only for class-byte values 0/1/2 at [candidate+0x8c]".to_string(), + "0x004131f0 / 0x00412fb0 / 0x004120b0 / 0x00412ab0 aux-candidate load and stem-policy chain owning subtype byte [candidate+0x32]".to_string(), + "0x004134d0 direct allocator calling constructor 0x0040f6d0 for new placed-structure rows".to_string(), + "0x0040f6d0 create-side initializer seeding [site+0x2a4], [site+0x276], [site+0x3d4/+0x3d5], and cleared local caches".to_string(), + "0x0040dc40 station-detail linked-site mutation validator/apply path consuming [site+0x276], candidate field [candidate+0x22], and company stat-family 0x2329/0x0d" + .to_string(), + "0x00417840 / 0x004197e0 / 0x004142c0 / 0x004142d0 projected-cell validation and compact-grid replay strip ahead of the linked-site mutation" + .to_string(), + "0x0040d1f0 / 0x00480710 / 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70 linked-site mutation-side side-state rebuild strip" + .to_string(), + "0x00402cb0 / 0x00403ed5 / 0x0040446b city-connection direct-placement commit family" + .to_string(), + "0x00403ef3 / 0x00404489 create-side callers of shared finalize helper 0x0040ef10".to_string(), + "0x0046f073 / 0x004707ff data-driven loader callers of shared finalize helper 0x0040ef10".to_string(), + "0x0046f073 / 0x004707ff tuple field [+0x0c] feeding 0x0040ef10 arg3 and then [site+0x276] at 0x0040f5d4".to_string(), + "0x004706b0 multiplayer transport selector-0x13 body re-entering 0x004197e0 / 0x004134d0 / 0x0040eba0 / 0x0052eb90 / 0x0040ef10 before 0x004707ff".to_string(), + "0x00472b40 multiplayer transport selector-0x72 counted live-world apply path whose inner builders 0x00472bef / 0x00472d03 reach 0x004134d0 from counted transport records".to_string(), + "0x00422bb4 direct non-tuple allocator caller pushing one 0x0062b2fc record plus local args and literal flags 1/0 into 0x004134d0, then returning the created row id through an out-param".to_string(), + "0x00508fd1 / 0x005098eb live controller family caching a created site id in [this+0x7c], re-entering 0x0040eba0 with immediate coords, and later calling 0x0040ef10 with arg3 forced to zero".to_string(), + "0x00473c20 live queued-site refresh draining scratch band 0x006ce808..0x006ce988 and re-entering 0x0040eba0 at 0x00473c98 before clearing each queued id slot".to_string(), + "0x0042128d broad zero-init in the 0x00421430 constructor neighborhood clearing [site+0x276] with the surrounding site reset band".to_string(), + "0x00422305 live score/category publisher writing [site+0x276] before event 0x7 dispatch, not ordinary restore".to_string(), + "0x004269c9 / 0x00426a2a acquisition commit and clear helpers mutating [site+0x276]/[site+0x27a] on chosen live rows".to_string(), + "0x004282a9 / 0x004300d6 bulk owner-transfer writes over existing live placed-structure rows".to_string(), + "0x00413440 paired tagged 0x36b1/0x36b2/0x36b3 collection serializer dispatching each live record through vtable slot +0x44".to_string(), + "0x004134d0 / 0x0040ef10 shared placed-structure allocator and finalize-or-rebuild lane for newly created or tuple-loaded site rows" + .to_string(), + "0x00481430 / 0x0047d8e0 dynamic side-buffer stream-load owner repopulating route-entry lists, three byte arrays, five proximity buckets, and trailing scratch band" + .to_string(), + "0x0040c9a0 deferred additive accumulator/reset helper folding tri-lane [site+0x310/+0x338/+0x360] into [site+0x2b4/+0x2b8/+0x2bc] and mirroring the nine-dword side array rooted at [site+0x2e4]" + .to_string(), + "0x0040a3a1..0x0040a4d3 broad live-collection maintenance sweep calling 0x0040c9a0 once per live placed structure after sibling sweeps over companies, source records, and peer sites".to_string(), + "0x0040fcc0..0x0040fe28 and 0x00422c62..0x00422d3c weighted scoring families consuming 0x0040cac0 without grounding a tri-lane producer".to_string(), + "0x005c8c50 +0x40 stream-load callback 0x0040ce60 canonicalizing the site stem and re-entering 0x0040cd70 / 0x0045c150 for restored rows" + .to_string(), + "0x0052edf0 generic base constructor seeding [this+0x04] from caller arg 1".to_string(), + "0x00455b70 placed-structure specialization constructor feeding 0x0052edf0 arg 3 as the primary selector and arg 1 as fallback" + .to_string(), + "0x00455c62 concrete placed-structure specialization caller of 0x0052edf0".to_string(), + "0x00456100 local wrapper duplicating its first incoming arg across the 0x00455b70 selector/fallback bundle" + .to_string(), + "0x00456072 fixed 0x55f2 callback forwarding three local dwords plus unit scalars into 0x00455b70" + .to_string(), + "0x0045c36e / 0x0045da65 / 0x0045e0fc dense 0x00456100 caller family over stack-backed buffers and default scalar lanes" + .to_string(), + "0x0045c36e feeds 0x00456100 selector arg 1 from [owner+0x23e], 0x0045da65 feeds zero, and 0x0045e0fc feeds [ebp+0x08]" + .to_string(), + "0x0045c150 save-backed loader for [owner+0x23e/+0x242] via tagged payload 0x5dc1 ahead of 0x0045c310" + .to_string(), + "0x0040ceab / 0x0040d1a1 local linked-site helper strip calling 0x0045c150 / 0x0045c310 directly" + .to_string(), + "0x00485819 typed placed-structure caller of 0x0052edf0 via 0x530640-style argument bundle" + .to_string(), + "0x00490a79 chooser-side caller of 0x00455b70 with literal selector 0x005cfd74 and fallback seed 0x005c87a8" + .to_string(), + "0x00406050 city-connection bonus/news sibling owner".to_string(), + "0x00409950 linked-transit roster sibling owner".to_string(), + ], + next_owner_questions: vec![ + "Which persisted restore-side site and peer identity lanes are sufficient to replay the acquisition-side selector family without depending on later live cache rebuilds?".to_string(), + "Which persisted control lane above 0x00431b20 repopulates placed-structure owner-company field [site+0x276] for restored rows before the grounded selector, mutation, and acquisition families run again?".to_string(), + "Which persisted placed-structure and city-or-region linkage fields are still missing for a shellless 0x004014b0 implementation once the peer-site restore subset, company route-anchor tuple, [site+0x276] live owner meaning, [site+0x2a4] self-id meaning, and the grounded live tri-lane scorer family are all treated as bounded?".to_string(), + "Which persisted inputs and exact shellless formulas feed the grounded tri-lane live scorer family 0x0040d450 / 0x00410b30..0x004118f4 above 0x00412560 before 0x0040c9a0 folds the results into [site+0x2b4/+0x2b8/+0x2bc]?".to_string(), + "Which restore or replay continuation above 0x004160aa -> 0x0040ee10 -> 0x0040edf6 makes the linked-site side-refresh lanes trustworthy for shellless linked-transit consumers before 0x00409950 runs?".to_string(), + ], + linked_transit_shellless_readiness_status: + "timed_cache_and_train_side_followons_grounded_site_cache_input_owners_missing".to_string(), + linked_transit_minimum_persisted_identity_inputs: vec![ + "save-backed company identity, current company id, and linked-transit latch [company+0x0d56] selecting one per-company cache cell root beneath [site+0x5bd][company_id]".to_string(), + "save-backed linked-transit route-anchor tuple [company+0x0d35] and fallback count lanes [company+0x7664/+0x7668/+0x766c] feeding the reachable-site strip above 0x00401860 / 0x004801a0".to_string(), + "save-backed placed-structure owner and class identity lanes [site+0x276] and [site+0x04] consumed by 0x0047efe0 / 0x0047fd50 before any linked-transit site is marked eligible".to_string(), + "save-backed placed-structure and peer identity lanes [site+0x2a4], [site+0x2a8], and [peer+0x04/+0x08] giving the live site and linked-peer ids that the cache rebuilds query".to_string(), + "save-backed world calendar lanes [world+0x15] and [world+0x0d] driving refresh cadence, age stamping, and the year-banded policy table used by 0x00408380".to_string(), + ], + linked_transit_live_rebuilt_cache_lanes: vec![ + "0x004093d0 stamps [company+0x0d3e], clears each selected [site+0x5bd][company_id] cell, frees and reallocates its peer table at +0x06, repopulates +0x02/+0x06/+0x0a, and marks bytes +0x00/+0x01 from the live site filter".to_string(), + "0x004093d0 fills each 0x0d-stride peer row from 0x004a6630, so peer-table dword +0x05 step count and float +0x09 normalized continuity share are rebuilt scratch from live route-entry tracker results".to_string(), + "0x00407bd0 clears [site+0x0e/+0x12/+0x16] before folding the rebuilt peer rows plus candidate tables back into weighted/raw/final per-site score lanes".to_string(), + "0x00481910 and 0x004819b0 rebuild the local occupancy/count lane [site+0x5c1] from current-site-id resolver 0x004a9340 rather than from any serialized cache blob".to_string(), + "0x004aee2b rewrites [site+0x5c5] from world counter [world+0x15], making the chooser age bonus a live rebuilt lane rather than persisted cache state".to_string(), + ], + linked_transit_runtime_backed_input_families: vec![ + "company linked-transit route-anchor tuple [company+0x0d35] and fallback count lanes [company+0x7664/+0x7668/+0x766c] through 0x00401860".to_string(), + "company linked-transit peer-cache refresh absolute counter [company+0x0d3e] driving the shorter 0x00409720 -> 0x004093d0 cadence".to_string(), + "company linked-transit autoroute site-score refresh absolute counter [company+0x0d3a] driving the longer 0x00409720 -> 0x00407bd0 cadence".to_string(), + "company linked-transit latch [company+0x0d56] consumed by 0x00409950 and the annual-finance debt lane".to_string(), + "active-company refresh owner 0x00429c10 walks the live company roster and re-enters 0x004093d0 for each active company when linked-transit site-peer caches need a broader rebuild".to_string(), + "placed-structure-side company cache root [site+0x5bd] allocated by 0x00407780 as a 0x20-entry pointer table of 0x1a-byte per-company cache cells and freed by 0x004077e0".to_string(), + "placed-structure replay strip 0x00444690 -> 0x004133b0 -> 0x0040ee10 -> 0x0040edf6 -> 0x00480710 already republishes linked-peer ids, route-entry anchors, and world-cell owner chains before later recurring call 0x004160aa re-enters 0x0040ee10".to_string(), + "placed-structure-side per-company cache cells addressed through [site+0x5bd][company_id] and peer rows filled by 0x004a6630".to_string(), + "per-company cache-cell layout is bounded too: bytes +0x00/+0x01 gate participation, dword +0x02 is peer-row count, dword +0x06 is peer-row pointer, dword +0x0a is the peer-cache refresh stamp, and floats +0x0e/+0x12/+0x16 are the weighted/raw/final linked-transit score lanes".to_string(), + "linked-transit peer-table row lanes +0x05 step count and +0x09 normalized continuity share under 0x004093d0".to_string(), + "linked-transit site-score cache lanes [site+0x0e/+0x12/+0x16] rebuilt by 0x00407bd0".to_string(), + "linked-transit local site occupancy/count lane [site+0x5c1] reset by 0x00481910, repopulated from current-site-id resolver 0x004a9340, adjusted by 0x004819b0, and consumed by 0x00408280 as a divisor/penalty lane".to_string(), + "linked-transit local site age lane [site+0x5c5] stamped from world counter [world+0x15] at 0x004aee2b and consumed by 0x00408280 as the zero-occupancy age bonus ladder".to_string(), + "structure candidate table root 0x0062ba8c is world-load owned by 0x0041f4e0 -> 0x0041ede0 -> 0x0041e970 before 0x00407bd0 reuses candidate-local bands".to_string(), + "route-entry tracker compatibility and endpoint-fallback chooser 0x004a6360 / 0x004a6630 sit under owner-notify refresh 0x00494fb0, so the peer-table metric source is already bounded above the linked-transit caches".to_string(), + "linked-transit aggregate roster-pressure helper 0x00408f70 consuming raw site-score lane [site+0x12]".to_string(), + "linked-transit ranked-site chooser 0x00408280 and staged autoroute entry builder 0x00408380 above the rebuilt site caches".to_string(), + "linked-transit train-side autoroute append / rotate strip 0x00409770 plus add-train owner 0x00409830 beneath roster balancer 0x00409950".to_string(), + ], + linked_transit_remaining_owner_gaps: vec![ + "which earlier restore or service owner feeds [site+0x276] and the live linked-peer rows before replay continuation 0x0040e360..0x0040edf6, now that direct inspection shows the 0x0040ea96..0x0040eb65 owner-company branch only consumes [site+0x276] and subtype-4 follow-on 0x0040eba0 already republishes [site+0x2a4] into the world-cell chains before 0x004093d0 / 0x00407bd0 rebuild their scratch cache lanes".to_string(), + "how much of the live placed-structure collection 0x006cec20 and linked-peer replay strip still has to run shelllessly beside already-grounded candidate-table owner 0x0041f4e0 / 0x0041ede0 and route-entry tracker owner 0x00494fb0 / 0x004a6360 before 0x00408280 / 0x00408380 / 0x00409770 become trustworthy".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/notes/linked_transit_runtime.rs b/crates/rrt-runtime/src/inspect/smp/services/company/notes/linked_transit_runtime.rs new file mode 100644 index 0000000..8943261 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/notes/linked_transit_runtime.rs @@ -0,0 +1,13 @@ +pub(super) fn push_linked_transit_runtime_notes(notes: &mut Vec) { + notes.push( + "The replay strip is tighter now too. 0x00444690 is the current late world bring-up caller of 0x004133b0, that outer owner drains queued site ids through 0x0040e450 and then sweeps every live placed structure through 0x0040ee10, and 0x0040ee10 itself reaches 0x0040edf6 -> 0x00480710 plus the later 0x0040e360 follow-on. A separate runtime path at 0x004160aa also re-enters 0x0040ee10 later. So [peer+0x08] replay is no longer the open question, and [site+0x04] is no longer an owner mystery either: the local linked-site helper strip seeds [site+0x3cc/+0x3d0] from 0x62b2fc / 0x62b268, reaches the save-backed 0x0045c150 / 0x0045c310 owner directly, that owner fills [owner+0x23e/+0x242] from tagged payload 0x5dc1, and 0x0045c36e then feeds [owner+0x23e] through 0x00456100 -> 0x00455b70 -> 0x0052edf0 into [site+0x04]. The remaining non-hook target is now the smaller shellless-simulation question: which subset of those persisted site/peer fields is actually sufficient to run 0x004014b0 and its city-connection sibling without shell state.".to_string(), + ); + notes.push( + "The same persisted selector seam is broader than just the two strings. Atlas-backed recovery now bounds 0x0040c980 -> 0x0045b560 as the derived serializer over [site+0x23e/+0x242/+0x246/+0x24e/+0x252], so the remaining restore-owner search should treat that 0x5dc1/0x5dc2 selector/child/runtime bundle as one persisted field family rather than only [site+0x23e/+0x242]." + .to_string(), + ); + notes.push( + "The loader-side counterpart now narrows the shellless minimum persisted subset too. 0x0045c150 restores [owner+0x23e] and [owner+0x242], clears the transient roots, and then hands off to 0x0045c310 / 0x0045b5f0 / 0x0045b6f0 to rebuild the primary child handle and larger ambient/animation/light/random-sound family. That means the broader 0x5dc1/0x5dc2 bundle should be treated as one persisted owner seam, but current shellless planning can keep the minimum identity subset at the cached ids [site+0x3cc/+0x3d0], the restored name-pair [owner+0x23e/+0x242], and the post-secondary discriminator byte while the child/runtime follow-ons stay on the rebuild side." + .to_string(), + ); +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/notes/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/company/notes/mod.rs new file mode 100644 index 0000000..bae67d1 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/notes/mod.rs @@ -0,0 +1,33 @@ +use super::near_city_acquisition::NearCityAcquisitionTraceInputs; +use super::peer_site::PeerSiteTraceInputs; + +mod acquisition_frontier; +mod linked_transit_reference; +mod linked_transit_runtime; +mod save_side_payload; + +pub(super) struct LinkedTransitTraceInputs { + pub(super) atlas_candidate_consumers: Vec, + pub(super) known_bridge_helpers: Vec, + pub(super) next_owner_questions: Vec, + pub(super) linked_transit_shellless_readiness_status: String, + pub(super) linked_transit_minimum_persisted_identity_inputs: Vec, + pub(super) linked_transit_live_rebuilt_cache_lanes: Vec, + pub(super) linked_transit_runtime_backed_input_families: Vec, + pub(super) linked_transit_remaining_owner_gaps: Vec, +} + +pub(super) fn build_linked_transit_trace_inputs() -> LinkedTransitTraceInputs { + linked_transit_reference::build_linked_transit_trace_inputs() +} + +pub(super) fn build_periodic_company_service_notes( + peer_site: &PeerSiteTraceInputs, + near_city: &NearCityAcquisitionTraceInputs, +) -> Vec { + let mut notes = Vec::new(); + acquisition_frontier::push_acquisition_frontier_notes(&mut notes, near_city); + save_side_payload::push_save_side_payload_notes(&mut notes, peer_site); + linked_transit_runtime::push_linked_transit_runtime_notes(&mut notes); + notes +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/notes/save_side_payload.rs b/crates/rrt-runtime/src/inspect/smp/services/company/notes/save_side_payload.rs new file mode 100644 index 0000000..c348012 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/notes/save_side_payload.rs @@ -0,0 +1,85 @@ +use super::super::peer_site::PeerSiteTraceInputs; + +pub(super) fn push_save_side_payload_notes( + notes: &mut Vec, + peer_site: &PeerSiteTraceInputs, +) { + let payload_summaries = &peer_site.peer_site_selector_candidate_saved_payload_summaries; + let payload_delta_summaries = + &peer_site.peer_site_selector_candidate_saved_payload_delta_summaries; + let footer_padding_summaries = + &peer_site.peer_site_selector_candidate_saved_footer_padding_summaries; + let companion_byte_summaries = + &peer_site.peer_site_selector_candidate_saved_companion_byte_summaries; + let policy_trailing_word_summaries = + &peer_site.peer_site_selector_candidate_saved_policy_trailing_word_summaries; + let nonzero_name_pair_summaries = + &peer_site.peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries; + let building_family_overlap_summaries = &peer_site + .peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries; + let building_family_residue_summaries = &peer_site + .peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries; + + if !payload_summaries.is_empty() { + notes.push(format!( + "The periodic-company trace now also carries a compact save-side summary of the tagged 0x5dc1 placed-structure profile payload/status pairs already parsed from the 0x36b1 triplet seam; dominant current pair is {} / {} x{}, dominant adjacent payload delta is {:?}, dominant post-secondary byte is {:?}, dominant fixed-policy trailing word is {:?}, and dominant pre-footer padding len is {:?}.", + payload_summaries[0].profile_payload_dword_hex, + payload_summaries[0].profile_status_kind, + payload_summaries[0].count, + payload_delta_summaries.first().map(|entry| entry.delta_hex.as_str()), + companion_byte_summaries + .first() + .map(|entry| entry.companion_byte_hex.as_str()), + policy_trailing_word_summaries + .first() + .map(|entry| entry.policy_trailing_word_hex.as_str()), + footer_padding_summaries.first().map(|entry| entry.padding_len) + )); + } + if !nonzero_name_pair_summaries.is_empty() { + let dominant_nonzero_companion = &nonzero_name_pair_summaries[0]; + notes.push(format!( + "The nonzero 0x5dc1 post-secondary byte residue is now narrower too: the trace exposes exact saved name pairs for nonzero-byte rows, and the current leading pair is {} / {} with byte {} x{}. The same atlas-backed owner strip already restores the repeated primary and secondary payload strings into [owner+0x23e] and [owner+0x242], so this byte should now be treated as a separate discriminator after the secondary string, not as the [owner+0x242] field itself. On grounded saves the exposed nonzero sample set is dominated by industry-like names rather than stations, maintenance, or residential rows, which makes the byte a stronger acquisition-side discriminator candidate than the monotone payload dword.", + dominant_nonzero_companion.primary_name, + dominant_nonzero_companion.secondary_name, + dominant_nonzero_companion.companion_byte_hex, + dominant_nonzero_companion.count + )); + } + if !building_family_overlap_summaries.is_empty() { + let dominant_overlap = &building_family_overlap_summaries[0]; + notes.push(format!( + "The same nonzero 0x5dc1 name-pair residue now bridges directly into the recovered stock Tier-2 building family too: the leading overlapping saved pair is {} / {} with byte {} x{}, and it matches the checked-in nonzero stock `.bty` header family (`dword_0xbb = 0x000001f4`) rather than the zero-valued station or maintenance/service families. That makes the acquisition-side discriminator and the Tier-2 banked-clone frontier a shared narrower industrial/commercial subset question, not two separate broad mysteries.", + dominant_overlap.primary_name, + dominant_overlap.secondary_name, + dominant_overlap.companion_byte_hex, + dominant_overlap.count + )); + } + if !building_family_residue_summaries.is_empty() { + let dominant_residue = &building_family_residue_summaries[0]; + notes.push(format!( + "The same trace now keeps the explicit non-overlap residue visible too: the leading saved pair still outside that recovered nonzero stock `.bty` family is {} / {} with byte {} x{}. That keeps the current Tier-2/source-selection queue honest: part of the peer-site nonzero residue now maps cleanly onto the recovered 0x000001f4 industrial/commercial family, but the remaining residue still needs a broader stock-header or later chooser-side explanation rather than being silently folded into the overlap set.", + dominant_residue.primary_name, + dominant_residue.secondary_name, + dominant_residue.companion_byte_hex, + dominant_residue.count + )); + } + notes.push( + "Direct disassembly now also separates the narrower peer-class gate from that payload residue: 0x0047fd50 resolves the linked peer through [site+0x04], reads candidate class byte [candidate+0x8c], and returns true only for values 0/1/2 while rejecting 3/4 and above. That means the newly isolated post-secondary byte is not the already-grounded station-or-transit class gate itself; it remains a separate saved discriminator above the restored name-pair payload.".to_string(), + ); + if let (Some(dominant_companion), Some(dominant_trailing_word)) = ( + companion_byte_summaries.first(), + policy_trailing_word_summaries.first(), + ) { + notes.push(format!( + "The same focused 0x36b1 triplet probe now also keeps the fixed-policy trailing word narrow at {} x{} while the profile side stays dominated by companion byte {} and payload/status pair {} / {}. Together that keeps the checked-in triplet seam looking like local structure/profile state rather than the missing acquisition owner-company lane [site+0x276] or cached tri-lane [site+0x310/+0x338/+0x360].", + dominant_trailing_word.policy_trailing_word_hex, + dominant_trailing_word.count, + dominant_companion.companion_byte_hex, + payload_summaries[0].profile_payload_dword_hex, + payload_summaries[0].profile_status_kind, + )); + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/peer_site.rs b/crates/rrt-runtime/src/inspect/smp/services/company/peer_site.rs new file mode 100644 index 0000000..88d0215 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/peer_site.rs @@ -0,0 +1,140 @@ +use crate::inspect::smp::services::*; + +pub(super) struct PeerSiteTraceInputs { + pub(super) peer_site_selector_candidate_owner_strip: String, + pub(super) peer_site_selector_candidate_persisted_tag_hex: String, + pub(super) peer_site_selector_candidate_selector_lane: String, + pub(super) peer_site_selector_candidate_secondary_payload_lane: String, + pub(super) peer_site_selector_candidate_post_secondary_byte_status: String, + pub(super) peer_site_selector_candidate_class_identity_status: String, + pub(super) peer_site_selector_candidate_helper_linkage: Vec, + pub(super) peer_site_selector_candidate_saved_payload_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_payload_delta_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_footer_padding_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_companion_byte_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_policy_trailing_word_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries: + Vec, + pub(super) peer_site_persisted_selector_bundle_fields: Vec, + pub(super) peer_site_rebuilt_transient_followon_fields: Vec, + pub(super) peer_site_shellless_minimum_persisted_identity_status: String, + pub(super) peer_site_shellless_minimum_persisted_identity_inputs: Vec, + pub(super) peer_site_restore_input_fields: Vec, + pub(super) peer_site_runtime_input_fields: Vec, + pub(super) peer_site_runtime_reconstruction_status: String, + pub(super) peer_site_runtime_reconstruction_steps: Vec, +} + +pub(super) fn build_peer_site_trace_inputs( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> PeerSiteTraceInputs { + let peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries = + summarize_peer_site_selector_candidate_saved_nonzero_companion_name_pairs(analysis); + + PeerSiteTraceInputs { + peer_site_selector_candidate_owner_strip: + "0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70" + .to_string(), + peer_site_selector_candidate_persisted_tag_hex: "0x5dc1".to_string(), + peer_site_selector_candidate_selector_lane: "[owner+0x23e]".to_string(), + peer_site_selector_candidate_secondary_payload_lane: "[owner+0x242]".to_string(), + peer_site_selector_candidate_post_secondary_byte_status: + "unresolved 0x5dc1 post-secondary discriminator byte after the repeated secondary payload string" + .to_string(), + peer_site_selector_candidate_class_identity_status: + "grounded_direct_local_helper_strip".to_string(), + peer_site_selector_candidate_helper_linkage: vec![ + "0x0040ceab -> 0x0045c150".to_string(), + "0x0040d1a1 -> 0x0045c310".to_string(), + "0x0040cd70 seeds [site+0x3cc/+0x3d0] from 0x62b2fc / 0x62b268".to_string(), + "0x0040d1e1 -> 0x0045c3c0 consumes the same owner family's [site+0x246] child lane" + .to_string(), + ], + peer_site_selector_candidate_saved_payload_summaries: + summarize_peer_site_selector_candidate_saved_payloads(analysis), + peer_site_selector_candidate_saved_payload_delta_summaries: + summarize_peer_site_selector_candidate_saved_payload_deltas(analysis), + peer_site_selector_candidate_saved_footer_padding_summaries: + summarize_peer_site_selector_candidate_saved_footer_padding(analysis), + peer_site_selector_candidate_saved_companion_byte_summaries: + summarize_peer_site_selector_candidate_saved_companion_bytes(analysis), + peer_site_selector_candidate_saved_policy_trailing_word_summaries: + summarize_peer_site_selector_candidate_saved_policy_trailing_words(analysis), + peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries: + summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_overlaps( + &peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries, + ), + peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries: + summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_residues( + &peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries, + ), + peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries, + peer_site_persisted_selector_bundle_fields: vec![ + "0x5dc1 payload lane [owner+0x23e] restored by 0x0045c150 and later fed into 0x0045c36e" + .to_string(), + "0x5dc1 payload lane [owner+0x242] restored by 0x0045c150 as the repeated secondary payload string" + .to_string(), + "0x5dc1 post-secondary one-byte residue after the repeated secondary payload string" + .to_string(), + "broader saved child/runtime selector bundle [owner+0x246/+0x24e/+0x252] emitted by 0x0040c980 -> 0x0045b560" + .to_string(), + ], + peer_site_rebuilt_transient_followon_fields: vec![ + "[owner+0x246] primary transient handle rebuilt from payload strings by 0x0045c310" + .to_string(), + "[owner+0x24a] ambient transient rebuilt through 0x0045b210 after 0x0045b5f0 refreshes the current derived position scalar" + .to_string(), + "transient roots [owner+0x24e/+0x256/+0x25a/+0x25e] cleared by 0x0045c150 before 0x0045b5f0 / 0x0045b6f0 rebuild follow-on variant state" + .to_string(), + "larger animation/light/random-sound variant family rooted in [owner+0x23e] rebuilt through 0x0045b6f0 / 0x0045b760 / 0x0045baf0" + .to_string(), + ], + peer_site_shellless_minimum_persisted_identity_status: + "name_pair_and_post_secondary_byte_minimum_identity_subset_child_runtime_bundle_rebuild_followon" + .to_string(), + peer_site_shellless_minimum_persisted_identity_inputs: vec![ + "[site+0x3cc] cached source placed-structure id".to_string(), + "[site+0x3d0] cached companion candidate/profile id".to_string(), + "0x5dc1 payload lane [owner+0x23e]".to_string(), + "0x5dc1 payload lane [owner+0x242]".to_string(), + "0x5dc1 post-secondary one-byte residue".to_string(), + ], + peer_site_restore_input_fields: vec![ + "[site+0x3cc] saved placed-structure id feeding 0x62b2fc".to_string(), + "[site+0x3d0] saved companion-region id from [placed+0x173] feeding 0x62b268" + .to_string(), + "0x5dc1 payload lane [owner+0x23e] feeding 0x0045c36e selector arg 1".to_string(), + "0x5dc1 payload lane [owner+0x242] carrying the restored secondary payload string" + .to_string(), + "0x5dc1 post-secondary one-byte residue after the repeated secondary payload string" + .to_string(), + ], + peer_site_runtime_input_fields: vec![ + "[site+0x04] live backing-record selector consumed by 0x0047efe0 / 0x0047fd50" + .to_string(), + "[site+0x2a8] linked peer-site id consumed by 0x0040d1f0".to_string(), + "[peer+0x08] route-entry anchor id consumed by 0x0047dda0".to_string(), + ], + peer_site_runtime_reconstruction_status: + "restore_subset_and_bring_up_reconstruct_runtime_subset".to_string(), + peer_site_runtime_reconstruction_steps: vec![ + "[site+0x04] restored from 0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70 -> 0x0052edf0" + .to_string(), + "[site+0x2a8] rewritten by 0x0040f6d0 after 0x00481390 returns the linked peer id" + .to_string(), + "[peer+0x08] refreshed during 0x00444690 -> 0x004133b0 -> 0x0040ee10 -> 0x0040edf6 -> 0x00480710" + .to_string(), + "world-cell owner and linked-site chains [cell+0xd4]/[cell+0xd6] republished during 0x00480710 via 0x0042bbf0/0x0042bbb0 and 0x0042c9f0/0x0042c9a0" + .to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/status/linked_transit_fields.rs b/crates/rrt-runtime/src/inspect/smp/services/company/status/linked_transit_fields.rs new file mode 100644 index 0000000..ff34de7 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/status/linked_transit_fields.rs @@ -0,0 +1,35 @@ +use super::super::notes::LinkedTransitTraceInputs; + +pub(super) struct LinkedTransitReportFields { + pub(super) atlas_candidate_consumers: Vec, + pub(super) known_bridge_helpers: Vec, + pub(super) next_owner_questions: Vec, + pub(super) linked_transit_shellless_readiness_status: String, + pub(super) linked_transit_minimum_persisted_identity_inputs: Vec, + pub(super) linked_transit_live_rebuilt_cache_lanes: Vec, + pub(super) linked_transit_runtime_backed_input_families: Vec, + pub(super) linked_transit_remaining_owner_gaps: Vec, +} + +pub(super) fn build_linked_transit_report_fields( + inputs: &LinkedTransitTraceInputs, +) -> LinkedTransitReportFields { + LinkedTransitReportFields { + atlas_candidate_consumers: inputs.atlas_candidate_consumers.clone(), + known_bridge_helpers: inputs.known_bridge_helpers.clone(), + next_owner_questions: inputs.next_owner_questions.clone(), + linked_transit_shellless_readiness_status: inputs + .linked_transit_shellless_readiness_status + .clone(), + linked_transit_minimum_persisted_identity_inputs: inputs + .linked_transit_minimum_persisted_identity_inputs + .clone(), + linked_transit_live_rebuilt_cache_lanes: inputs + .linked_transit_live_rebuilt_cache_lanes + .clone(), + linked_transit_runtime_backed_input_families: inputs + .linked_transit_runtime_backed_input_families + .clone(), + linked_transit_remaining_owner_gaps: inputs.linked_transit_remaining_owner_gaps.clone(), + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/status/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/company/status/mod.rs new file mode 100644 index 0000000..2fa6de9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/status/mod.rs @@ -0,0 +1,6 @@ +mod linked_transit_fields; +mod near_city_fields; +mod peer_site_fields; +mod report; + +pub(in crate::inspect::smp) use report::build_periodic_company_service_trace_report; diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/status/near_city_fields.rs b/crates/rrt-runtime/src/inspect/smp/services/company/status/near_city_fields.rs new file mode 100644 index 0000000..18a89a4 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/status/near_city_fields.rs @@ -0,0 +1,105 @@ +use super::super::near_city_acquisition::NearCityAcquisitionTraceInputs; +use crate::inspect::smp::services::*; + +pub(super) struct NearCityReportFields { + pub(super) near_city_acquisition_region_input_fields: Vec, + pub(super) near_city_acquisition_peer_input_fields: Vec, + pub(super) near_city_acquisition_company_input_fields: Vec, + pub(super) near_city_acquisition_shellless_readiness_status: String, + pub(super) near_city_acquisition_runtime_backed_input_families: Vec, + pub(super) near_city_acquisition_site_owner_company_projection_status: String, + pub(super) near_city_acquisition_site_self_id_projection_status: String, + pub(super) near_city_acquisition_site_cached_tri_lane_projection_status: String, + pub(super) near_city_acquisition_tri_lane_live_service_status: String, + pub(super) near_city_acquisition_candidate_subtype_projection_status: String, + pub(super) near_city_acquisition_backing_record_projection_status: String, + pub(super) near_city_acquisition_nontransport_persisted_source_status: String, + pub(super) near_city_acquisition_nontransport_persisted_source_candidates: Vec, + pub(super) near_city_acquisition_tri_lane_save_shape_family_status: String, + pub(super) near_city_acquisition_tri_lane_save_shape_family_candidates: + Vec, + pub(super) near_city_acquisition_tri_lane_live_owner_families: Vec, + pub(super) near_city_acquisition_tri_lane_candidate_gate_fields: Vec, + pub(super) near_city_acquisition_tri_lane_runtime_writer_roles: Vec, + pub(super) near_city_acquisition_tri_lane_direct_caller_families: Vec, + pub(super) near_city_acquisition_tri_lane_formula_input_lanes: Vec, + pub(super) near_city_acquisition_projection_hypotheses: Vec, + pub(super) near_city_acquisition_remaining_owner_gaps: Vec, + pub(super) near_city_acquisition_region_lane_statuses: Vec, +} + +pub(super) fn build_near_city_report_fields( + inputs: &NearCityAcquisitionTraceInputs, +) -> NearCityReportFields { + NearCityReportFields { + near_city_acquisition_region_input_fields: inputs + .near_city_acquisition_region_input_fields + .clone(), + near_city_acquisition_peer_input_fields: inputs + .near_city_acquisition_peer_input_fields + .clone(), + near_city_acquisition_company_input_fields: inputs + .near_city_acquisition_company_input_fields + .clone(), + near_city_acquisition_shellless_readiness_status: inputs + .near_city_acquisition_shellless_readiness_status + .clone(), + near_city_acquisition_runtime_backed_input_families: inputs + .near_city_acquisition_runtime_backed_input_families + .clone(), + near_city_acquisition_site_owner_company_projection_status: inputs + .near_city_acquisition_site_owner_company_projection_status + .clone(), + near_city_acquisition_site_self_id_projection_status: inputs + .near_city_acquisition_site_self_id_projection_status + .clone(), + near_city_acquisition_site_cached_tri_lane_projection_status: inputs + .near_city_acquisition_site_cached_tri_lane_projection_status + .clone(), + near_city_acquisition_tri_lane_live_service_status: inputs + .near_city_acquisition_tri_lane_live_service_status + .clone(), + near_city_acquisition_candidate_subtype_projection_status: inputs + .near_city_acquisition_candidate_subtype_projection_status + .clone(), + near_city_acquisition_backing_record_projection_status: inputs + .near_city_acquisition_backing_record_projection_status + .clone(), + near_city_acquisition_nontransport_persisted_source_status: inputs + .near_city_acquisition_nontransport_persisted_source_status + .clone(), + near_city_acquisition_nontransport_persisted_source_candidates: inputs + .near_city_acquisition_nontransport_persisted_source_candidates + .clone(), + near_city_acquisition_tri_lane_save_shape_family_status: inputs + .near_city_acquisition_tri_lane_save_shape_family_status + .clone(), + near_city_acquisition_tri_lane_save_shape_family_candidates: inputs + .near_city_acquisition_tri_lane_save_shape_family_candidates + .clone(), + near_city_acquisition_tri_lane_live_owner_families: inputs + .near_city_acquisition_tri_lane_live_owner_families + .clone(), + near_city_acquisition_tri_lane_candidate_gate_fields: inputs + .near_city_acquisition_tri_lane_candidate_gate_fields + .clone(), + near_city_acquisition_tri_lane_runtime_writer_roles: inputs + .near_city_acquisition_tri_lane_runtime_writer_roles + .clone(), + near_city_acquisition_tri_lane_direct_caller_families: inputs + .near_city_acquisition_tri_lane_direct_caller_families + .clone(), + near_city_acquisition_tri_lane_formula_input_lanes: inputs + .near_city_acquisition_tri_lane_formula_input_lanes + .clone(), + near_city_acquisition_projection_hypotheses: inputs + .near_city_acquisition_projection_hypotheses + .clone(), + near_city_acquisition_remaining_owner_gaps: inputs + .near_city_acquisition_remaining_owner_gaps + .clone(), + near_city_acquisition_region_lane_statuses: inputs + .near_city_acquisition_region_lane_statuses + .clone(), + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/status/peer_site_fields.rs b/crates/rrt-runtime/src/inspect/smp/services/company/status/peer_site_fields.rs new file mode 100644 index 0000000..7a841a4 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/status/peer_site_fields.rs @@ -0,0 +1,108 @@ +use super::super::peer_site::PeerSiteTraceInputs; +use crate::inspect::smp::services::*; + +pub(super) struct PeerSiteReportFields { + pub(super) peer_site_selector_candidate_owner_strip: String, + pub(super) peer_site_selector_candidate_persisted_tag_hex: String, + pub(super) peer_site_selector_candidate_selector_lane: String, + pub(super) peer_site_selector_candidate_secondary_payload_lane: String, + pub(super) peer_site_selector_candidate_post_secondary_byte_status: String, + pub(super) peer_site_selector_candidate_class_identity_status: String, + pub(super) peer_site_selector_candidate_helper_linkage: Vec, + pub(super) peer_site_selector_candidate_saved_payload_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_payload_delta_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_footer_padding_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_companion_byte_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_policy_trailing_word_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries: + Vec, + pub(super) peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries: + Vec, + pub(super) peer_site_persisted_selector_bundle_fields: Vec, + pub(super) peer_site_rebuilt_transient_followon_fields: Vec, + pub(super) peer_site_shellless_minimum_persisted_identity_status: String, + pub(super) peer_site_shellless_minimum_persisted_identity_inputs: Vec, + pub(super) peer_site_restore_input_fields: Vec, + pub(super) peer_site_runtime_input_fields: Vec, + pub(super) peer_site_runtime_reconstruction_status: String, + pub(super) peer_site_runtime_reconstruction_steps: Vec, +} + +pub(super) fn build_peer_site_report_fields(inputs: &PeerSiteTraceInputs) -> PeerSiteReportFields { + PeerSiteReportFields { + peer_site_selector_candidate_owner_strip: inputs + .peer_site_selector_candidate_owner_strip + .clone(), + peer_site_selector_candidate_persisted_tag_hex: inputs + .peer_site_selector_candidate_persisted_tag_hex + .clone(), + peer_site_selector_candidate_selector_lane: inputs + .peer_site_selector_candidate_selector_lane + .clone(), + peer_site_selector_candidate_secondary_payload_lane: inputs + .peer_site_selector_candidate_secondary_payload_lane + .clone(), + peer_site_selector_candidate_post_secondary_byte_status: inputs + .peer_site_selector_candidate_post_secondary_byte_status + .clone(), + peer_site_selector_candidate_class_identity_status: inputs + .peer_site_selector_candidate_class_identity_status + .clone(), + peer_site_selector_candidate_helper_linkage: inputs + .peer_site_selector_candidate_helper_linkage + .clone(), + peer_site_selector_candidate_saved_payload_summaries: inputs + .peer_site_selector_candidate_saved_payload_summaries + .clone(), + peer_site_selector_candidate_saved_payload_delta_summaries: inputs + .peer_site_selector_candidate_saved_payload_delta_summaries + .clone(), + peer_site_selector_candidate_saved_footer_padding_summaries: inputs + .peer_site_selector_candidate_saved_footer_padding_summaries + .clone(), + peer_site_selector_candidate_saved_companion_byte_summaries: inputs + .peer_site_selector_candidate_saved_companion_byte_summaries + .clone(), + peer_site_selector_candidate_saved_policy_trailing_word_summaries: inputs + .peer_site_selector_candidate_saved_policy_trailing_word_summaries + .clone(), + peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries: inputs + .peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries + .clone(), + peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries: + inputs + .peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries + .clone(), + peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries: + inputs + .peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries + .clone(), + peer_site_persisted_selector_bundle_fields: inputs + .peer_site_persisted_selector_bundle_fields + .clone(), + peer_site_rebuilt_transient_followon_fields: inputs + .peer_site_rebuilt_transient_followon_fields + .clone(), + peer_site_shellless_minimum_persisted_identity_status: inputs + .peer_site_shellless_minimum_persisted_identity_status + .clone(), + peer_site_shellless_minimum_persisted_identity_inputs: inputs + .peer_site_shellless_minimum_persisted_identity_inputs + .clone(), + peer_site_restore_input_fields: inputs.peer_site_restore_input_fields.clone(), + peer_site_runtime_input_fields: inputs.peer_site_runtime_input_fields.clone(), + peer_site_runtime_reconstruction_status: inputs + .peer_site_runtime_reconstruction_status + .clone(), + peer_site_runtime_reconstruction_steps: inputs + .peer_site_runtime_reconstruction_steps + .clone(), + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/company/status/report.rs b/crates/rrt-runtime/src/inspect/smp/services/company/status/report.rs new file mode 100644 index 0000000..46003ce --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/company/status/report.rs @@ -0,0 +1,151 @@ +use super::super::entries::build_periodic_company_service_entries; +use super::super::near_city_acquisition::build_near_city_acquisition_trace_inputs; +use super::super::notes::{ + build_linked_transit_trace_inputs, build_periodic_company_service_notes, +}; +use super::super::peer_site::build_peer_site_trace_inputs; +use super::linked_transit_fields::build_linked_transit_report_fields; +use super::near_city_fields::build_near_city_report_fields; +use super::peer_site_fields::build_peer_site_report_fields; +use crate::inspect::smp::services::*; + +pub(in crate::inspect::smp) fn build_periodic_company_service_trace_report( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> SmpPeriodicCompanyServiceTraceReport { + let profile_family = analysis.profile_family.clone(); + let selected_company_id = analysis.selected_company_id; + let region_record_body_present = analysis.region_record_triplets.is_some(); + let placed_structure_record_body_present = analysis.placed_structure_record_triplets.is_some(); + let infrastructure_asset_side_buffer_present = + analysis.placed_structure_dynamic_side_buffer.is_some(); + let world_issue_37_present = analysis.world_issue_37.is_some(); + let world_finance_neighborhood_present = analysis.world_finance_neighborhood.is_some(); + + let peer_site = build_peer_site_trace_inputs(analysis); + let near_city = build_near_city_acquisition_trace_inputs(analysis); + let linked_transit = build_linked_transit_trace_inputs(); + + let peer_site_fields = build_peer_site_report_fields(&peer_site); + let near_city_fields = build_near_city_report_fields(&near_city); + let linked_transit_fields = build_linked_transit_report_fields(&linked_transit); + + let companies = build_periodic_company_service_entries(analysis); + let notes = build_periodic_company_service_notes(&peer_site, &near_city); + + SmpPeriodicCompanyServiceTraceReport { + profile_family, + selected_company_id, + world_issue_37_present, + world_finance_neighborhood_present, + region_record_body_present, + placed_structure_record_body_present, + infrastructure_asset_side_buffer_present, + peer_site_selector_candidate_owner_strip: peer_site_fields + .peer_site_selector_candidate_owner_strip, + peer_site_selector_candidate_persisted_tag_hex: peer_site_fields + .peer_site_selector_candidate_persisted_tag_hex, + peer_site_selector_candidate_selector_lane: peer_site_fields + .peer_site_selector_candidate_selector_lane, + peer_site_selector_candidate_secondary_payload_lane: peer_site_fields + .peer_site_selector_candidate_secondary_payload_lane, + peer_site_selector_candidate_post_secondary_byte_status: peer_site_fields + .peer_site_selector_candidate_post_secondary_byte_status, + peer_site_selector_candidate_class_identity_status: peer_site_fields + .peer_site_selector_candidate_class_identity_status, + peer_site_selector_candidate_helper_linkage: peer_site_fields + .peer_site_selector_candidate_helper_linkage, + peer_site_selector_candidate_saved_payload_summaries: peer_site_fields + .peer_site_selector_candidate_saved_payload_summaries, + peer_site_selector_candidate_saved_payload_delta_summaries: peer_site_fields + .peer_site_selector_candidate_saved_payload_delta_summaries, + peer_site_selector_candidate_saved_footer_padding_summaries: peer_site_fields + .peer_site_selector_candidate_saved_footer_padding_summaries, + peer_site_selector_candidate_saved_companion_byte_summaries: peer_site_fields + .peer_site_selector_candidate_saved_companion_byte_summaries, + peer_site_selector_candidate_saved_policy_trailing_word_summaries: peer_site_fields + .peer_site_selector_candidate_saved_policy_trailing_word_summaries, + peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries: + peer_site_fields.peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries, + peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries: + peer_site_fields + .peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries, + peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries: + peer_site_fields + .peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries, + peer_site_persisted_selector_bundle_fields: peer_site_fields + .peer_site_persisted_selector_bundle_fields, + peer_site_rebuilt_transient_followon_fields: peer_site_fields + .peer_site_rebuilt_transient_followon_fields, + peer_site_shellless_minimum_persisted_identity_status: peer_site_fields + .peer_site_shellless_minimum_persisted_identity_status, + peer_site_shellless_minimum_persisted_identity_inputs: peer_site_fields + .peer_site_shellless_minimum_persisted_identity_inputs, + peer_site_restore_input_fields: peer_site_fields.peer_site_restore_input_fields, + peer_site_runtime_input_fields: peer_site_fields.peer_site_runtime_input_fields, + peer_site_runtime_reconstruction_status: peer_site_fields + .peer_site_runtime_reconstruction_status, + peer_site_runtime_reconstruction_steps: peer_site_fields + .peer_site_runtime_reconstruction_steps, + near_city_acquisition_region_input_fields: near_city_fields + .near_city_acquisition_region_input_fields, + near_city_acquisition_peer_input_fields: near_city_fields + .near_city_acquisition_peer_input_fields, + near_city_acquisition_company_input_fields: near_city_fields + .near_city_acquisition_company_input_fields, + near_city_acquisition_shellless_readiness_status: near_city_fields + .near_city_acquisition_shellless_readiness_status, + near_city_acquisition_runtime_backed_input_families: near_city_fields + .near_city_acquisition_runtime_backed_input_families, + near_city_acquisition_site_owner_company_projection_status: near_city_fields + .near_city_acquisition_site_owner_company_projection_status, + near_city_acquisition_site_self_id_projection_status: near_city_fields + .near_city_acquisition_site_self_id_projection_status, + near_city_acquisition_site_cached_tri_lane_projection_status: near_city_fields + .near_city_acquisition_site_cached_tri_lane_projection_status, + near_city_acquisition_tri_lane_live_service_status: near_city_fields + .near_city_acquisition_tri_lane_live_service_status, + near_city_acquisition_candidate_subtype_projection_status: near_city_fields + .near_city_acquisition_candidate_subtype_projection_status, + near_city_acquisition_backing_record_projection_status: near_city_fields + .near_city_acquisition_backing_record_projection_status, + near_city_acquisition_nontransport_persisted_source_status: near_city_fields + .near_city_acquisition_nontransport_persisted_source_status, + near_city_acquisition_nontransport_persisted_source_candidates: near_city_fields + .near_city_acquisition_nontransport_persisted_source_candidates, + near_city_acquisition_tri_lane_save_shape_family_status: near_city_fields + .near_city_acquisition_tri_lane_save_shape_family_status, + near_city_acquisition_tri_lane_save_shape_family_candidates: near_city_fields + .near_city_acquisition_tri_lane_save_shape_family_candidates, + near_city_acquisition_tri_lane_live_owner_families: near_city_fields + .near_city_acquisition_tri_lane_live_owner_families, + near_city_acquisition_tri_lane_candidate_gate_fields: near_city_fields + .near_city_acquisition_tri_lane_candidate_gate_fields, + near_city_acquisition_tri_lane_runtime_writer_roles: near_city_fields + .near_city_acquisition_tri_lane_runtime_writer_roles, + near_city_acquisition_tri_lane_direct_caller_families: near_city_fields + .near_city_acquisition_tri_lane_direct_caller_families, + near_city_acquisition_tri_lane_formula_input_lanes: near_city_fields + .near_city_acquisition_tri_lane_formula_input_lanes, + near_city_acquisition_projection_hypotheses: near_city_fields + .near_city_acquisition_projection_hypotheses, + near_city_acquisition_remaining_owner_gaps: near_city_fields + .near_city_acquisition_remaining_owner_gaps, + near_city_acquisition_region_lane_statuses: near_city_fields + .near_city_acquisition_region_lane_statuses, + atlas_candidate_consumers: linked_transit_fields.atlas_candidate_consumers, + known_bridge_helpers: linked_transit_fields.known_bridge_helpers, + next_owner_questions: linked_transit_fields.next_owner_questions, + linked_transit_shellless_readiness_status: linked_transit_fields + .linked_transit_shellless_readiness_status, + linked_transit_minimum_persisted_identity_inputs: linked_transit_fields + .linked_transit_minimum_persisted_identity_inputs, + linked_transit_live_rebuilt_cache_lanes: linked_transit_fields + .linked_transit_live_rebuilt_cache_lanes, + linked_transit_runtime_backed_input_families: linked_transit_fields + .linked_transit_runtime_backed_input_families, + linked_transit_remaining_owner_gaps: linked_transit_fields + .linked_transit_remaining_owner_gaps, + companies, + notes, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/atlas.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/atlas.rs new file mode 100644 index 0000000..13498fe --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/atlas.rs @@ -0,0 +1,69 @@ +pub(super) struct InfrastructureAtlasInputs { + pub(super) atlas_candidate_consumers: Vec, + pub(super) known_owner_bridge_fields: Vec, + pub(super) known_bridge_helpers: Vec, + pub(super) next_owner_questions: Vec, +} + +pub(super) fn build_infrastructure_atlas_inputs() -> InfrastructureAtlasInputs { + let atlas_candidate_consumers = vec![ + "0x00491c60 infrastructure tagged side-buffer serializer owner".to_string(), + "0x00493be0 infrastructure tagged side-buffer collection load owner".to_string(), + "0x004a2c80 infrastructure composition chooser owner (DT family)".to_string(), + "0x004a34e0 infrastructure composition chooser sibling (ST family)".to_string(), + "0x0048a1e0 infrastructure child attach helper".to_string(), + "0x0048dcf0 infrastructure tagged child-stream restore outer owner".to_string(), + "0x0048dd50 infrastructure child rebuild loop".to_string(), + "0x00490a3c infrastructure payload attach helper".to_string(), + "0x004559d0 infrastructure tagged string-triplet serializer".to_string(), + "0x00455870 infrastructure tagged string-triplet load companion".to_string(), + "0x00455930 infrastructure scalar-triplet serializer sibling".to_string(), + "0x00448a70 / 0x00493660 / 0x0048b660 route and world follow-on family".to_string(), + "0x004133b0 placed-structure local-runtime refresh outer owner".to_string(), + ]; + let known_owner_bridge_fields = vec![ + "outer stream prelude [u16 child count, optional saved primary-child byte]".to_string(), + "[this+0x248] cached primary-child slot".to_string(), + "[this+0x206/+0x20a/+0x20e] route-entry resolver fields".to_string(), + "[this+0x1e2/+0x1e6/+0x1ea] published anchor triplet".to_string(), + "[this+0x4b/+0x4f/+0x53] companion local triplet lane".to_string(), + "child list [this+0x75] under the Infrastructure owner".to_string(), + "non-direct live-entry directory [collection+0x3c] with 12-byte rows (payload pointer, previous live id, next live id)".to_string(), + ]; + let known_bridge_helpers = vec![ + "0x00493be0 tagged 0x38a5/0x38a6/0x38a7 collection load owner".to_string(), + "0x00491c60 tagged 0x38a5/0x38a6/0x38a7 collection serializer owner".to_string(), + "0x004a2c80/0x004a34e0 paired upstream infrastructure composition choosers with decoded DT/ST table families plus BallastCap/Overpass literals".to_string(), + "0x0048a340 infrastructure chooser selector setter for [0x226]/[0x219]/[0x251]/bit 0x20".to_string(), + "0x0048a6c0 per-entry serializer for outer child-count / primary-child prelude and child payload callbacks".to_string(), + "0x00490960 infrastructure child constructor / selector propagator".to_string(), + "0x00455a40 raw vtable slot +0x44 dispatch wrapper".to_string(), + "0x00455a50 raw vtable slot +0x40 dispatch wrapper with global bridge reset".to_string(), + "0x004559d0 tagged child payload serializer for 0x55f1/0x55f2/0x55f3".to_string(), + "0x00490200 infrastructure seeded-lane route/link comparator".to_string(), + "0x00518140 indexed_collection_resolve_live_entry_payload_pointer_by_live_id".to_string(), + "0x005181f0 indexed_collection_unlink_non_direct_live_entry".to_string(), + "0x00518260 indexed_collection_link_non_direct_live_entry".to_string(), + "0x00518380 indexed_collection_find_nth_live_entry_id".to_string(), + "0x00518680 indexed_collection_load_header_bitset_and_non_direct_tables".to_string(), + "0x005395d0 shared child-attach list owner".to_string(), + "0x00539530 shared position-lane seed helper".to_string(), + "0x0053a5b0 shared third position-lane seed helper".to_string(), + "0x0052e8b0 runtime_object_publish_companion_triplet_lane_4b_4f_53".to_string(), + "0x00530720 runtime_object_publish_anchor_triplet_and_optionally_rebind_world_cell_handle" + .to_string(), + "0x0048e140 / 0x0048e160 / 0x0048e180 route-entry resolver helpers".to_string(), + ]; + let next_owner_questions = vec![ + "With 0x00491c60, 0x0048a6c0, 0x00490960, 0x00455a40, and 0x004559d0 now grounded as the full child-construction and write-side dispatch chain for the `0x38a5/0x38a6/0x38a7` family, how do the remaining compact-prefix regimes subdivide the already-mapped save-side mode families (0x0a BallastCap, 0x0b TrackCap, 0x02 Tunnel, 0x01 Bridge, with 0x03 Overpass only grounded statically) before they surface in the seeded lanes [this+0x206/+0x20a/+0x20e], the slot +0x4c serializer, and the trailing 0x52ec50 footer path?".to_string(), + "Inside the grounded overpass/ballast branch ([this+0x226]==3) of the paired chooser siblings, when do the fixed BallastCap and Overpass literals (0x5cb138/0x5cb150 and 0x5cb168/0x5cb180) fire, and does the pure BallastCap class 0x0055/0x00 stay a boundary artifact or become a real outer prelude consumed by 0x0048dcf0?".to_string(), + "Which 0x38a5 embedded name-pair groups survive into the per-child vtable +0x40 payload callbacks dispatched through 0x00455a50?".to_string(), + "After the direct route-entry bridge helpers over [this+0x206/+0x20a/+0x20e] are grounded, which later route/local-runtime owner above 0x00448a70/0x00493660/0x0048b660 still depends on the remaining mixed exact classes once [this+0x248] is demoted to child-list cache/cleanup state?".to_string(), + ]; + InfrastructureAtlasInputs { + atlas_candidate_consumers, + known_owner_bridge_fields, + known_bridge_helpers, + next_owner_questions, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/entrypoints.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/entrypoints.rs new file mode 100644 index 0000000..7596d45 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/entrypoints.rs @@ -0,0 +1,13 @@ +use std::path::Path; + +use crate::inspect::smp::services::{ + SmpInfrastructureAssetTraceReport, build_infrastructure_asset_trace_report, +}; +use crate::inspect::smp::world::inspect_save_company_and_chairman_analysis_file; + +pub fn inspect_save_infrastructure_asset_trace_file( + path: &Path, +) -> Result> { + let analysis = inspect_save_company_and_chairman_analysis_file(path)?; + Ok(build_infrastructure_asset_trace_report(&analysis)) +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/family_scan.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/family_scan.rs new file mode 100644 index 0000000..36c2c64 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/family_scan.rs @@ -0,0 +1,43 @@ +use crate::inspect::smp::services::*; + +pub(super) struct InfrastructureFamilyScan { + pub(super) bridge_like_name_pair_count: usize, + pub(super) tunnel_like_name_pair_count: usize, + pub(super) track_cap_like_name_pair_count: usize, + pub(super) st_only_name_pair_corpus: bool, +} + +pub(super) fn analyze_infrastructure_name_pairs( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> InfrastructureFamilyScan { + let side_buffer = analysis.placed_structure_dynamic_side_buffer.as_ref(); + let name_pair_summaries = side_buffer + .map(|probe| probe.name_pair_summaries.as_slice()) + .unwrap_or(&[]); + let bridge_like_name_pair_count = name_pair_summaries + .iter() + .filter(|summary| summary.primary_name.contains("Bridge")) + .count(); + let tunnel_like_name_pair_count = name_pair_summaries + .iter() + .filter(|summary| summary.primary_name.contains("Tunnel")) + .count(); + let track_cap_like_name_pair_count = name_pair_summaries + .iter() + .filter(|summary| summary.primary_name.contains("TrackCap")) + .count(); + let st_only_name_pair_corpus = !name_pair_summaries.is_empty() + && name_pair_summaries + .iter() + .all(|summary| summary.primary_name.contains("ST")) + && !name_pair_summaries + .iter() + .any(|summary| summary.primary_name.contains("DT")); + + InfrastructureFamilyScan { + bridge_like_name_pair_count, + tunnel_like_name_pair_count, + track_cap_like_name_pair_count, + st_only_name_pair_corpus, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/attach_rebuild.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/attach_rebuild.rs new file mode 100644 index 0000000..502dd2d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/attach_rebuild.rs @@ -0,0 +1,81 @@ +use super::constructor_families::build_constructor_family_evidence; +use super::live_entry_directory::build_live_entry_directory_evidence; +use super::save_side_corpus::build_save_side_corpus_evidence; +use crate::inspect::smp::services::*; + +pub(super) fn build_attach_rebuild_hypothesis( + analysis: &SmpSaveCompanyChairmanAnalysisReport, + scan: &super::super::super::family_scan::InfrastructureFamilyScan, +) -> SmpServiceConsumerHypothesis { + let side_buffer = analysis.placed_structure_dynamic_side_buffer.as_ref(); + let mut evidence = vec![ + "atlas already bounds these helpers under the literal Infrastructure owner".to_string(), + "the side-buffer corpus is disjoint from the placed-structure triplet corpus, so a separate child/rebuild family is more plausible than a compact alias".to_string(), + "direct disassembly now shows 0x00493be0 opening tag family 0x38a5/0x38a6/0x38a7, reading one shared dword into the owner-local 0x90/0x94 lane, iterating each live collection entry, and dispatching every loaded infrastructure record through 0x0048dcf0 before the later follow-on owners run".to_string(), + "direct disassembly now also shows 0x00491c60 as the serializer sibling of 0x00493be0: it writes tags 0x38a5/0x38a6/0x38a7, serializes the shared owner-local dword from [this+0x90], iterates live entries through 0x00518380/0x00518140, and dispatches each entry to 0x0048a6c0".to_string(), + ]; + evidence.extend(build_constructor_family_evidence()); + evidence.push("objdump on 0x52ec50 now also makes the short footer bytes literal: it serializes one byte from bit 5 of [this+0x20] and one byte from bit 6 of [this+0x20] through 0x531030, so the residual compact-prefix ambiguity still lives in how those footer bits compose with the next-record prelude rather than in the seeded name lanes themselves".to_string()); + evidence.push("direct disassembly now also grounds one concrete consumer strip below those footer bits: 0x00528d90 only admits the child when the explicit caller override is set, the surrounding global override byte [owner+0x3692] is set, or bit 0x20 in [child+0x20] is set; the sibling loop at 0x00529730 only takes the later 0x530280 follow-on when bit 0x40 in [child+0x20] is set".to_string()); + evidence.push("that footer-bit consumer strip is tied to a broader higher-layer owner family now too: the same 0x005295f0..0x005297b7 loop repopulates a candidate cell set through 0x00533ba0, walks candidate child lists through 0x00556ef0/0x00556f00, and honors the same controller mode byte [owner+0x3692] that the checked-in atlas already ties to the world-window presentation dispatcher. So the remaining bit-0x20 question belongs to that nearby-presentation/controller family rather than to a free-floating serializer flag".to_string()); + evidence.push("the neighboring helpers tighten that owner family further: atlas-backed 0x00533ba0 is the nearby-presentation cell-table helper under the layout/presenter strip, direct disassembly shows 0x00548da0 walking list root [layout+0x2593], and direct disassembly of 0x0054bab0 mutates layout slots [layout+0x2637/+0x263b/+0x2643]. That means the 0x005295f0..0x005297b7 footer-bit consumer is sitting in layout/presentation state, not in a simulation-owned infrastructure service".to_string()); + evidence.push("objdump on 0x531030/0x5a464d/0x5a44a8 now also shows the infrastructure writer is not hiding another per-owner transform there: 0x531030 just forwards the caller-supplied pointer and byte count into the generic stream backend, and 0x5a44a8 is the shared chunked stream write path keyed by the stream handle rather than an infrastructure-specific encoder".to_string()); + evidence.push("that caller-matrix split now rules out one easy explanation for the mixed save-side prefixes: the shared 0xff0000ff/0x0001/0xff class cannot come from selector-copy state alone, because its dominant TrackCap rows come from mode-0x0b callers that bypass selector-copy entirely while the tunnel residue comes from mode-0x02 callers that necessarily flow through it".to_string()); + evidence.extend(build_save_side_corpus_evidence(analysis, scan)); + evidence.extend(build_live_entry_directory_evidence()); + evidence.push("direct disassembly now also shows the shared child payload callback 0x00455fc0 opening 0x55f1, parsing three len-prefixed strings through 0x531380, opening 0x55f2, seeding the child through 0x455b70, dispatching slot +0x48, and then opening 0x55f3".to_string()); + evidence.push("direct disassembly now also shows 0x00455b70 storing those three payload strings into [this+0x206/+0x20a/+0x20e], defaulting the second lane through a fixed literal when absent and defaulting the third lane back to the first string when absent".to_string()); + evidence.push("direct disassembly now also shows the post-+0x48 helper pair 0x52ebd0/0x52ec50 loading and serializing two single-byte lanes around the trailing 0x55f3 tag while folding them into bits 0x20 and 0x40 of [this+0x20]".to_string()); + evidence.push("direct disassembly now also shows 0x455870 consuming six 4-byte lanes from the fixed 0x55f2 chunk and forwarding them into 0x530720 then 0x52e8b0, while 0x455930 serializes the same six dword lanes back through 0x531030".to_string()); + evidence.push("direct disassembly now also shows 0x530720 publishing the first fixed-triplet lane into [this+0x1e2/+0x1e6/+0x1ea], while 0x52e8b0 publishes the second fixed-triplet lane into [this+0x4b/+0x4f/+0x53] and sets bit 0x02".to_string()); + evidence.push("direct disassembly now also shows the outer owner at 0x0048dcf0 reading one u16 child count through 0x531150 into the stream prelude, zeroing [this+0x08], and conditionally reading one saved primary-child byte before the per-child callback loop runs".to_string()); + evidence.push( + side_buffer + .and_then(|probe| probe.live_entry_prelude_summary.as_ref()) + .map(|summary| { + format!( + "the widened save-side probe now also decodes {} live-entry payload starts inside the records span; dominant child count={} x{}, dominant saved primary-child byte={} x{}, and {} payloads reach the first 0x55f1 child callback at offset +0x3", + summary.rows_with_payload_pointer_inside_records_span, + summary.dominant_child_count.unwrap_or_default(), + summary.dominant_child_count_count, + summary + .dominant_saved_primary_child_byte_hex + .as_deref() + .unwrap_or("0x00"), + summary.dominant_saved_primary_child_byte_count, + summary.rows_with_first_name_tag_at_offset_3 + ) + }) + .unwrap_or_else(|| { + "no live-entry payload-start summary was available for the outer prelude" + .to_string() + }), + ); + evidence.push("local .rdata at 0x005cfd00 now also proves the infrastructure child table uses the shared tagged callback strip directly: slot +0x40 = 0x455fc0, slot +0x44 = 0x4559d0, slot +0x48 = 0x455870, and slot +0x4c = 0x455930".to_string()); + evidence.push("direct disassembly now shows 0x0048a1e0 cloning the first child triplet bands through 0x52e880/0x52e720, destroying the prior child, seeding a new literal Infrastructure child through 0x455b70 with payload seed 0x5c87a8, attaching through 0x5395d0 or 0x53a5d0, and republishing the two bands through 0x52e8b0/0x530720".to_string()); + evidence.push("direct disassembly now also shows the outer owner at 0x0048dcf0 reading a child count plus optional primary-child ordinal from the tagged stream through 0x531150, zeroing [this+0x08], dispatching each fresh child through 0x455a50 -> vtable slot +0x40, culling ordinals above 5, and restoring cached primary-child slot [this+0x248] from the saved ordinal".to_string()); + evidence.push("the smaller attach primitive 0x00490a3c no longer looks like the semantic fork by itself: it just allocates one literal Infrastructure child, seeds it through 0x455b70 with caller-provided stem input, attaches it through 0x5395d0, seeds position lanes through 0x539530/0x53a5b0, and optionally caches it as primary child".to_string()); + + SmpServiceConsumerHypothesis { + label: "infrastructure child attach/rebuild path".to_string(), + status: if side_buffer.is_some() { + "highest_priority_static_mapping_target".to_string() + } else { + "possible_consumer_family".to_string() + }, + candidate_consumers: vec![ + "0x00493be0 infrastructure tagged side-buffer collection load owner".to_string(), + "0x0048a1e0 infrastructure child attach helper".to_string(), + "0x0048dcf0 infrastructure tagged child-stream restore outer owner".to_string(), + "0x0048dd50 infrastructure child rebuild loop".to_string(), + "0x00490a3c infrastructure payload attach helper".to_string(), + ], + evidence, + blockers: vec![ + "how the remaining mixed exact compact-prefix classes map back onto constructor semantics now that the whole prelude corpus is split directly: 0xff0000ff/0x0002/0xff is pure bridge, 0xff000000/{0x0001,0x0002}/0xff are pure bridge, 0xf3010100/0x0055/0x00 is pure ballast-cap, and 0x0005d368/0x0001/0xff is pure track-cap, leaving only 0xff0000ff/0x0001/0xff and 0x000055f3/0x0001/0xff as the mixed residual classes".to_string(), + "how the payload streams reached through 0x00518380 -> 0x00518140 align with the embedded 0x55f1 name-pair groups and compact-prefix regimes surfaced by the save-side probe".to_string(), + "how the observed 0x55f3-to-next-0x55f1 gaps partition between the two 0x52ebd0 flag bytes and the next-record prelude now that 0x0048a6c0 is grounded as the writer for the outer child-count / primary-child prelude".to_string(), + "which fields written through the grounded 0x00490960 -> 0x004559d0 -> slot +0x4c -> 0x52ec50 chain retain the 0x38a5 embedded name-pair semantics before route/local-runtime follow-ons take over".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/constructor_families.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/constructor_families.rs new file mode 100644 index 0000000..580987f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/constructor_families.rs @@ -0,0 +1,24 @@ +pub(super) fn build_constructor_family_evidence() -> Vec { + vec![ + "direct disassembly now also grounds paired upstream chooser siblings: 0x004a2c80 routes the DT family and 0x004a34e0 routes the ST family, with both repeatedly calling 0x0048a1e0 and branching on child type codes at [this+0x226], selector bytes at [this+0x219]/[this+0x251]/[this+0x252], bit 0x20 in [this+0x24c], and follow-on owners 0x0048a340/0x0048f4c0/0x00490200/0x00490960".to_string(), + "direct disassembly now also shows 0x0048a6c0 serializing the outer per-record prelude directly: it writes the current child count as a u16, writes the saved primary-child ordinal byte derived from [this+0x248], and then serializes each child through 0x00455a40".to_string(), + "local .rdata at 0x005cfd00 now also proves the missing write-side slot directly: for Infrastructure children, 0x00455a40 lands on vtable slot +0x44 = 0x004559d0, alongside +0x40 = 0x00455fc0, +0x48 = 0x00455870, and +0x4c = 0x00455930".to_string(), + "direct disassembly now also shows 0x004559d0 writing 0x55f1, serializing the three string lanes [this+0x206/+0x20a/+0x20e], writing 0x55f2, dispatching slot +0x4c, re-entering 0x52ec50, and then writing the closing 0x55f3 tag".to_string(), + "direct disassembly now also shows the paired chooser siblings calling 0x00490960 directly on the child-construction side, alongside 0x0048a340, 0x0048f4c0, and 0x00490200".to_string(), + "direct disassembly now also shows 0x00490960 copying selector fields into the child object ([this+0x219], [this+0x251], bit 0x20 in [this+0x24c], and [this+0x226]), allocating a fresh 0x23a Infrastructure child, seeding it through 0x00455b70 with caller-supplied stem input plus fixed literal Infrastructure at 0x005cfd74, attaching it through 0x005395d0, seeding position lanes through 0x00539530/0x0053a5b0, and optionally caching it as primary child in [this+0x248]".to_string(), + "the currently grounded direct-constructor chooser branches are narrower now too: the repeated calls at 0x004a2eba/0x004a30f9/0x004a339c feed 0x00490960 with mode arg 0x0a and stem arg 0x005cb138 = BallastCapDT_Cap.3dp, so they bypass the selector-copy block at 0x004909e2 and go straight into fresh child allocation/seeding".to_string(), + "the wider direct-calls sweep now also grounds stable 0x00490960 mode families: mode 0x0b pairs with fixed TrackCapDT/ST_Cap literals at 0x0048ed01/0x0048ed20, mode 0x03 with OverpassST_section at 0x00495a44, mode 0x02 with the decoded TunnelST/TunnelDT tables and zero-stem fallbacks across 0x004a17eb/0x004a1995/0x004a1b44/0x004a1b7d/0x004a1b95, and mode 0x01 with the decoded BridgeDT/BridgeST tables plus bridge zero-stem fallbacks across 0x004a1dae/0x004a2043/0x004a2082/0x004a221e/0x004a22a5/0x004a23aa/0x004a23eb/0x004a2409/0x004a24f6".to_string(), + "objdump on 0x00490960 now also sharpens the source-side comparison for the remaining mixed exact-prefix classes: mode lives at [esp+0x10], stem at [esp+0x14], args 3/4 at [esp+0x18]/[esp+0x1c] feed 0x539530, arg 5 at [esp+0x20] feeds 0x53a5b0, arg 10 at [esp+0x34] gates whether the new child is cached into [this+0x248], and the selector-copy block at 0x004909e2..0x00490a32 reads bytes from [esp+0x28]/[esp+0x2c]/[esp+0x30] into [this+0x219]/[this+0x251]/bit0x20 in [this+0x24c]. The fixed TrackCap mode-0x0b branches at 0x0048ed01/0x0048ed20 push literals 0x005cb198/0x005cb1ac after the same pre-seeded 1,-1,-1,0,0 flag bundle, so they reach 0x490960 with arg7/arg8/arg9 = -1/-1/0 and bypass that selector-copy block because mode >= 4. The tunnel mode-0x02 family at 0x004a17eb/0x004a1995/0x004a1b44/0x004a1b7d plus zero-stem fallback 0x004a1b95 necessarily flows through the selector-copy block because mode < 4, and the objdump caller bundles show those branches reaching 0x490960 with arg8 fixed at 1, arg9 fixed at 0, and only arg7 varying through the branch-local register (ebx/ebp) before the table or fallback stem is pushed".to_string(), + "direct disassembly now also makes that tunnel-versus-track-cap residue more exact: 0x004a17eb/0x004a1995 drive mode-0x02 through TunnelDT/TunnelST tables 0x621a94/0x621a64 with arg7 entering as a one-bit selector (0 or 1) after the local sbb/inc pair; 0x004a1b44/0x004a1b7d repeat the same one-bit arg7 pattern through sibling tables 0x621a9c/0x621a6c; and the fallback 0x004a1b95 clears both stem and selector bundle entirely. By contrast, 0x0048ed01/0x0048ed20 reach mode-0x0b with the exact same 1,-1,-1,0,0 bundle and differ only by the pushed stem literal 0x005cb198 versus 0x005cb1ac.".to_string(), + "objdump on 0x00455b70 now also makes the shared child seed strip concrete: after zeroing the same [this+0x206/+0x20a/+0x20e] lanes, it copies stack args 1/2/3 into them through 0x51d820 whenever those args are non-null, so the 0x490960 call pattern seeds [0x206] from fixed payload literal 0x005c87a8, [0x20a] from the caller stem, and [0x20e] from fixed literal 0x005cfd74 = \"Infrastructure\" before 0x004559d0 later serializes those same three lanes".to_string(), + "objdump on 0x51d820 now also shows those seeded lanes are owned heap strings, not encoded ids: it frees any prior pointer through 0x5a1145, counts the incoming NUL-terminated ASCII bytes, allocates a fresh buffer through 0x5a125d, and copies the source string byte-for-byte into the destination slot".to_string(), + "direct disassembly now also shows 0x00490200 reading the seeded lanes [this+0x206/+0x20a/+0x20e] back through the live route collection at 0x006cfca8, classifying peer relationships with [this+0x216/+0x218/+0x201/+0x202], and therefore acting as a route/link comparator above the same child payload fields that 0x004559d0 later serializes".to_string(), + "the direct route-entry bridge is tighter now too: 0x0048e140/0x0048e160/0x0048e180 simply resolve [this+0x206/+0x20a/+0x20e] through the live route collection at 0x006cfca8 and return the pointed route-entry or null, while 0x0048e1a0 walks the first two seeded lanes, resolves each peer route, and compares [peer+0x20e]/[peer+0x201] plus conditional [peer+0x206]/[peer+0x20a] against [this+0x202] before returning a boolean match".to_string(), + "the neighboring cached-primary-child path is narrower now too: 0x0048ed30 reads [this+0x248], walks child list [this+0x08] through 0x556ef0/0x556fa0, clears [this+0x248] when it matches the current child, destroys the child through 0x455d20/0x455650/0x53b080, and tears the list down through 0x556f20/0x5570b0/0x5571d0. That makes [this+0x248] a child-list cache and cleanup lane rather than the first route-entry bridge.".to_string(), + "the chooser tables now decode to concrete asset families too: 0x621a44/0x621a54 feed BridgeST caps/sections, 0x621a64 feeds TunnelST cap/section variants, 0x621a74/0x621a84 feed BridgeDT caps/sections, and 0x621a94 feeds TunnelDT variants; fixed literals 0x5cb138/0x5cb150 are BallastCapDT/ST and 0x5cb168/0x5cb180 are OverpassDT/ST".to_string(), + "the top-level chooser branches are grounded now too: [this+0x226]==1 routes bridge families, [this+0x226]==2 routes tunnel families, [this+0x226]==3 routes overpass/ballast families, and bit 0x20 in [this+0x24c] selects the cap-oriented side over the section-oriented side inside those DT/ST siblings".to_string(), + "direct disassembly now also shows 0x0048a340 as the exact chooser-state setter: its dword argument writes [this+0x226], its next two byte arguments write [this+0x219] and [this+0x251], and its final byte argument toggles bit 0x20 in [this+0x24c]".to_string(), + "the material selectors are grounded now too: in the bridge branch, [this+0x219] indexes Steel/Stone/Wood tables directly while value 2 takes the special suspension-cap path through [this+0x252]; in the tunnel branch, [this+0x251] selects Brick versus Concrete while the cap/section split comes from bit 0x20 choosing the base versus +0x8 table entry".to_string(), + "the [this+0x252] selector is partially grounded now too: when [this+0x219]==2, the chooser jump tables dispatch fixed BridgeDT/BridgeST suspension-cap literals for R10, L10, 12, 14, 16, and 18 variants instead of using the general Bridge* table families".to_string(), + ] +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/live_entry_directory.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/live_entry_directory.rs new file mode 100644 index 0000000..9365d8c --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/live_entry_directory.rs @@ -0,0 +1,8 @@ +pub(super) fn build_live_entry_directory_evidence() -> Vec { + vec![ + "direct disassembly now shows 0x00518140 resolving a non-direct live entry through the tombstone bitset and then returning the first dword of a 12-byte row from [collection+0x3c] for the 0x38a5 path".to_string(), + "direct disassembly now also shows 0x005181f0/0x00518260 treating the same 12-byte rows as a live-entry directory: dword +0 is the payload pointer, dword +4 is previous live id, and dword +8 is next live id, with collection head/tail caches alongside them".to_string(), + "direct disassembly now also shows 0x00493be0 iterating live-entry ordinals through 0x00518380(ordinal, 0), converting each ordinal to a live id, then resolving that live id through 0x00518140 before handing the resulting payload pointer to 0x0048dcf0".to_string(), + "direct disassembly now shows 0x00518680 loading the non-direct collection header, tombstone bitset, and live-id-bound-scaled 12-byte tables for the non-direct path before 0x00493be0 starts iterating".to_string(), + ] +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/mod.rs new file mode 100644 index 0000000..7d64bd8 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/mod.rs @@ -0,0 +1,114 @@ +use super::super::family_scan::InfrastructureFamilyScan; +use super::InfrastructureTraceNarrative; +use super::follow_on::build_route_local_runtime_follow_on_hypothesis; +use super::notes::build_infrastructure_trace_notes; +use crate::inspect::smp::services::*; + +mod attach_rebuild; +mod constructor_families; +mod live_entry_directory; +mod payload_writer; +mod save_side_corpus; + +pub(super) fn build_infrastructure_trace_narrative( + analysis: &SmpSaveCompanyChairmanAnalysisReport, + scan: &InfrastructureFamilyScan, +) -> InfrastructureTraceNarrative { + let side_buffer = analysis.placed_structure_dynamic_side_buffer.as_ref(); + let alignment = analysis + .placed_structure_dynamic_side_buffer_alignment + .as_ref(); + let st_only_name_pair_corpus = scan.st_only_name_pair_corpus; + + let candidate_consumer_hypotheses = vec![ + attach_rebuild::build_attach_rebuild_hypothesis(analysis, scan), + payload_writer::build_serializer_load_companion_hypothesis(side_buffer.is_some()), + build_route_local_runtime_follow_on_hypothesis(side_buffer.is_some()), + ]; + let branches = vec![ + build_service_trace_branch_status( + "infrastructure_asset_owner_seam", + if side_buffer.is_some() { + "grounded_separate_owner_seam" + } else { + "blocked_missing_side_buffer_owner_seam" + }, + &[ + "0x38a5/0x38a6/0x38a7 tagged family", + "embedded 0x55f1 dual-name rows", + "compact 6-byte prefix regimes", + ], + if side_buffer.is_some() { + &[] + } else { + &["0x38a5 owner seam"] + }, + &[ + "0x00493be0 infrastructure tagged side-buffer collection load owner", + "0x0048dcf0 infrastructure tagged child-stream restore outer owner", + ], + &[ + "This seam should be treated as infrastructure-asset state rather than as a compact alias of placed-structure triplets.", + ], + ), + build_service_trace_branch_status( + "placed_structure_triplet_alias", + if alignment.is_some_and(|probe| probe.overlapping_name_pair_count == 0) { + "disproved_by_grounded_probe" + } else { + "unresolved" + }, + &[ + "0x36b1 placed-structure triplet corpus", + "0x38a5 side-buffer name-pair corpus", + ], + &[], + &[], + &[ + "Grounded q.gms evidence currently shows zero overlap between the side-buffer name-pair corpus and the placed-structure triplet name-pair corpus.", + ], + ), + build_service_trace_branch_status( + "city_connection_consumer_mapping", + "blocked_missing_infrastructure_asset_consumer_mapping", + &[ + "grounded 0x38a5 owner seam", + "placed-structure triplet seam", + ], + &[ + "higher-layer consumer dispatch mapping", + "compact prefix regime semantics", + ], + &[ + "0x0048a1e0 infrastructure child attach helper", + "0x0048dd50 infrastructure child rebuild loop", + "0x00490a3c infrastructure payload attach helper", + ], + &[ + "The remaining problem is how higher-layer service code consumes this separate seam, not whether the seam exists.", + ], + ), + build_service_trace_branch_status( + "linked_transit_consumer_mapping", + "blocked_missing_infrastructure_asset_consumer_mapping", + &["grounded 0x38a5 owner seam", "company linked-transit latch"], + &[ + "side-buffer consumer mapping", + "route or roster rebuild owner path", + ], + &[ + "0x00448a70 / 0x00493660 / 0x0048b660 route and world follow-on family", + "0x004133b0 placed-structure local-runtime refresh outer owner", + ], + &[ + "The next slice should target the consumer path above the side-buffer seam rather than another raw save scan.", + ], + ), + ]; + let notes = build_infrastructure_trace_notes(st_only_name_pair_corpus); + InfrastructureTraceNarrative { + candidate_consumer_hypotheses, + branches, + notes, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/payload_writer.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/payload_writer.rs new file mode 100644 index 0000000..39ba7bc --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/payload_writer.rs @@ -0,0 +1,27 @@ +use crate::inspect::smp::services::*; + +pub(super) fn build_serializer_load_companion_hypothesis( + side_buffer_present: bool, +) -> SmpServiceConsumerHypothesis { + SmpServiceConsumerHypothesis { + label: "infrastructure serializer/load companion path".to_string(), + status: if side_buffer_present { + "strong_static_mapping_candidate".to_string() + } else { + "possible_consumer_family".to_string() + }, + candidate_consumers: vec![ + "0x004559d0 infrastructure tagged string-triplet serializer".to_string(), + "0x00455870 infrastructure tagged string-triplet load companion".to_string(), + "0x00455930 infrastructure scalar-triplet serializer sibling".to_string(), + ], + evidence: vec![ + "atlas already bounds the serializer/load strip around the Infrastructure owner and the same 0x55f1/0x55f2/0x55f3 tag family".to_string(), + "local .rdata at 0x005cfd00 now proves the infrastructure child vtable points straight at 0x455fc0/0x4559d0/0x455870/0x455930 for the load, tagged serializer, triplet-restore, and scalar serializer slots".to_string(), + "the save-side side-buffer carries embedded dual-name rows plus compact prefixes, which is compatible with a serializer-side bridge".to_string(), + ], + blockers: vec![ + "which exact 0x38a5 rows belong to shared 0x55f1/0x55f2/0x55f3 child records versus outer collection metadata, now that the concrete write-side slot +0x44 serializer is grounded as 0x004559d0".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/save_side_corpus.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/save_side_corpus.rs new file mode 100644 index 0000000..e6d09ad --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/consumers/save_side_corpus.rs @@ -0,0 +1,521 @@ +use crate::inspect::smp::services::*; + +pub(super) fn build_save_side_corpus_evidence( + analysis: &SmpSaveCompanyChairmanAnalysisReport, + scan: &super::super::super::family_scan::InfrastructureFamilyScan, +) -> Vec { + let side_buffer = analysis.placed_structure_dynamic_side_buffer.as_ref(); + let bridge_like_name_pair_count = scan.bridge_like_name_pair_count; + let tunnel_like_name_pair_count = scan.tunnel_like_name_pair_count; + let track_cap_like_name_pair_count = scan.track_cap_like_name_pair_count; + + vec![ + format!( + "real side-buffer name families currently count bridge/tunnel/track-cap pairs as {}/{}/{}", + bridge_like_name_pair_count, + tunnel_like_name_pair_count, + track_cap_like_name_pair_count + ), + side_buffer + .and_then(|probe| probe.first_record_child_count_after_owner_shared) + .map(|child_count| { + format!( + "grounded q.gms bytes now also show the first 0x38a6 record starting immediately after that shared dword with child_count={}, saved_primary_child_byte={}, and first 0x55f1 at offset +0x{:x}", + child_count, + side_buffer + .and_then(|probe| { + probe.first_record_saved_primary_child_byte_after_owner_shared_hex + .as_deref() + }) + .unwrap_or("0x00"), + side_buffer + .and_then(|probe| { + probe.first_record_first_name_tag_relative_offset_after_owner_shared + }) + .unwrap_or_default() + ) + }) + .unwrap_or_else(|| { + "no grounded first-record prelude summary was available after the shared 0x38a6 owner dword".to_string() + }), + format!( + "current save-side probe reports {} embedded 0x55f1 rows with a third decoded string", + side_buffer + .map(|probe| probe.decoded_embedded_name_row_with_tertiary_name_count) + .unwrap_or_default() + ), + format!( + "current save-side probe also reports {} complete 0x55f1/0x55f2/0x55f3 envelopes, dominant 0x55f2 chunk len 0x{:x}, and dominant 0x55f3 span 0x{:x}", + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .map(|summary| summary.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope) + .unwrap_or_default(), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.dominant_policy_chunk_len) + .unwrap_or_default(), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.dominant_profile_chunk_len) + .unwrap_or_default() + ), + format!( + "current save-side probe reports {} rows with the short 0x06-byte trailing span; dominant short flag pair is {}/{} x{}", + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()) + .map(|summary| summary.row_count_with_0x06_profile_span) + .unwrap_or_default(), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()) + .and_then(|summary| summary.dominant_flag_pair.as_ref()) + .map(|pair| pair.first_flag_byte_hex.as_str()) + .unwrap_or("0x00"), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()) + .and_then(|summary| summary.dominant_flag_pair.as_ref()) + .map(|pair| pair.second_flag_byte_hex.as_str()) + .unwrap_or("0x00"), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()) + .and_then(|summary| summary.dominant_flag_pair.as_ref()) + .map(|pair| pair.count) + .unwrap_or_default() + ), + format!( + "current save-side probe reports {} fixed 0x1a policy rows; dominant trailing word is {} x{}", + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.fixed_policy_summary.as_ref()) + .map(|summary| summary.row_count_with_0x1a_policy_chunk) + .unwrap_or_default(), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.fixed_policy_summary.as_ref()) + .and_then(|summary| summary.dominant_trailing_word_hex.as_deref()) + .unwrap_or("0x0000"), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.fixed_policy_summary.as_ref()) + .map(|summary| summary.dominant_trailing_word_count) + .unwrap_or_default() + ), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.dominant_profile_span_class_summary.as_ref()) + .map(|summary| { + format!( + "the dominant 0x{:x}-byte post-profile class is now narrowed too: dominant name pair is {:?}/{:?} x{}, dominant compact prefix is {}/{}/{} x{}, and dominant prelude candidate is {}/{} x{} across {} rows", + summary.profile_chunk_len_to_next_name_or_end, + summary.dominant_primary_name, + summary.dominant_secondary_name, + summary.dominant_name_pair_count, + summary + .dominant_prefix_leading_dword_hex + .as_deref() + .unwrap_or("0x00000000"), + summary + .dominant_prefix_trailing_word_hex + .as_deref() + .unwrap_or("0x0000"), + summary + .dominant_prefix_separator_byte_hex + .as_deref() + .unwrap_or("0x00"), + summary.dominant_prefix_count, + summary + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.child_count_candidate_hex.as_str()) + .unwrap_or("0x0000"), + summary + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) + .unwrap_or("0x00"), + summary + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.count) + .unwrap_or_default(), + summary.row_count + ) + }) + .unwrap_or_else(|| { + "no dominant post-profile class summary was available for the embedded 0x55f3 spans".to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.dominant_profile_span_class_summary.as_ref()) + .map(|summary| { + format!( + "the dominant post-profile outliers are now explicit too: name-pair counts={:?}, compact-prefix counts={:?}, candidate-pattern counts={:?}", + summary + .name_pair_summaries + .iter() + .map(|entry| format!( + "{:?}/{:?}:{}", + entry.primary_name, entry.secondary_name, entry.count + )) + .collect::>(), + summary + .compact_prefix_pattern_summaries + .iter() + .map(|entry| format!( + "{}/{}/{}:{}", + entry.prefix_leading_dword_hex, + entry.prefix_trailing_word_hex, + entry.prefix_separator_byte_hex, + entry.count + )) + .collect::>(), + summary + .candidate_pattern_summaries + .iter() + .map(|entry| format!( + "{}/{}:{}", + entry.child_count_candidate_hex, + entry.saved_primary_child_byte_candidate_hex, + entry.count + )) + .collect::>() + ) + }) + .unwrap_or_else(|| { + "no dominant post-profile outlier breakdown was available".to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .map(|summary| { + format!( + "candidate-pattern correlations now split the remaining prelude classes cleanly too: {:?}", + summary + .candidate_pattern_correlations + .iter() + .map(|entry| format!( + "{}/{} rows={} dominant-name={:?}/{:?} x{} dominant-prev-span={:?} x{}", + entry.child_count_candidate_hex, + entry.saved_primary_child_byte_candidate_hex, + entry.row_count, + entry.dominant_primary_name, + entry.dominant_secondary_name, + entry.dominant_name_pair_count, + entry.dominant_profile_span, + entry.dominant_profile_span_count + )) + .collect::>() + ) + }) + .unwrap_or_else(|| { + "no candidate-pattern correlation summary was available".to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .map(|summary| { + format!( + "mode-family correlations now also split the candidate patterns directly: {:?}", + summary + .candidate_pattern_correlations + .iter() + .map(|entry| format!( + "{}/{} rows={} dominant-mode={:?} x{} mode-counts={:?}", + entry.child_count_candidate_hex, + entry.saved_primary_child_byte_candidate_hex, + entry.row_count, + entry.dominant_mode_family, + entry.dominant_mode_family_count, + entry + .mode_family_counts + .iter() + .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) + .collect::>() + )) + .collect::>() + ) + }) + .unwrap_or_else(|| { + "no mode-family correlation summary was available for the prelude candidates" + .to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .map(|summary| { + format!( + "profile-span mode-family correlations now also split the previous 0x55f3 spans directly: {:?}", + summary + .profile_span_correlations + .iter() + .map(|entry| format!( + "span=0x{:x} rows={} dominant-mode={:?} x{} mode-counts={:?}", + entry.previous_profile_chunk_len_to_next_name_or_end, + entry.row_count, + entry.dominant_mode_family, + entry.dominant_mode_family_count, + entry + .mode_family_counts + .iter() + .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) + .collect::>() + )) + .collect::>() + ) + }) + .unwrap_or_else(|| { + "no profile-span mode-family correlation summary was available".to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .map(|summary| { + format!( + "exact compact-prefix correlations now split the residual prelude classes directly: {:?}", + summary + .compact_prefix_correlations + .iter() + .map(|entry| format!( + "{}/{}/{} rows={} dominant-name={:?}/{:?} x{} dominant-span={:?} x{} dominant-candidate={:?} dominant-mode={:?} x{}", + entry.prefix_leading_dword_hex, + entry.prefix_trailing_word_hex, + entry.prefix_separator_byte_hex, + entry.row_count, + entry.dominant_primary_name, + entry.dominant_secondary_name, + entry.dominant_name_pair_count, + entry.dominant_profile_span, + entry.dominant_profile_span_count, + entry + .dominant_candidate_pattern + .as_ref() + .map(|pattern| format!( + "{}/{}:{}", + pattern.child_count_candidate_hex, + pattern.saved_primary_child_byte_candidate_hex, + pattern.count + )), + entry.dominant_mode_family, + entry.dominant_mode_family_count + )) + .collect::>() + ) + }) + .unwrap_or_else(|| { + "no compact-prefix correlation summary was available for the prelude candidates" + .to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .and_then(|summary| { + summary.candidate_pattern_correlations.iter().find(|entry| { + entry.child_count_candidate == 2 + && entry.saved_primary_child_byte_candidate == 0xff + && entry.dominant_primary_name.as_deref() + == Some("BridgeSTWood_Section.3dp") + && entry.dominant_secondary_name.as_deref() == Some("Infrastructure") + }) + }) + .map(|correlation| { + format!( + "the bridge-only two-child class is now grounded save-side too: candidate pattern {}/{} spans {} rows, stays pure {:?}/{:?}, and the dominant prior profile span is {:?} x{}", + correlation.child_count_candidate_hex, + correlation.saved_primary_child_byte_candidate_hex, + correlation.row_count, + correlation.dominant_primary_name, + correlation.dominant_secondary_name, + correlation.dominant_profile_span, + correlation.dominant_profile_span_count + ) + }) + .unwrap_or_else(|| { + "no grounded pure bridge-only two-child candidate class was available in the prelude correlations".to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .and_then(|summary| { + summary + .profile_span_correlations + .iter() + .find(|row| row.previous_profile_chunk_len_to_next_name_or_end == 3) + }) + .map(|correlation| { + format!( + "current save-side probe now also shows the short 0x03-byte post-profile gaps collapsing cleanly to the next-record prelude: dominant candidate pattern is {}/{} x{} across {} rows, mode counts={:?}, prefix counts={:?}", + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.child_count_candidate_hex.as_str()) + .unwrap_or("0x0000"), + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) + .unwrap_or("0x00"), + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.count) + .unwrap_or_default(), + correlation.row_count, + correlation + .mode_family_counts + .iter() + .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) + .collect::>(), + correlation + .compact_prefix_pattern_summaries + .iter() + .map(|prefix| format!( + "{}/{}/{}:{}", + prefix.prefix_leading_dword_hex, + prefix.prefix_trailing_word_hex, + prefix.prefix_separator_byte_hex, + prefix.count + )) + .collect::>() + ) + }) + .unwrap_or_else(|| { + "no grounded short post-profile gap correlation was available".to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .and_then(|summary| { + summary + .profile_span_correlations + .iter() + .find(|row| row.previous_profile_chunk_len_to_next_name_or_end == 0x27) + }) + .map(|correlation| { + format!( + "the sparse 0x27 post-profile outlier is explicit too: mode counts={:?}, prefix counts={:?}, sample names={:?}", + correlation + .mode_family_counts + .iter() + .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) + .collect::>(), + correlation + .compact_prefix_pattern_summaries + .iter() + .map(|prefix| format!( + "{}/{}/{}:{}", + prefix.prefix_leading_dword_hex, + prefix.prefix_trailing_word_hex, + prefix.prefix_separator_byte_hex, + prefix.count + )) + .collect::>(), + correlation + .sample_rows + .iter() + .map(|row| format!("{:?}/{:?}", row.primary_name, row.secondary_name)) + .collect::>() + ) + }) + .unwrap_or_else(|| "no sparse 0x27 post-profile outlier was available".to_string()), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .and_then(|summary| { + summary.compact_prefix_correlations.iter().find(|entry| { + entry.prefix_leading_dword == 0xff00_00ff + && entry.prefix_trailing_word == 0x0001 + && entry.prefix_separator_byte == 0xff + }) + }) + .map(|correlation| { + format!( + "the 0xff0000ff/0x0001/0xff compact-prefix class is now explicit: dominant prelude={}/{} dominant-name={:?}/{:?} dominant-span={:?}", + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.child_count_candidate_hex.as_str()) + .unwrap_or("0x0000"), + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) + .unwrap_or("0x00"), + correlation.dominant_primary_name, + correlation.dominant_secondary_name, + correlation.dominant_profile_span + ) + }) + .unwrap_or_else(|| { + "no 0xff0000ff/0x0001/0xff compact-prefix class summary was available" + .to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .and_then(|summary| { + summary.compact_prefix_correlations.iter().find(|entry| { + entry.prefix_leading_dword == 0x0000_55f3 + && entry.prefix_trailing_word == 0x0001 + && entry.prefix_separator_byte == 0xff + }) + }) + .map(|correlation| { + format!( + "the 0x000055f3/0x0001/0xff compact-prefix class is now explicit: dominant prelude={}/{} dominant-name={:?}/{:?} dominant-span={:?}", + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.child_count_candidate_hex.as_str()) + .unwrap_or("0x0000"), + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) + .unwrap_or("0x00"), + correlation.dominant_primary_name, + correlation.dominant_secondary_name, + correlation.dominant_profile_span + ) + }) + .unwrap_or_else(|| { + "no 0x000055f3/0x0001/0xff compact-prefix class summary was available" + .to_string() + }), + side_buffer + .and_then(|probe| probe.payload_envelope_summary.as_ref()) + .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) + .and_then(|summary| { + summary.compact_prefix_correlations.iter().find(|entry| { + entry.prefix_leading_dword == 0xff00_00ff + && entry.prefix_trailing_word == 0x0002 + && entry.prefix_separator_byte == 0xff + }) + }) + .map(|correlation| { + format!( + "the 0xff0000ff/0x0002/0xff compact-prefix class is now explicit: dominant prelude={}/{} dominant-name={:?}/{:?} dominant-span={:?}", + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.child_count_candidate_hex.as_str()) + .unwrap_or("0x0000"), + correlation + .dominant_candidate_pattern + .as_ref() + .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) + .unwrap_or("0x00"), + correlation.dominant_primary_name, + correlation.dominant_secondary_name, + correlation.dominant_profile_span + ) + }) + .unwrap_or_else(|| { + "no 0xff0000ff/0x0002/0xff compact-prefix class summary was available" + .to_string() + }), + "the current grounded q.gms side-buffer name corpus now maps directly onto those constructor families too: BridgeSTWood_Section.3dp aligns with mode 0x01 Bridge, TunnelSTBrick_Cap/Section.3dp with mode 0x02 Tunnel, BallastCapST_Cap.3dp with mode 0x0a BallastCap, and TrackCapST_Cap.3dp with mode 0x0b TrackCap; only the Overpass mode-0x03 family remains static-only in the current save corpus".to_string(), + ] +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/follow_on.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/follow_on.rs new file mode 100644 index 0000000..a045a5e --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/follow_on.rs @@ -0,0 +1,31 @@ +use crate::inspect::smp::services::*; + +pub(super) fn build_route_local_runtime_follow_on_hypothesis( + side_buffer_present: bool, +) -> SmpServiceConsumerHypothesis { + SmpServiceConsumerHypothesis { + label: "route/local-runtime follow-on path".to_string(), + status: if side_buffer_present { + "secondary_candidate_after_attach_rebuild".to_string() + } else { + "possible_consumer_family".to_string() + }, + candidate_consumers: vec![ + "0x00448a70 / 0x00493660 / 0x0048b660 route and world follow-on family" + .to_string(), + "0x004133b0 placed-structure local-runtime refresh outer owner".to_string(), + ], + evidence: vec![ + "atlas ties the Infrastructure rebuild loop to later route-side and local-runtime follow-on owners".to_string(), + "current side-buffer trace shows separate infrastructure state and the direct seeded-lane bridge is now grounded before these later owners run".to_string(), + "direct disassembly now shows 0x00448a70 as a world-overlay byte write helper over [world+0x15e1/+0x162d], with the neighboring 0x00448af0 reading three world bitsets at [world+0x2139/+0x213d/+0x2141] rather than any infrastructure child fields".to_string(), + "direct disassembly now shows 0x00493660 as a counter-and-follow-on owner over one infrastructure child: it updates local counters by [child+0x218], [child+0x226], and [child+0x44], optionally resolves a peer through 0x48dcb0 and 0x426c20, then maps one world-raster byte back into the companion region collection 0x006cfc9c and calls 0x487960".to_string(), + "direct disassembly now shows 0x0048b660 as a presentation-color/style owner: it gates on global shell state, then branches on [child+0x216], [child+0x218], [child+0x226], [child+0x44], and bit 0x40 in [child+0x201] before publishing fixed RGBA tuples through 0x53a350".to_string(), + "the remaining local-runtime helpers are tighter now too: 0x0048e2c0 flips bit 0x20 in [child+0x201] and, when enabling that bit, reruns 0x53a3a0 plus 0x48a9e0; 0x0048e330 similarly flips bit 0x40 in [child+0x201] and tail-calls 0x48b660; and 0x0048e3c0 updates [child+0x22e] through the auxiliary route tracker 0x006cfcb4 after resolving one route entry via [child+0x212], then scans class-0 regions through 0x455800/0x455810 and 0x51dbb0 before setting bit 0x02 in [child+0x24c] only when the region test fails".to_string(), + ], + blockers: vec![ + "which mixed exact compact-prefix classes still survive into these later owners after the seeded-lane bridge and earlier child-stream restore semantics are accounted for".to_string(), + "no direct save-side correlation yet between the remaining mixed exact classes and the later 0x00493660 counter buckets, 0x0048b660 style branches, or the 0x0048e2c0/0x0048e3c0 flag-and-region follow-ons".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/footer_flags.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/footer_flags.rs new file mode 100644 index 0000000..5e63451 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/footer_flags.rs @@ -0,0 +1,8 @@ +pub(super) fn build_footer_flag_notes() -> Vec { + vec![ + "Cross-save q/p traces now also split those two mixed residual classes by footer and span behavior: 0x000055f3/0x0001/0xff always carries short-flag pair 0x01/0x00 on a fixed span-0x03 tunnel-dominant family, while 0xff0000ff/0x0001/0xff always carries short-flag pair 0x00/0x00 on the scattered-span TrackCap-dominant outlier family. The remaining unknown is therefore the meaning of those short-flag families and the sparse branch that routes a minority of tunnel rows into the 0xff0000ff outlier class.".to_string(), + "Direct consumers of those footer bits are grounded now too: bit 0x20 of [child+0x20] is the admission gate into the 0x00528d90 branch when no caller/global override is present, while bit 0x40 only feeds the later 0x00529730 -> 0x530280 follow-on. Since both mixed residual classes keep the second footer byte at zero in q/p, the remaining split is now specifically the first footer byte / bit-0x20 gate rather than both footer bytes.".to_string(), + "That bit-0x20 gate is no longer floating without context either: the 0x005295f0..0x005297b7 consumer strip repopulates candidate cells through 0x00533ba0, walks child lists through 0x00556ef0/0x00556f00, and honors the same controller mode byte [owner+0x3692] that the atlas already places under the world-window presentation dispatcher. The next infrastructure pass should therefore treat the remaining bit-0x20 question as a nearby-presentation/controller owner problem, not as a serializer-only problem.".to_string(), + "That owner family is layout-state specific now too: the surrounding helpers sit on atlas-backed layout/presenter roots, with 0x00548da0 walking layout list root [layout+0x2593] and 0x0054bab0 mutating layout slots [layout+0x2637/+0x263b/+0x2643]. So the remaining bit-0x20 split is increasingly a layout/presentation admission question above the infrastructure seam, not a simulation-owned route or rebuild question.".to_string(), + ] +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/mod.rs new file mode 100644 index 0000000..a1cbf89 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/mod.rs @@ -0,0 +1,19 @@ +use crate::inspect::smp::services::*; + +mod consumers; +mod follow_on; +mod footer_flags; +mod notes; + +pub(super) struct InfrastructureTraceNarrative { + pub(super) candidate_consumer_hypotheses: Vec, + pub(super) branches: Vec, + pub(super) notes: Vec, +} + +pub(super) fn build_infrastructure_trace_narrative( + analysis: &SmpSaveCompanyChairmanAnalysisReport, + scan: &super::family_scan::InfrastructureFamilyScan, +) -> InfrastructureTraceNarrative { + consumers::build_infrastructure_trace_narrative(analysis, scan) +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/notes.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/notes.rs new file mode 100644 index 0000000..99d2e38 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/hypotheses/notes.rs @@ -0,0 +1,14 @@ +use super::footer_flags::build_footer_flag_notes; + +pub(super) fn build_infrastructure_trace_notes(st_only_name_pair_corpus: bool) -> Vec { + let mut notes = vec![ + "Infrastructure asset trace now makes the side-buffer-versus-triplet split explicit: owner seam identity is grounded, the pure bridge-only 0x0002/0xff candidate class is grounded save-side, the upstream chooser above the child attach path is grounded as paired DT/ST siblings at 0x004a2c80 and 0x004a34e0 with decoded Bridge/Tunnel/BallastCap/Overpass families, grounded top-level branch meaning, grounded bridge/tunnel material selector roles, a concrete child-construction/write-side chain through 0x00490960, 0x00491c60, 0x0048a6c0, 0x00455a40, and 0x004559d0, and stable 0x00490960 mode families for BallastCap, TrackCap, Overpass, Tunnel, and Bridge branches. The current save-side name corpus already maps BallastCap, TrackCap, Tunnel, and Bridge rows onto those families directly, the candidate-pattern correlation narrows the dominant mixed 0x0001/0xff class to bridge:62 / track_cap:21 / tunnel:19, and the exact compact-prefix correlation now splits the full prelude corpus into mostly pure classes: 0xff0000ff/0x0002/0xff is pure bridge, 0xff000000/{0x0001,0x0002}/0xff are pure bridge, 0xf3010100/0x0055/0x00 is pure ballast-cap, and 0x0005d368/0x0001/0xff is pure track-cap, leaving only 0xff0000ff/0x0001/0xff and 0x000055f3/0x0001/0xff as the mixed residual classes.".to_string(), + ]; + notes.extend(build_footer_flag_notes()); + notes.push(if st_only_name_pair_corpus { + "The current save-side side-buffer corpus is ST-only, so this trace directly exercises the ST chooser sibling while the DT sibling remains grounded statically but unexercised in this save.".to_string() + } else { + "The current save-side side-buffer corpus is not ST-only, so both chooser siblings or a DT-facing save-side class may be in play.".to_string() + }); + notes +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/mod.rs new file mode 100644 index 0000000..847d844 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/mod.rs @@ -0,0 +1,8 @@ +mod atlas; +mod entrypoints; +mod family_scan; +mod hypotheses; +mod status; + +pub use entrypoints::inspect_save_infrastructure_asset_trace_file; +pub(in crate::inspect::smp) use status::build_infrastructure_asset_trace_report; diff --git a/crates/rrt-runtime/src/inspect/smp/services/infrastructure/status.rs b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/status.rs new file mode 100644 index 0000000..0431557 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/infrastructure/status.rs @@ -0,0 +1,48 @@ +use super::atlas::build_infrastructure_atlas_inputs; +use super::family_scan::analyze_infrastructure_name_pairs; +use super::hypotheses::build_infrastructure_trace_narrative; +use crate::inspect::smp::services::*; + +pub(in crate::inspect::smp) fn build_infrastructure_asset_trace_report( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> SmpInfrastructureAssetTraceReport { + let scan = analyze_infrastructure_name_pairs(analysis); + let atlas = build_infrastructure_atlas_inputs(); + let narrative = build_infrastructure_trace_narrative(analysis, &scan); + let side_buffer = analysis.placed_structure_dynamic_side_buffer.as_ref(); + let alignment = analysis + .placed_structure_dynamic_side_buffer_alignment + .as_ref(); + + SmpInfrastructureAssetTraceReport { + profile_family: analysis.profile_family.clone(), + placed_structure_collection_header_present: analysis + .placed_structure_collection_header + .is_some(), + placed_structure_record_triplet_count: analysis + .placed_structure_record_triplets + .as_ref() + .map(|probe| probe.record_count) + .unwrap_or_default(), + side_buffer_present: side_buffer.is_some(), + side_buffer_decoded_embedded_name_row_count: side_buffer + .map(|probe| probe.decoded_embedded_name_row_count) + .unwrap_or_default(), + side_buffer_unique_name_pair_count: side_buffer + .map(|probe| probe.unique_embedded_name_pair_count) + .unwrap_or_default(), + bridge_like_name_pair_count: scan.bridge_like_name_pair_count, + tunnel_like_name_pair_count: scan.tunnel_like_name_pair_count, + track_cap_like_name_pair_count: scan.track_cap_like_name_pair_count, + triplet_alignment_overlap_count: alignment + .map(|probe| probe.overlapping_name_pair_count) + .unwrap_or_default(), + atlas_candidate_consumers: atlas.atlas_candidate_consumers, + known_owner_bridge_fields: atlas.known_owner_bridge_fields, + known_bridge_helpers: atlas.known_bridge_helpers, + next_owner_questions: atlas.next_owner_questions, + candidate_consumer_hypotheses: narrative.candidate_consumer_hypotheses, + branches: narrative.branches, + notes: narrative.notes, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/mod.rs new file mode 100644 index 0000000..8be44da --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/mod.rs @@ -0,0 +1,13 @@ +use super::world::SmpSaveCompanyChairmanAnalysisReport; + +mod company; +mod infrastructure; +mod model; +mod region; +mod shared; + +pub use company::*; +pub use infrastructure::*; +pub use model::*; +pub use region::*; +pub(super) use shared::*; diff --git a/crates/rrt-runtime/src/inspect/smp/services/model/company.rs b/crates/rrt-runtime/src/inspect/smp/services/model/company.rs new file mode 100644 index 0000000..36b41b9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/model/company.rs @@ -0,0 +1,209 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::services::{ + SmpPeriodicTriLaneSaveShapeFamilyCandidateSummaryEntry, SmpServiceConsumerHypothesis, + SmpServiceTraceBranchStatus, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPeriodicCompanyServiceTraceEntry { + pub company_id: u32, + pub name: String, + pub active: bool, + #[serde(default)] + pub linked_chairman_profile_id: Option, + pub preferred_locomotive_engine_type_raw_u8: u8, + pub city_connection_latch: bool, + pub linked_transit_latch: bool, + pub linked_transit_autoroute_site_score_cache_refresh_absolute_counter: u32, + pub linked_transit_site_peer_cache_refresh_absolute_counter: u32, + #[serde(default)] + pub branches: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureProfilePayloadSummaryEntry { + pub profile_payload_dword_hex: String, + pub profile_status_kind: String, + pub count: usize, + #[serde(default)] + pub sample_primary_names: Vec, + #[serde(default)] + pub sample_secondary_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureProfilePayloadDeltaSummaryEntry { + pub delta_hex: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureProfileFooterPaddingSummaryEntry { + pub padding_len: usize, + pub count: usize, + #[serde(default)] + pub sample_hex_bytes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureProfileCompanionByteSummaryEntry { + pub companion_byte_hex: String, + pub count: usize, + #[serde(default)] + pub sample_primary_names: Vec, + #[serde(default)] + pub sample_secondary_names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { + pub companion_byte_hex: String, + pub primary_name: String, + pub secondary_name: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureNonzeroCompanionBuildingFamilyOverlapSummaryEntry { + pub companion_byte_hex: String, + pub primary_name: String, + pub secondary_name: String, + pub count: usize, + pub primary_matches_nonzero_stock_building_header_family: bool, + pub secondary_matches_nonzero_stock_building_header_family: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureNonzeroCompanionBuildingFamilyResidueSummaryEntry { + pub companion_byte_hex: String, + pub primary_name: String, + pub secondary_name: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructurePolicyTrailingWordSummaryEntry { + pub policy_trailing_word_hex: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPeriodicCompanyServiceTraceReport { + pub profile_family: String, + #[serde(default)] + pub selected_company_id: Option, + #[serde(default)] + pub world_issue_37_present: bool, + #[serde(default)] + pub world_finance_neighborhood_present: bool, + #[serde(default)] + pub region_record_body_present: bool, + #[serde(default)] + pub placed_structure_record_body_present: bool, + #[serde(default)] + pub infrastructure_asset_side_buffer_present: bool, + pub peer_site_selector_candidate_owner_strip: String, + pub peer_site_selector_candidate_persisted_tag_hex: String, + pub peer_site_selector_candidate_selector_lane: String, + pub peer_site_selector_candidate_secondary_payload_lane: String, + pub peer_site_selector_candidate_post_secondary_byte_status: String, + pub peer_site_selector_candidate_class_identity_status: String, + #[serde(default)] + pub peer_site_selector_candidate_helper_linkage: Vec, + #[serde(default)] + pub peer_site_selector_candidate_saved_payload_summaries: + Vec, + #[serde(default)] + pub peer_site_selector_candidate_saved_payload_delta_summaries: + Vec, + #[serde(default)] + pub peer_site_selector_candidate_saved_footer_padding_summaries: + Vec, + #[serde(default)] + pub peer_site_selector_candidate_saved_companion_byte_summaries: + Vec, + #[serde(default)] + pub peer_site_selector_candidate_saved_policy_trailing_word_summaries: + Vec, + #[serde(default)] + pub peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries: + Vec, + #[serde(default)] + pub peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries: + Vec, + #[serde(default)] + pub peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries: + Vec, + #[serde(default)] + pub peer_site_persisted_selector_bundle_fields: Vec, + #[serde(default)] + pub peer_site_rebuilt_transient_followon_fields: Vec, + pub peer_site_shellless_minimum_persisted_identity_status: String, + #[serde(default)] + pub peer_site_shellless_minimum_persisted_identity_inputs: Vec, + #[serde(default)] + pub peer_site_restore_input_fields: Vec, + #[serde(default)] + pub peer_site_runtime_input_fields: Vec, + pub peer_site_runtime_reconstruction_status: String, + #[serde(default)] + pub peer_site_runtime_reconstruction_steps: Vec, + #[serde(default)] + pub near_city_acquisition_region_input_fields: Vec, + #[serde(default)] + pub near_city_acquisition_peer_input_fields: Vec, + #[serde(default)] + pub near_city_acquisition_company_input_fields: Vec, + pub near_city_acquisition_shellless_readiness_status: String, + #[serde(default)] + pub near_city_acquisition_runtime_backed_input_families: Vec, + pub near_city_acquisition_site_owner_company_projection_status: String, + pub near_city_acquisition_site_self_id_projection_status: String, + pub near_city_acquisition_site_cached_tri_lane_projection_status: String, + pub near_city_acquisition_tri_lane_live_service_status: String, + pub near_city_acquisition_candidate_subtype_projection_status: String, + pub near_city_acquisition_backing_record_projection_status: String, + pub near_city_acquisition_nontransport_persisted_source_status: String, + #[serde(default)] + pub near_city_acquisition_nontransport_persisted_source_candidates: Vec, + pub near_city_acquisition_tri_lane_save_shape_family_status: String, + #[serde(default)] + pub near_city_acquisition_tri_lane_save_shape_family_candidates: + Vec, + #[serde(default)] + pub near_city_acquisition_tri_lane_live_owner_families: Vec, + #[serde(default)] + pub near_city_acquisition_tri_lane_candidate_gate_fields: Vec, + #[serde(default)] + pub near_city_acquisition_tri_lane_runtime_writer_roles: Vec, + #[serde(default)] + pub near_city_acquisition_tri_lane_direct_caller_families: Vec, + #[serde(default)] + pub near_city_acquisition_tri_lane_formula_input_lanes: Vec, + #[serde(default)] + pub near_city_acquisition_projection_hypotheses: Vec, + #[serde(default)] + pub near_city_acquisition_remaining_owner_gaps: Vec, + #[serde(default)] + pub near_city_acquisition_region_lane_statuses: Vec, + #[serde(default)] + pub atlas_candidate_consumers: Vec, + #[serde(default)] + pub known_bridge_helpers: Vec, + #[serde(default)] + pub next_owner_questions: Vec, + pub linked_transit_shellless_readiness_status: String, + #[serde(default)] + pub linked_transit_minimum_persisted_identity_inputs: Vec, + #[serde(default)] + pub linked_transit_live_rebuilt_cache_lanes: Vec, + #[serde(default)] + pub linked_transit_runtime_backed_input_families: Vec, + #[serde(default)] + pub linked_transit_remaining_owner_gaps: Vec, + #[serde(default)] + pub companies: Vec, + #[serde(default)] + pub notes: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/model/infrastructure.rs b/crates/rrt-runtime/src/inspect/smp/services/model/infrastructure.rs new file mode 100644 index 0000000..6c9640f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/model/infrastructure.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::services::{SmpServiceConsumerHypothesis, SmpServiceTraceBranchStatus}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpInfrastructureAssetTraceReport { + pub profile_family: String, + #[serde(default)] + pub placed_structure_collection_header_present: bool, + #[serde(default)] + pub placed_structure_record_triplet_count: usize, + #[serde(default)] + pub side_buffer_present: bool, + #[serde(default)] + pub side_buffer_decoded_embedded_name_row_count: usize, + #[serde(default)] + pub side_buffer_unique_name_pair_count: usize, + #[serde(default)] + pub bridge_like_name_pair_count: usize, + #[serde(default)] + pub tunnel_like_name_pair_count: usize, + #[serde(default)] + pub track_cap_like_name_pair_count: usize, + #[serde(default)] + pub triplet_alignment_overlap_count: usize, + #[serde(default)] + pub atlas_candidate_consumers: Vec, + #[serde(default)] + pub known_owner_bridge_fields: Vec, + #[serde(default)] + pub known_bridge_helpers: Vec, + #[serde(default)] + pub next_owner_questions: Vec, + #[serde(default)] + pub candidate_consumer_hypotheses: Vec, + #[serde(default)] + pub branches: Vec, + #[serde(default)] + pub notes: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/model/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/model/mod.rs new file mode 100644 index 0000000..a92bc52 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/model/mod.rs @@ -0,0 +1,9 @@ +mod company; +mod infrastructure; +mod region; +mod shared; + +pub use company::*; +pub use infrastructure::*; +pub use region::*; +pub use shared::*; diff --git a/crates/rrt-runtime/src/inspect/smp/services/model/region.rs b/crates/rrt-runtime/src/inspect/smp/services/model/region.rs new file mode 100644 index 0000000..876b507 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/model/region.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::services::{SmpServiceConsumerHypothesis, SmpServiceTraceBranchStatus}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRegionServiceTraceEntry { + pub name: String, + #[serde(default)] + pub profile_collection_count: Option, + pub policy_leading_f32_0_text: String, + pub policy_leading_f32_1_text: String, + pub policy_leading_f32_2_text: String, + #[serde(default)] + pub policy_reserved_dword_hex_words: Vec, + pub policy_trailing_word_hex: String, + #[serde(default)] + pub branches: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRegionServiceTraceReport { + pub profile_family: String, + #[serde(default)] + pub region_collection_header_present: bool, + #[serde(default)] + pub region_record_triplet_count: usize, + #[serde(default)] + pub queued_notice_record_count: usize, + #[serde(default)] + pub atlas_candidate_consumers: Vec, + #[serde(default)] + pub known_owner_bridge_fields: Vec, + #[serde(default)] + pub known_bridge_helpers: Vec, + #[serde(default)] + pub next_owner_questions: Vec, + #[serde(default)] + pub candidate_consumer_hypotheses: Vec, + #[serde(default)] + pub entries: Vec, + #[serde(default)] + pub notes: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/model/shared.rs b/crates/rrt-runtime/src/inspect/smp/services/model/shared.rs new file mode 100644 index 0000000..7f56969 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/model/shared.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpServiceTraceBranchStatus { + pub branch_name: String, + pub status: String, + #[serde(default)] + pub grounded_inputs: Vec, + #[serde(default)] + pub blocking_inputs: Vec, + #[serde(default)] + pub candidate_consumers: Vec, + #[serde(default)] + pub notes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPeriodicTriLaneSaveShapeFamilyCandidateSummaryEntry { + pub rank: usize, + pub shape_family_signature: String, + pub rows_offset_hex: String, + pub row_count: usize, + pub row_stride_hex: String, + #[serde(default)] + pub best_probable_density_lane_relative_offset_hex: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpServiceConsumerHypothesis { + pub label: String, + pub status: String, + #[serde(default)] + pub candidate_consumers: Vec, + #[serde(default)] + pub evidence: Vec, + #[serde(default)] + pub blockers: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/atlas.rs b/crates/rrt-runtime/src/inspect/smp/services/region/atlas.rs new file mode 100644 index 0000000..529be9d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/atlas.rs @@ -0,0 +1,60 @@ +pub(super) struct RegionAtlasFields { + pub(super) atlas_candidate_consumers: Vec, + pub(super) known_owner_bridge_fields: Vec, + pub(super) known_bridge_helpers: Vec, + pub(super) next_owner_questions: Vec, +} + +pub(super) fn build_region_atlas_fields() -> RegionAtlasFields { + RegionAtlasFields { + atlas_candidate_consumers: vec![ + "0x00422100 periodic class-0 region picker and queue seed owner".to_string(), + "0x004337c0 queued 0x20-byte notice-node append helper".to_string(), + "0x00437c00 queued-kind dispatch owner".to_string(), + "0x004c7520 kind-7 region-focused custom-modal owner".to_string(), + "0x004358d0 pending region bonus service owner".to_string(), + "0x00438710 recurring queued-notice service owner".to_string(), + "0x00420410 world_region_refresh_profile_availability_summary_bytes_0x2f6_0x2fa_0x2fe" + .to_string(), + "0x004204c0 world_region_refresh_profile_availability_display_strings_for_cached_selector_0x2f2" + .to_string(), + "0x00420030 / 0x00420280 city-connection peer probes".to_string(), + "0x0047efe0 placed-structure linked-company resolver".to_string(), + ], + known_owner_bridge_fields: vec![ + "[region+0x25e] pending-bonus severity/source lane".to_string(), + "[region+0x276] pending bonus amount".to_string(), + "[region+0x302] completion latch".to_string(), + "[region+0x316] one-shot fallback notice latch".to_string(), + "[region+0x356] localized region name".to_string(), + "[region+0x23a] world-scalar-backed region lane used in notices".to_string(), + ], + known_bridge_helpers: vec![ + "0x004207d0 city_site_format_connection_bonus_status_label".to_string(), + "0x00420030 city_connection_bonus_exists_matching_peer_site".to_string(), + "0x00420280 city_connection_bonus_select_first_matching_peer_site".to_string(), + "0x0047efe0 placed_structure_query_linked_company_id".to_string(), + "0x00480bb0 placed_structure_refresh_linked_site_display_name_and_route_anchor".to_string(), + "0x00420650 city-site local scalar refresh release-side companion".to_string(), + "0x00421510 world_region_collection_refresh_records_from_tagged_bundle".to_string(), + "0x0041f5c0 world_region_load_tagged_payload_and_profile_collection_0x37f".to_string(), + "0x00455fc0 shared region tagged-payload reload companion".to_string(), + "0x00455870 region triplet-band tagged restore callback (world-region vtable +0x48)" + .to_string(), + "0x00455930 region triplet-band tagged serializer callback (world-region vtable +0x4c)" + .to_string(), + "0x00420410 region profile availability-summary rebuild helper".to_string(), + "0x004204c0 region profile availability-display rebuild helper".to_string(), + "0x004cc930 selected-region severity/source editor helper".to_string(), + "0x00438150 fixed-region severity/source reseed owner".to_string(), + "0x00442cc0 fixed-region severity/source clamp owner".to_string(), + ], + next_owner_questions: vec![ + "Which restore seam re-seeds [region+0x25e] and clears [region+0x302/+0x316] before the grounded 0x00422100 -> 0x004358d0 producer/consumer cycle runs again?".to_string(), + "Which stable region id or class discriminator survives save/load strongly enough to drive 0x004358d0 after the class-0 raster/id rebuilds are ruled out?".to_string(), + "Which later global restore continuation after 0x00444887 rehydrates [region+0x2a4] and [region+0x310/+0x338/+0x360] once the 0x00421510 -> 0x0041f5c0 -> 0x00455fc0 path and the 0x00444b90 -> 0x00420560 per-region follow-on are both ruled down?".to_string(), + "Which post-load generation owner under 0x004384d0 actually republishes acquisition-side region lanes after world_load_saved_runtime_state_bundle returns: the 319 placed-structure replay strip, the 320 building setup strip, or the 321 economy-seeding tail?".to_string(), + "How far can the grounded 0x00420030/0x00420280 plus 0x0047efe0 connection chain be rehosted directly before the transient queued-notice family matters again?".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/entries.rs b/crates/rrt-runtime/src/inspect/smp/services/region/entries.rs new file mode 100644 index 0000000..346657f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/entries.rs @@ -0,0 +1,72 @@ +use crate::inspect::smp::services::SmpRegionServiceTraceEntry; +use crate::inspect::smp::services::build_service_trace_branch_status; +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; + +pub(super) fn build_region_service_trace_entries( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + analysis + .region_record_triplets + .as_ref() + .map(|probe| { + probe.entries + .iter() + .map(|entry| SmpRegionServiceTraceEntry { + name: entry.name.clone(), + profile_collection_count: entry + .profile_collection + .as_ref() + .map(|collection| collection.live_record_count), + policy_leading_f32_0_text: format!("{:.6}", entry.policy_leading_f32_0), + policy_leading_f32_1_text: format!("{:.6}", entry.policy_leading_f32_1), + policy_leading_f32_2_text: format!("{:.6}", entry.policy_leading_f32_2), + policy_reserved_dword_hex_words: entry + .policy_reserved_dwords + .iter() + .map(|word| format!("0x{word:08x}")) + .collect(), + policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(), + branches: vec![ + build_service_trace_branch_status( + "pending_bonus_queue_seed", + "blocked_missing_pending_bonus_owner_lane", + &[ + "region triplet envelope", + "embedded profile subcollection", + "policy float lanes", + ], + &[ + "[region+0x276] pending amount lane", + "[region+0x25e] severity/source lane", + ], + &[ + "0x00422100 periodic class-0 region picker and queue seed owner", + "0x004337c0 queued 0x20-byte notice-node append helper", + ], + &["The queued kind-7 notice family is not obviously persisted in ordinary saves, so the pending queue must be treated as transient until a direct owner seam is found."], + ), + build_service_trace_branch_status( + "city_connection_completion", + "blocked_missing_completion_and_one_shot_latches", + &[ + "region triplet envelope", + "region name stem", + ], + &[ + "[region+0x302] completion latch", + "[region+0x316] one-shot notice latch", + "stable region id or class discriminator", + ], + &[ + "0x004358d0 pending region bonus service owner", + "0x00420030 / 0x00420280 city-connection peer probes", + "0x0047efe0 placed-structure linked-company resolver", + ], + &["The remaining region blocker is a separate owner seam for the latches the city-connection branch reads and writes."], + ), + ], + }) + .collect::>() + }) + .unwrap_or_default() +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/entrypoints.rs b/crates/rrt-runtime/src/inspect/smp/services/region/entrypoints.rs new file mode 100644 index 0000000..7324ae5 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/entrypoints.rs @@ -0,0 +1,11 @@ +use super::build_region_service_trace_report; +use crate::inspect::smp::services::SmpRegionServiceTraceReport; +use crate::inspect::smp::world::inspect_save_company_and_chairman_analysis_file; +use std::path::Path; + +pub fn inspect_save_region_service_trace_file( + path: &Path, +) -> Result> { + let analysis = inspect_save_company_and_chairman_analysis_file(path)?; + Ok(build_region_service_trace_report(&analysis)) +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/later_restore.rs b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/later_restore.rs new file mode 100644 index 0000000..60d2cf3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/later_restore.rs @@ -0,0 +1,27 @@ +use crate::inspect::smp::services::SmpServiceConsumerHypothesis; + +pub(super) fn build_later_global_restore_hypothesis() -> SmpServiceConsumerHypothesis { + SmpServiceConsumerHypothesis { + label: "later global restore continuation".to_string(), + status: "next_global_restore_handoff_target".to_string(), + candidate_consumers: vec![ + "0x00444887 broader restore continuation after region refresh".to_string(), + "0x00487c20 territory_collection_refresh_records_from_tagged_bundle".to_string(), + "0x0040b5d0 support_collection_refresh_records_from_tagged_bundle".to_string(), + "0x00444b90 later per-region restore follow-on loop".to_string(), + "0x00420560 region profile-derived scalar refresh helper".to_string(), + ], + evidence: vec![ + "the checked-in region trace already grounds 0x00444887 as the first caller-side checkpoint above 0x00421510: it refreshes the region collection and then immediately advances into the territory and support collection refresh owners".to_string(), + "the neighboring atlas-backed restore symmetry already rules the territory side down somewhat too: 0x00487c20 only restores the live territory collection metadata/id family and its current per-entry slots 0x00487670/0x00487680 are still no-op load/save callbacks, so the territory leg now looks less likely than support or a later region-local rebuild to hide [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), + "the atlas-backed support seam is broader than a direct region payload owner too: 0x0040b5d0 sits over collection 0x0062b244, whose grounded live owners maintain goose-entry counters and neighboring world support lanes [world+0x4c9a/+0x4c9e/+0x4ca6/+0x4caa] plus selected support-entry state rather than an obvious per-region acquisition latch family".to_string(), + "the same grounded evidence already narrows the later per-region follow-on too: 0x00444b90 dispatches 0x00420560 over each live region after the broader restore continuation has already advanced".to_string(), + "direct disassembly already rules that per-region follow-on down as a latch owner: 0x00420560 only zeroes and recomputes [region+0x312] from the embedded profile collection [region+0x37f]/[region+0x383], revisits the linked placed-structure chain for class-mix terms, and lazily seeds the year-driven [region+0x317/+0x31b] band through 0x00420350, not [region+0x276/+0x302/+0x316]".to_string(), + "that leaves the broader 0x00444887 continuation as the next structured restore seam above the ruled-down 0x00421510 -> 0x0041f5c0 -> 0x00455fc0 path when chasing acquisition-side lanes [region+0x2a4] and [region+0x310/+0x338/+0x360]".to_string(), + ], + blockers: vec![ + "the concrete later-global restore owner that rehydrates [region+0x2a4] and [region+0x310/+0x338/+0x360] is still not grounded below the 0x00444887 continuation".to_string(), + "the later region-local rebuild now looks stronger than territory refresh 0x00487c20 or the broader support seam 0x0040b5d0, but the exact restore owner below the 0x00444887 continuation is still not grounded".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/mod.rs new file mode 100644 index 0000000..8816671 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/mod.rs @@ -0,0 +1,22 @@ +use crate::inspect::smp::services::SmpServiceConsumerHypothesis; +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; + +mod later_restore; +mod modal_dispatch; +mod pending_bonus; +mod periodic_producer; +mod post_load_generation; +mod tagged_load; + +pub(super) fn build_region_service_hypotheses( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + vec![ + pending_bonus::build_pending_bonus_hypothesis(analysis), + tagged_load::build_tagged_load_hypothesis(analysis), + periodic_producer::build_periodic_producer_hypothesis(), + later_restore::build_later_global_restore_hypothesis(), + post_load_generation::build_post_load_generation_hypothesis(), + modal_dispatch::build_modal_dispatch_hypothesis(), + ] +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/modal_dispatch.rs b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/modal_dispatch.rs new file mode 100644 index 0000000..82ae179 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/modal_dispatch.rs @@ -0,0 +1,19 @@ +use crate::inspect::smp::services::SmpServiceConsumerHypothesis; + +pub(super) fn build_modal_dispatch_hypothesis() -> SmpServiceConsumerHypothesis { + SmpServiceConsumerHypothesis { + label: "queued kind-7 modal dispatch path".to_string(), + status: "shell_adjacent_reference_only".to_string(), + candidate_consumers: vec![ + "0x00437c00 queued-kind dispatch owner".to_string(), + "0x004c7520 kind-7 region-focused custom-modal owner".to_string(), + ], + evidence: vec![ + "atlas already bounds this family as the shell-facing modal dispatch above the queued region id".to_string(), + "it is still useful as a reference owner for field identity, but not the first shellless rehost target".to_string(), + ], + blockers: vec![ + "full shell/dialog ownership remains out of scope".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/pending_bonus.rs b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/pending_bonus.rs new file mode 100644 index 0000000..9cccc02 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/pending_bonus.rs @@ -0,0 +1,32 @@ +use crate::inspect::smp::services::SmpServiceConsumerHypothesis; +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; + +pub(super) fn build_pending_bonus_hypothesis( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> SmpServiceConsumerHypothesis { + SmpServiceConsumerHypothesis { + label: "pending region bonus service path".to_string(), + status: if analysis.region_record_triplets.is_some() { + "highest_priority_static_mapping_target".to_string() + } else { + "possible_consumer_family".to_string() + }, + candidate_consumers: vec![ + "0x004358d0 pending region bonus service owner".to_string(), + "0x00420030 / 0x00420280 city-connection peer probes".to_string(), + "0x0047efe0 placed-structure linked-company resolver".to_string(), + ], + evidence: vec![ + "atlas already bounds this owner as the direct consumer of [region+0x276], [region+0x302], and [region+0x316]".to_string(), + "the new region trace already proves the record envelope and profile subcollection, so the remaining gap is the separate persisted latch seam rather than the service owner".to_string(), + "the neighboring region-profile summary/display strip is now grounded as rebuild-only follow-on work: 0x00420410 and 0x004204c0 walk the restored profile collection [region+0x37f], resolve backing candidates through 0x00412b70, and then rebuild [region+0x2f6/+0x2fa/+0x2fe] plus cached-selector display strings [region+0x2f2] from candidate bytes [candidate+0xba/+0xbb] rather than reading persisted region-owned copies".to_string(), + "direct disassembly now shows 0x004358d0 calling 0x00420030 twice plus 0x00420280, resolving the linked company through 0x0047efe0, posting company stat slot 4, and then clearing [region+0x276] while stamping [region+0x302] or [region+0x316]".to_string(), + "that same direct disassembly now also tightens the branch meaning: the linked-company branch formats the localized region-name notice from [region+0x356], posts it through 0x004554e0 and 0x0042a080, clears [region+0x276], and stamps [region+0x302]=1, while the fallback branch only runs when [region+0x316]==0 and then flips that one-shot latch to 1 before emitting its alternate notice".to_string(), + "the checked-in constructor owner 0x00421200 now also proves these latches are initialized locally at record construction time, which narrows the remaining gap to post-construction restore or rebuild rather than basic field identity".to_string(), + ], + blockers: vec![ + "restore seam that re-seeds [region+0x25e] and clears [region+0x302/+0x316] between service cycles".to_string(), + "stable region id or class discriminator".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/periodic_producer.rs b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/periodic_producer.rs new file mode 100644 index 0000000..a848b2d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/periodic_producer.rs @@ -0,0 +1,22 @@ +use crate::inspect::smp::services::SmpServiceConsumerHypothesis; + +pub(super) fn build_periodic_producer_hypothesis() -> SmpServiceConsumerHypothesis { + SmpServiceConsumerHypothesis { + label: "periodic producer and queued-notice path".to_string(), + status: "secondary_candidate_after_pending_service".to_string(), + candidate_consumers: vec![ + "0x00422100 periodic class-0 region picker and queue seed owner".to_string(), + "0x004337c0 queued 0x20-byte notice-node append helper".to_string(), + "0x00438710 recurring queued-notice service owner".to_string(), + ], + evidence: vec![ + "atlas ties these owners to the transient kind-7 queue family rooted at [world+0x66a6]".to_string(), + "grounded save probes now show that the ordinary-save queue family is not obviously persisted, so this looks more like runtime rebuild state than a direct save seam".to_string(), + "direct disassembly now shows 0x00422100 itself owning the pending-amount seed: it counts eligible class-0 regions with [region+0x276]==0 and [region+0x302]==0, samples one candidate, buckets [region+0x25e] against three thresholds, writes the resulting amount to [region+0x276], and then appends the kind-7 queued notice through 0x004337c0".to_string(), + ], + blockers: vec![ + "transient queue is not obviously persisted in ordinary saves".to_string(), + "needs the upstream restore seam for [region+0x25e/+0x302/+0x316] rather than more queue-side probing".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/post_load_generation.rs b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/post_load_generation.rs new file mode 100644 index 0000000..bbcc825 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/post_load_generation.rs @@ -0,0 +1,32 @@ +use crate::inspect::smp::services::SmpServiceConsumerHypothesis; + +pub(super) fn build_post_load_generation_hypothesis() -> SmpServiceConsumerHypothesis { + SmpServiceConsumerHypothesis { + label: "post-load generation pipeline handoff".to_string(), + status: "next_post_load_owner_family".to_string(), + candidate_consumers: vec![ + "0x004384d0 world_run_post_load_generation_pipeline".to_string(), + "0x004133b0 placed_structure_collection_refresh_local_runtime_records_and_position_scalars".to_string(), + "0x00421c20 world_region_collection_run_building_population_pass".to_string(), + "0x004235c0 world_region_balance_structure_demand_and_place_candidates".to_string(), + "0x00423d30 world_region_refresh_cached_category_totals_and_weight_slots".to_string(), + "0x00437b20 simulation_run_chunked_fast_forward_burst".to_string(), + ], + evidence: vec![ + "the checked-in shell-load subgraph and function map now place world_load_saved_runtime_state_bundle 0x00446d40 directly ahead of world_run_post_load_generation_pipeline 0x004384d0, so the first later non-hook owner family after the ruled-down 0x00444887 continuation is the post-load generation strip rather than another tagged region payload callback".to_string(), + "0x004384d0 already exposes the stage ordering tightly enough to subdivide the next search: id 319 refreshes the route-entry collection, auxiliary route trackers, and then 0x004133b0 placed-structure local-runtime replay; id 320 runs 0x00421c20(1.0, 1) for the region-owned building setup strip; id 321 runs 0x00437b20 and then sweeps regions through 0x00423d30".to_string(), + "direct disassembly now tightens the early 0x004384d0 setup strip too: before the conditional 320/321 gates it always runs 0x0044fb70 transport/pricing-grid setup and 0x0041ea50 candidate-local-service setup, and the extra arg-guarded 0x00421b60 -> 0x004882e0 default-region pair sits beside them as the last pre-320/321 setup branch. Those owners are therefore setup-side world-grid, candidate-table, and border-refresh work rather than the missing [region+0x2a4] / [region+0x310/+0x338/+0x360] republisher".to_string(), + "the neighboring 319 helpers are ruled down more tightly now too: 0x004377a0 stays on chairman-slot/profile materialization by normalizing the 16 slot bundles at [world+0x69da], republishing selector bytes into [0x006cec7c+0x87], populating the live chairman-profile collection 0x006ceb9c, and clearing selected company/chairman bytes [world+0x21/+0x25]; 0x004348e0 only gates the transient list at [world+0x66a6]; 0x00437c00 is that list's typed dispatcher over queue-node kind byte [node+0x08]; and the later 0x0044d410 calls are world-rect/grid refresh work. None of those helpers republish [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), + "the 319 placed-structure replay strip is now grounded as more than generic setup glue: 0x004133b0 drains queued site ids through 0x0040e450, sweeps every live placed structure through 0x0040ee10, and then reaches the already-grounded linked-site follow-on 0x00480710, which is a stronger structural bridge into acquisition-side site or peer state than the ruled-down territory/support loaders".to_string(), + "the surrounding 319 helpers are ruled down further now too: 0x00437220 and 0x004377a0 stay on chairman-slot selector/profile materialization over [world+0x69d8/+0x69db] and scenario selector bytes [0x006cec7c+0x87], while 0x00434d40 only seeds the subtype-2 candidate runtime latch [candidate+0x7b0] after the later burst".to_string(), + "the 320 building setup strip is narrower but still relevant: 0x00421c20 dispatches every live region into 0x004235c0, and that worker consults the region profile collection [region+0x37f], the placed-instance registry 0x0062b26c, and demand-balancing helpers like 0x00422900/0x00422be0/0x00422ee0, so current evidence keeps it in the same acquisition-adjacent owner family even though it is not yet a direct writer to [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), + "direct worker recovery now narrows 0x004235c0 further: it stays inside the live region demand-and-placement family by routing through 0x00422900 cached category accumulation, 0x004234e0 projected structure-count scalars, 0x00422be0 placed-count subtraction, and 0x00422ee0 placement attempts over 0x0062b26c rather than any restore-time field republisher".to_string(), + "the 321 economy-seeding tail is also now bounded as a narrower cache refresh rather than generic noise: 0x00437b20 only stages a fast-forward guard and then sweeps 0x0062bae0 through 0x00423d30, which refreshes the cached category band [region+0x27a/+0x27e/+0x282/+0x286], so it remains a weaker but still explicit post-load owner family to rule in or out before returning to the deeper 0x00446d40 continuation".to_string(), + "direct local disassembly now narrows 0x00423d30 as well: it only republishes [region+0x27a/+0x27e/+0x282/+0x286] through 0x00422900 after the 0x00437b20 burst and does not touch [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), + ], + blockers: vec![ + "current grounded evidence still does not show which post-load subphase actually republishes [region+0x2a4] or the cached tri-lane [region+0x310/+0x338/+0x360]".to_string(), + "0x00421c20 -> 0x004235c0 and 0x00437b20 -> 0x00423d30 are still grounded as region-side demand and cache refresh passes rather than direct restore writers, so the next static pass still needs the exact later field bridge".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/tagged_load.rs b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/tagged_load.rs new file mode 100644 index 0000000..b0544d3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/hypotheses/tagged_load.rs @@ -0,0 +1,46 @@ +use crate::inspect::smp::services::SmpServiceConsumerHypothesis; +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; + +pub(super) fn build_tagged_load_hypothesis( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> SmpServiceConsumerHypothesis { + SmpServiceConsumerHypothesis { + label: "region tagged-load restore path".to_string(), + status: if analysis.region_record_triplets.is_some() { + "parallel_static_mapping_target".to_string() + } else { + "possible_consumer_family".to_string() + }, + candidate_consumers: vec![ + "0x00421510 world_region_collection_refresh_records_from_tagged_bundle".to_string(), + "0x0041f5c0 world_region_load_tagged_payload_and_profile_collection_0x37f".to_string(), + "0x00455fc0 shared region tagged-payload reload companion".to_string(), + ], + evidence: vec![ + "the checked-in function map already grounds 0x00421510 as the tagged region-collection load owner that dispatches each live record through vtable slot +0x40".to_string(), + "the checked-in function map already grounds 0x0041f5c0 as the per-record load slot that reloads the tagged payload through 0x00455fc0 and then rebuilds profile collection [region+0x37f]".to_string(), + "constructor-side evidence now proves the latches are initialized locally, so the remaining gap can legitimately be framed as post-construction restore or rebuild".to_string(), + "direct disassembly of 0x0041f590/0x0041f5b0 now proves the world-region vtable root is 0x005c9a28, so the 0x00455fc0 dispatch at slot +0x48 lands on 0x00455870 and the serializer sibling at +0x4c lands on 0x00455930".to_string(), + "direct disassembly of 0x00455870/0x00455930 now shows that callback pair only restores and serializes two helper-local three-lane scalar bands: 0x00455870 reads six dwords through 0x531150 and forwards them to 0x530720 -> [helper+0x1e2/+0x1e6/+0x1ea] and 0x52e8b0 -> [helper+0x4b/+0x4f/+0x53], while 0x00455930 writes that same pair back through 0x531030; it still does not touch acquisition-side lanes [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), + "direct disassembly now tightens the rest of 0x00455fc0 too: after the +0x48 callback it only runs 0x0052ebd0 to read two one-byte generic flags through 0x531150 into base object bytes [this+0x20], [this+0x8d], [this+0x5c..+0x61], [this+0x1ee], [this+0x1fa], and [this+0x3e], then opens 0x55f3 only for span accounting before returning, so the missing region latches are not hiding in the remainder of 0x00455fc0 either".to_string(), + "direct disassembly of 0x00421510 now also shows the exact tagged loop shape: it opens 0x5209, reads 0x520a, iterates live entry ordinals through 0x518380/0x518140, seeds a stack-local world-region vtable helper through 0x0041f590/0x0041f5b0, dispatches each loaded record through slot +0x40, and only then closes 0x520b".to_string(), + "direct disassembly of 0x0041f5c0 now also shows its post-0x00455fc0 work is local to the profile collection path: it clamps [region+0x31b] back to 1.0f when needed, zeroes [region+0x317], allocates one 0x88-sized helper through 0x53b070/0x518b90, stores it at [region+0x37f], loads that helper through 0x518680, and clears [region+0x385] before returning".to_string(), + "the first caller-side checkpoint above 0x00421510 is now grounded too: 0x00444887 invokes the region collection refresh inside a broader restore sequence and then immediately advances to territory_collection_refresh_records_from_tagged_bundle 0x00487c20 and support_collection_refresh_records_from_tagged_bundle 0x0040b5d0, which makes the missing latches look like a later global rebuild seam rather than hidden work inside 0x00421510 itself".to_string(), + "direct disassembly now rules down the next 0x00444887 continuation branch too: after the region, territory, and support refresh owners, 0x00433130 only opens 0x4e99/0x4e9a, repopulates live event collection 0x0062be18 through 0x0042db20, and closes 0x4e9b, so that event-side loader is not the missing later region restore handoff for [region+0x2a4] or [region+0x310/+0x338/+0x360] either".to_string(), + "that broader restore strip now also has one grounded later region-local sweep: at 0x00444b08 it re-enters the live region collection through 0x00421ce0, which walks live records via 0x518380/0x518140 and dispatches 0x0041fb00 per record".to_string(), + "the checked-in atlas already grounds 0x0041fb00 as the class-0-only default-region helper under the same family, and 0x00421730 as the later raster finalizer that repopulates [world+0x212d] from class-0 region ids".to_string(), + "direct disassembly now tightens that later sweep too: 0x0041fb00 exits immediately for nonzero [region+0x23e], while 0x00421730 clears the per-cell region word at [world+0x212d]+cell*4+1, seeds cached bounds-like fields [region+0x242/+0x246/+0x24a/+0x24e/+0x252], and only then enters the class-0 path that consumes [region+0x256] and the coordinate helpers 0x00455800/0x00455810".to_string(), + "the companion region-set root is runtime-owned now too: direct disassembly of the broader bring-up strip at 0x00448740..0x0044881f shows 0x006cfc9c being allocated through 0x53b070 and constructed through 0x00487bd0 before later rebuild passes run, so the 0x00487650/0x004881b0 companion path is operating on a runtime-owned helper collection rather than a hidden save-owned latch seam".to_string(), + "the later restore-band siblings are tighter now too: 0x00487de0 clears the prior chunked border queues through 0x00533cf0, builds a small per-region id map from [region+0x00]/[region+0x35] keyed by class [region+0x31], scans the live world raster at [world+0x2131], and appends fresh border-segment rows through 0x00536ea0 without touching [region+0x25e/+0x276/+0x302/+0x316]".to_string(), + "the neighboring world-grid reseed 0x0044c4b0 is tighter now too: it clears bit 0x10 across the live grid byte at cell offset +0xe6, then walks the live region collection at 0x0062bae0, admits only class-0 records via [region+0x23e], resolves one representative center cell through 0x00455f60, and marks that same bit back on, which again reads as raster presentation state rather than pending/completion latch restore".to_string(), + "the companion region-set rebuild above that border pass is narrower now too: 0x00487650 is only a small constructor wrapper over 0x00487540 that seeds [region+0x00] from the caller-supplied id, while 0x004881b0 rebuilds [region+0x3d] from the world raster histogram, zeroes [region+0x41], folds class-0 children back into parent [region+0x41] through [region+0x35], and then tails into the border emitter on 0x006cfc9c via 0x00487de0".to_string(), + "the later class-0 batch at 0x00438087 is narrower now too: it walks live class-0 regions through 0x0062bae0, scales the mirrored severity/source pair [region+0x25a/+0x25e] from the current value using world-side factors, clamps the result, and then hands the whole collection to 0x00421c20; it never reads or writes [region+0x276/+0x302/+0x316]".to_string(), + "the follow-on 0x00421c20 is now bounded as a parameterized region-collection helper rather than a latch owner: it loops the same collection with caller-provided scalar arguments, dispatches each record through 0x004235c0, and never touches the pending/completion/one-shot lanes directly".to_string(), + "the subsequent world follow-ons are narrower too: 0x00437b20 only stages one world-side reentry guard at [world+0x46c38], iterates the live region collection through 0x00423d30, and then tails into 0x00434d40, while 0x00437220 rebuilds broader world byte-set state around [world+0x66be/+0x69db] and other global collections. Those branches are still broader runtime follow-ons, not direct owners of [region+0x276/+0x302/+0x316]".to_string(), + ], + blockers: vec![ + "which later restore or rebuild owner rehydrates [region+0x276/+0x302/+0x316] after the 0x00421510 -> 0x0041f5c0 -> 0x00455fc0 path completes".to_string(), + "whether [region+0x25e] severity/source and any stable region id/class discriminator are serialized elsewhere in the tagged region body or recomputed later by another post-load owner after the 0x00421ce0 -> 0x0041fb00 -> 0x00421730 class-0 raster/id sweep, 0x004881b0 companion cell-count rebuild, 0x00487de0 border rebuild, 0x0044c4b0 center-cell reseed, the 0x00438087 mirrored severity/source batch, and the 0x00421c20 -> 0x004235c0 follow-on helper loop are all ruled out as latch owners".to_string(), + ], + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/mod.rs b/crates/rrt-runtime/src/inspect/smp/services/region/mod.rs new file mode 100644 index 0000000..5e2e587 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/mod.rs @@ -0,0 +1,16 @@ +mod atlas; +mod entries; +mod entrypoints; +mod hypotheses; +mod notes; +mod report; + +use crate::inspect::smp::services::SmpRegionServiceTraceReport; +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; +pub use entrypoints::inspect_save_region_service_trace_file; + +pub(in crate::inspect::smp) fn build_region_service_trace_report( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> SmpRegionServiceTraceReport { + report::build_region_service_trace_report(analysis) +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/notes.rs b/crates/rrt-runtime/src/inspect/smp/services/region/notes.rs new file mode 100644 index 0000000..a423827 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/notes.rs @@ -0,0 +1,84 @@ +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; +use std::collections::BTreeSet; + +pub(super) fn build_region_service_trace_notes( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + let mut notes = Vec::new(); + notes.push( + "Region service trace treats the queued kind-7 notice family as transient runtime state until a persisted owner seam is found.".to_string(), + ); + notes.push( + "Direct disassembly now grounds the core producer/consumer pair itself: 0x00422100 seeds [region+0x276] from the severity/source lane [region+0x25e] and appends the kind-7 notice through 0x004337c0, while 0x004358d0 consumes that amount, runs the city-connection peer probes 0x00420030/0x00420280 plus the linked-company resolver 0x0047efe0, and then stamps [region+0x302] or [region+0x316].".to_string(), + ); + notes.push( + "Direct disassembly now also tightens the severity/source side itself: 0x004cc930 is a selected-region editor helper that writes [region+0x25a] and [region+0x25e] together from one integer input, while 0x00438150 and 0x00442cc0 are fixed-region global reseed/clamp owners over collection 0x0062bae0 that adjust the same mirrored pair for hardcoded region ids.".to_string(), + ); + notes.push( + "Two more apparent offset hits are now ruled out as region false leads: 0x0043a5a0 is a separate constructor under vtable root 0x005ca078 that zeroes its own [this+0x302/+0x316] fields during local object setup, and 0x0045c460/0x0045c8xx is a separate vtable-0x005cb5e8 helper family whose [this+0x316] is a child-array pointer serialized through 0x61a9/0x61aa/0x61ab rather than a region latch.".to_string(), + ); + notes.push( + "A direct-writer census now narrows the remaining literal offset path further: the other `0x302/0x316` writer bands at 0x0043dd45/0x0043de19/0x0043e0a7/0x0043f5bc all hang off the same non-region 0x005ca078 object family as 0x0043a5a0 through helpers 0x0043af60/0x0043b030, so the only grounded region-owned literal writes left are the constructor 0x00421200 plus the producer/consumer pair 0x00422100 and 0x004358d0.".to_string(), + ); + notes.push( + "The later post-load per-region sweep is ruled down further now too: in the 0x00444887 restore strip, the follow-on loop at 0x00444b90 dispatches 0x00420560 over each live region, but that helper only zeroes and recomputes [region+0x312] from the embedded profile collection [region+0x37f]/[region+0x383] and lazily seeds the year-driven [region+0x317/+0x31b] band through 0x00420350, not [region+0x276/+0x302/+0x316].".to_string(), + ); + notes.push( + "The immediate profile-summary/display strip is ruled onto the rebuild side too: 0x00420410 rebuilds summary dwords [region+0x2f6/+0x2fa/+0x2fe] and 0x004204c0 refreshes cached-selector display strings [region+0x2f2] by walking the restored profile collection [region+0x37f], resolving backing candidates through 0x00412b70, and consuming candidate bytes [candidate+0xba/+0xbb]. Those bytes are therefore consumer-side summaries, not hidden persisted region lanes.".to_string(), + ); + notes.push( + "The current region seam is strong enough to prove record-envelope ownership, profile subcollection ownership, and the absence of hidden 0x55f3 tail padding on grounded saves.".to_string(), + ); + if let Some(probe) = analysis.region_record_triplets.as_ref() { + let mut trailing_words = probe + .entries + .iter() + .map(|entry| entry.policy_trailing_word_hex.clone()) + .collect::>(); + trailing_words.sort(); + trailing_words.dedup(); + let preview = trailing_words + .iter() + .take(4) + .cloned() + .collect::>() + .join(", "); + notes.push(format!( + "Region 0x55f2 trailing-word candidates currently collapse to {} distinct value(s): {}.", + trailing_words.len(), + if preview.is_empty() { + "none".to_string() + } else { + preview + } + )); + if probe.entries.iter().all(|entry| { + entry.policy_reserved_dwords.iter().all(|word| *word == 0) + && entry.policy_trailing_word == 1 + }) { + notes.push( + "Grounded region 0x55f2 fixed-policy chunks also keep all three reserved dwords at zero while the trailing word stays 0x0001, so that chunk is not currently carrying the missing latch/id discriminator." + .to_string(), + ); + } + let pre_name_prefix_count = probe + .entries + .iter() + .filter(|entry| entry.pre_name_prefix_len != 0) + .count(); + let unique_pre_name_prefix_lens = probe + .entries + .iter() + .map(|entry| entry.pre_name_prefix_len) + .collect::>() + .into_iter() + .collect::>(); + notes.push(format!( + "Grounded live-entry payload starts now also show {} of {} region records with nonzero bytes before the first 0x55f1 tag; unique pre-name prefix lengths are {:?}.", + pre_name_prefix_count, + probe.entries.len(), + unique_pre_name_prefix_lens + )); + } + notes +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/region/report.rs b/crates/rrt-runtime/src/inspect/smp/services/region/report.rs new file mode 100644 index 0000000..20d17b4 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/region/report.rs @@ -0,0 +1,36 @@ +use super::atlas::build_region_atlas_fields; +use super::entries::build_region_service_trace_entries; +use super::hypotheses::build_region_service_hypotheses; +use super::notes::build_region_service_trace_notes; +use crate::inspect::smp::services::SmpRegionServiceTraceReport; +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; + +pub(in crate::inspect::smp) fn build_region_service_trace_report( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> SmpRegionServiceTraceReport { + let atlas = build_region_atlas_fields(); + let candidate_consumer_hypotheses = build_region_service_hypotheses(analysis); + let entries = build_region_service_trace_entries(analysis); + let notes = build_region_service_trace_notes(analysis); + SmpRegionServiceTraceReport { + profile_family: analysis.profile_family.clone(), + region_collection_header_present: analysis.region_collection_header.is_some(), + region_record_triplet_count: analysis + .region_record_triplets + .as_ref() + .map(|probe| probe.record_count) + .unwrap_or_default(), + queued_notice_record_count: analysis + .region_queued_notice_records + .as_ref() + .map(|probe| probe.entries.len()) + .unwrap_or_default(), + atlas_candidate_consumers: atlas.atlas_candidate_consumers, + known_owner_bridge_fields: atlas.known_owner_bridge_fields, + known_bridge_helpers: atlas.known_bridge_helpers, + next_owner_questions: atlas.next_owner_questions, + candidate_consumer_hypotheses, + entries, + notes, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/services/shared.rs b/crates/rrt-runtime/src/inspect/smp/services/shared.rs new file mode 100644 index 0000000..e8dc4f8 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/services/shared.rs @@ -0,0 +1,419 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use crate::inspect::smp::services::*; +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; + +pub(in crate::inspect::smp) fn build_service_trace_branch_status( + branch_name: &str, + status: &str, + grounded_inputs: &[&str], + blocking_inputs: &[&str], + candidate_consumers: &[&str], + notes: &[&str], +) -> SmpServiceTraceBranchStatus { + SmpServiceTraceBranchStatus { + branch_name: branch_name.to_string(), + status: status.to_string(), + grounded_inputs: grounded_inputs + .iter() + .map(|value| value.to_string()) + .collect(), + blocking_inputs: blocking_inputs + .iter() + .map(|value| value.to_string()) + .collect(), + candidate_consumers: candidate_consumers + .iter() + .map(|value| value.to_string()) + .collect(), + notes: notes.iter().map(|value| value.to_string()).collect(), + } +} + +pub(in crate::inspect::smp) fn summarize_peer_site_selector_candidate_saved_payloads( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { + return Vec::new(); + }; + let mut grouped = + BTreeMap::<(String, String), (usize, BTreeSet, BTreeSet)>::new(); + for entry in &triplets.entries { + let grouped_entry = grouped + .entry(( + entry.profile_payload_dword_hex.clone(), + entry.profile_status_kind.clone(), + )) + .or_insert_with(|| (0, BTreeSet::new(), BTreeSet::new())); + grouped_entry.0 += 1; + if grouped_entry.1.len() < 4 { + grouped_entry.1.insert(entry.primary_name.clone()); + } + if grouped_entry.2.len() < 4 { + grouped_entry.2.insert(entry.secondary_name.clone()); + } + } + let mut summaries = grouped + .into_iter() + .map( + |( + (profile_payload_dword_hex, profile_status_kind), + (count, primary_names, secondary_names), + )| { + SmpSavePlacedStructureProfilePayloadSummaryEntry { + profile_payload_dword_hex, + profile_status_kind, + count, + sample_primary_names: primary_names.into_iter().collect(), + sample_secondary_names: secondary_names.into_iter().collect(), + } + }, + ) + .collect::>(); + summaries.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| { + left.profile_payload_dword_hex + .cmp(&right.profile_payload_dword_hex) + }) + .then_with(|| left.profile_status_kind.cmp(&right.profile_status_kind)) + }); + summaries.truncate(8); + summaries +} + +pub(in crate::inspect::smp) fn summarize_peer_site_selector_candidate_saved_payload_deltas( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { + return Vec::new(); + }; + let mut payload_values = triplets + .entries + .iter() + .map(|entry| entry.profile_payload_dword) + .collect::>(); + payload_values.sort_unstable(); + payload_values.dedup(); + let mut delta_counts = BTreeMap::::new(); + for window in payload_values.windows(2) { + let delta = window[1].wrapping_sub(window[0]); + *delta_counts.entry(delta).or_insert(0) += 1; + } + let mut summaries = delta_counts + .into_iter() + .map( + |(delta, count)| SmpSavePlacedStructureProfilePayloadDeltaSummaryEntry { + delta_hex: format!("0x{delta:08x}"), + count, + }, + ) + .collect::>(); + summaries.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.delta_hex.cmp(&right.delta_hex)) + }); + summaries.truncate(6); + summaries +} + +pub(in crate::inspect::smp) fn summarize_peer_site_selector_candidate_saved_footer_padding( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { + return Vec::new(); + }; + let mut grouped = BTreeMap::)>::new(); + for entry in &triplets.entries { + let grouped_entry = grouped + .entry(entry.profile_pre_footer_padding_len) + .or_insert_with(|| (0, BTreeSet::new())); + grouped_entry.0 += 1; + if grouped_entry.1.len() < 4 { + grouped_entry + .1 + .insert(entry.profile_pre_footer_padding_hex_bytes.join(",")); + } + } + let mut summaries = grouped + .into_iter() + .map(|(padding_len, (count, sample_hex_bytes))| { + SmpSavePlacedStructureProfileFooterPaddingSummaryEntry { + padding_len, + count, + sample_hex_bytes: sample_hex_bytes.into_iter().collect(), + } + }) + .collect::>(); + summaries.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.padding_len.cmp(&right.padding_len)) + }); + summaries.truncate(6); + summaries +} + +pub(in crate::inspect::smp) fn summarize_peer_site_selector_candidate_saved_companion_bytes( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { + return Vec::new(); + }; + let mut grouped = BTreeMap::, BTreeSet)>::new(); + for entry in &triplets.entries { + let Some(companion_byte_hex) = entry.profile_companion_byte_hex.as_ref() else { + continue; + }; + let grouped_entry = grouped + .entry(companion_byte_hex.clone()) + .or_insert_with(|| (0, BTreeSet::new(), BTreeSet::new())); + grouped_entry.0 += 1; + if grouped_entry.1.len() < 4 { + grouped_entry.1.insert(entry.primary_name.clone()); + } + if grouped_entry.2.len() < 4 { + grouped_entry.2.insert(entry.secondary_name.clone()); + } + } + let mut summaries = grouped + .into_iter() + .map( + |(companion_byte_hex, (count, primary_names, secondary_names))| { + SmpSavePlacedStructureProfileCompanionByteSummaryEntry { + companion_byte_hex, + count, + sample_primary_names: primary_names.into_iter().collect(), + sample_secondary_names: secondary_names.into_iter().collect(), + } + }, + ) + .collect::>(); + summaries.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.companion_byte_hex.cmp(&right.companion_byte_hex)) + }); + summaries.truncate(8); + summaries +} + +pub(in crate::inspect::smp) fn summarize_peer_site_selector_candidate_saved_policy_trailing_words( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { + return Vec::new(); + }; + let mut grouped = BTreeMap::::new(); + for entry in &triplets.entries { + *grouped + .entry(entry.policy_trailing_word_hex.clone()) + .or_insert(0) += 1; + } + let mut summaries = grouped + .into_iter() + .map(|(policy_trailing_word_hex, count)| { + SmpSavePlacedStructurePolicyTrailingWordSummaryEntry { + policy_trailing_word_hex, + count, + } + }) + .collect::>(); + summaries.sort_by(|left, right| { + right.count.cmp(&left.count).then_with(|| { + left.policy_trailing_word_hex + .cmp(&right.policy_trailing_word_hex) + }) + }); + summaries.truncate(8); + summaries +} + +pub(in crate::inspect::smp) fn summarize_peer_site_selector_candidate_saved_nonzero_companion_name_pairs( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { + return Vec::new(); + }; + let mut grouped = BTreeMap::<(String, String, String), usize>::new(); + for entry in &triplets.entries { + let Some(companion_byte_hex) = entry.profile_companion_byte_hex.as_ref() else { + continue; + }; + if companion_byte_hex == "0x00" { + continue; + } + *grouped + .entry(( + companion_byte_hex.clone(), + entry.primary_name.clone(), + entry.secondary_name.clone(), + )) + .or_insert(0) += 1; + } + let mut summaries = grouped + .into_iter() + .map( + |((companion_byte_hex, primary_name, secondary_name), count)| { + SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { + companion_byte_hex, + primary_name, + secondary_name, + count, + } + }, + ) + .collect::>(); + summaries.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.companion_byte_hex.cmp(&right.companion_byte_hex)) + .then_with(|| left.primary_name.cmp(&right.primary_name)) + .then_with(|| left.secondary_name.cmp(&right.secondary_name)) + }); + summaries.truncate(10); + summaries +} + +pub(in crate::inspect::smp) fn summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_overlaps( + summaries: &[SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry], +) -> Vec { + let nonzero_stock_family_aliases = nonzero_stock_building_header_family_aliases(); + let mut overlaps = summaries + .iter() + .filter_map(|entry| { + let primary_matches = nonzero_stock_family_aliases + .contains(&canonicalize_building_like_name(&entry.primary_name)); + let secondary_matches = nonzero_stock_family_aliases + .contains(&canonicalize_building_like_name(&entry.secondary_name)); + if !(primary_matches || secondary_matches) { + return None; + } + Some( + SmpSavePlacedStructureNonzeroCompanionBuildingFamilyOverlapSummaryEntry { + companion_byte_hex: entry.companion_byte_hex.clone(), + primary_name: entry.primary_name.clone(), + secondary_name: entry.secondary_name.clone(), + count: entry.count, + primary_matches_nonzero_stock_building_header_family: primary_matches, + secondary_matches_nonzero_stock_building_header_family: secondary_matches, + }, + ) + }) + .collect::>(); + overlaps.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.companion_byte_hex.cmp(&right.companion_byte_hex)) + .then_with(|| left.primary_name.cmp(&right.primary_name)) + .then_with(|| left.secondary_name.cmp(&right.secondary_name)) + }); + overlaps.truncate(10); + overlaps +} + +pub(in crate::inspect::smp) fn summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_residues( + summaries: &[SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry], +) -> Vec { + let nonzero_stock_family_aliases = nonzero_stock_building_header_family_aliases(); + let mut residues = summaries + .iter() + .filter(|entry| { + !nonzero_stock_family_aliases + .contains(&canonicalize_building_like_name(&entry.primary_name)) + && !nonzero_stock_family_aliases + .contains(&canonicalize_building_like_name(&entry.secondary_name)) + }) + .map( + |entry| SmpSavePlacedStructureNonzeroCompanionBuildingFamilyResidueSummaryEntry { + companion_byte_hex: entry.companion_byte_hex.clone(), + primary_name: entry.primary_name.clone(), + secondary_name: entry.secondary_name.clone(), + count: entry.count, + }, + ) + .collect::>(); + residues.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.companion_byte_hex.cmp(&right.companion_byte_hex)) + .then_with(|| left.primary_name.cmp(&right.primary_name)) + .then_with(|| left.secondary_name.cmp(&right.secondary_name)) + }); + residues.truncate(10); + residues +} + +pub(in crate::inspect::smp) fn nonzero_stock_building_header_family_aliases() -> BTreeSet { + [ + "Brewery", + "ConcretePlant", + "ConstructionFirm", + "DairyProcessor", + "Distillery", + "ElectronicsPlant", + "Furnace", + "FurnitureFactory", + "Hospital", + "Lumbermill", + "MachineShop", + "MeatPackingPlant", + "Museum", + "PaperMill", + "PharmaceuticalPlant", + "Port", + "Recycling Plant", + "Steel Mill", + "Textile Mill", + "TextileMill", + "Tire Factory", + "Tool and Die", + "Toolndie", + "Warehouse", + ] + .into_iter() + .map(canonicalize_building_like_name) + .collect() +} + +pub(in crate::inspect::smp) fn canonicalize_building_like_name(name: &str) -> String { + name.chars() + .filter(|ch| !matches!(ch, ' ' | '_' | '-')) + .flat_map(|ch| ch.to_lowercase()) + .collect() +} + +pub(in crate::inspect::smp) fn summarize_near_city_acquisition_tri_lane_save_shape_family_candidates( + analysis: &SmpSaveCompanyChairmanAnalysisReport, +) -> Vec { + let Some(probe) = analysis.region_fixed_row_run_candidates.as_ref() else { + return Vec::new(); + }; + probe + .candidates + .iter() + .take(5) + .enumerate() + .map( + |(index, candidate)| SmpPeriodicTriLaneSaveShapeFamilyCandidateSummaryEntry { + rank: index + 1, + shape_family_signature: candidate.shape_family_signature.clone(), + rows_offset_hex: candidate.rows_offset_hex.clone(), + row_count: candidate.row_count, + row_stride_hex: candidate.row_stride_hex.clone(), + best_probable_density_lane_relative_offset_hex: candidate + .best_probable_density_lane_relative_offset_hex + .clone(), + }, + ) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/special_conditions/aligned_band.rs b/crates/rrt-runtime/src/inspect/smp/special_conditions/aligned_band.rs new file mode 100644 index 0000000..2336789 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/special_conditions/aligned_band.rs @@ -0,0 +1,174 @@ +use crate::inspect::smp::special_conditions::*; + +pub(in crate::inspect::smp) fn parse_smp_aligned_runtime_rule_band_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + special_conditions_probe: Option<&SmpSpecialConditionsProbe>, +) -> Option { + special_conditions_probe?; + if SMP_ALIGNED_RUNTIME_RULE_END_OFFSET > bytes.len() { + return None; + } + + let source_kind = match file_extension_hint.unwrap_or("") { + "gmp" => "map-smp-aligned-runtime-rule-band", + "gms" => "save-smp-aligned-runtime-rule-band", + "gmx" => "sandbox-smp-aligned-runtime-rule-band", + _ => "smp-aligned-runtime-rule-band", + } + .to_string(); + let profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()); + + let mut nonzero_lanes = Vec::new(); + for band_index in 0..SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT { + let absolute_offset = SPECIAL_CONDITIONS_OFFSET + band_index * 4; + let value = read_u32_at(bytes, absolute_offset)?; + if value == 0 { + continue; + } + let lane_kind = 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" + } + .to_string(); + let known_label = if band_index < SPECIAL_CONDITION_COUNT { + Some( + KNOWN_SPECIAL_CONDITION_DEFINITIONS[band_index] + .label + .to_string(), + ) + } else { + None + }; + nonzero_lanes.push(SmpAlignedRuntimeRuleBandLane { + band_index, + absolute_offset, + relative_offset: absolute_offset - SPECIAL_CONDITIONS_OFFSET, + absolute_offset_hex: format!("0x{absolute_offset:04x}"), + relative_offset_hex: format!("0x{:x}", absolute_offset - SPECIAL_CONDITIONS_OFFSET), + lane_kind, + known_label, + value, + value_hex: format!("0x{value:08x}"), + probable_f32_le: probable_normal_f32_string(value), + }); + } + + let nonzero_band_indices = nonzero_lanes + .iter() + .map(|lane| lane.band_index) + .collect::>(); + let nonzero_post_window_overlap_band_indices = nonzero_lanes + .iter() + .filter(|lane| { + lane.band_index >= SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX + && lane.band_index + < SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX + + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT + }) + .map(|lane| lane.band_index) + .collect::>(); + let nonzero_post_window_overlap_post_relative_offset_hexes = nonzero_lanes + .iter() + .filter(|lane| { + lane.band_index >= SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX + && lane.band_index + < SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX + + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT + }) + .map(|lane| { + format!( + "0x{:x}", + (lane.band_index - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX) * 4 + ) + }) + .collect::>(); + let nonzero_relative_offset_hexes = nonzero_lanes + .iter() + .map(|lane| lane.relative_offset_hex.clone()) + .collect::>(); + let mut evidence = vec![ + format!( + "fixed `.smp`-aligned runtime-rule band at 0x{SPECIAL_CONDITIONS_OFFSET:04x}..0x{SMP_ALIGNED_RUNTIME_RULE_END_OFFSET:04x}" + ), + format!( + "band spans {} known editor rule dwords plus one trailing scalar", + SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT + ), + "first 36 dwords overlap the older fixed matrix probe rooted at 0x0d64".to_string(), + format!( + "trailing band indices {}..{} alias the leading post-sentinel window offsets {}..{}", + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX, + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX + + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT + - 1, + "0x00", + format!( + "0x{:x}", + (SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - 1) * 4 + ) + ), + "band matches the grounded `.smp` save/load copy into `[world+0x4a7f..+0x4b43]`" + .to_string(), + ]; + if nonzero_lanes.is_empty() { + evidence + .push("all dwords in the aligned runtime-rule band are zero for this file".to_string()); + } else { + evidence.push(format!( + "observed {} nonzero lanes at band indices {:?}", + nonzero_lanes.len(), + nonzero_band_indices + )); + if !nonzero_post_window_overlap_band_indices.is_empty() { + evidence.push(format!( + "nonzero overlap lanes mirror post-window offsets {:?}", + nonzero_post_window_overlap_post_relative_offset_hexes + )); + } + } + + Some(SmpAlignedRuntimeRuleBandProbe { + profile_family, + source_kind, + band_offset: SPECIAL_CONDITIONS_OFFSET, + band_end_offset: SMP_ALIGNED_RUNTIME_RULE_END_OFFSET, + band_len: SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - SPECIAL_CONDITIONS_OFFSET, + band_len_hex: format!( + "0x{:x}", + SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - SPECIAL_CONDITIONS_OFFSET + ), + dword_count: SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT, + known_editor_rule_dword_count: SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT, + trailing_scalar_index: SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1, + trailing_scalar_offset: SPECIAL_CONDITIONS_OFFSET + + (SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1) * 4, + trailing_scalar_offset_hex: format!( + "0x{:04x}", + SPECIAL_CONDITIONS_OFFSET + (SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1) * 4 + ), + post_window_overlap_start_index: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX, + post_window_overlap_dword_count: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT, + post_window_overlap_end_index: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX + + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT + - 1, + post_window_overlap_post_relative_offset_start_hex: "0x0".to_string(), + post_window_overlap_post_relative_offset_end_hex: format!( + "0x{:x}", + (SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - 1) * 4 + ), + nonzero_post_window_overlap_band_indices, + nonzero_post_window_overlap_post_relative_offset_hexes, + nonzero_lane_count: nonzero_lanes.len(), + nonzero_band_indices, + nonzero_relative_offset_hexes, + nonzero_lanes, + evidence, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/special_conditions/locomotive_policy.rs b/crates/rrt-runtime/src/inspect/smp/special_conditions/locomotive_policy.rs new file mode 100644 index 0000000..e76e593 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/special_conditions/locomotive_policy.rs @@ -0,0 +1,182 @@ +use crate::inspect::smp::special_conditions::*; + +pub(in crate::inspect::smp) fn parse_locomotive_policy_neighborhood_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + special_conditions_probe: Option<&SmpSpecialConditionsProbe>, +) -> Option { + special_conditions_probe?; + if LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET > bytes.len() { + return None; + } + + let profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let source_kind = match file_extension_hint.unwrap_or("") { + "gmp" => "locomotive-policy-neighborhood", + "gms" => "locomotive-policy-neighborhood", + "gmx" => "locomotive-policy-neighborhood", + _ => "locomotive-policy-neighborhood", + } + .to_string(); + + let exact_fields = [ + ( + "selected-year bucket companion scalar", + LOCOMOTIVE_POLICY_FIELD_NEG3_RUNTIME_OBJECT_OFFSET, + 0x0f87usize, + 4usize, + ), + ( + "startup-dispatch reset-owned band at +0x4cae", + LOCOMOTIVE_POLICY_FIELD_NEG2_RUNTIME_OBJECT_OFFSET, + 0x0f93usize, + 4usize, + ), + ( + "startup-dispatch reset-owned band at +0x4cb2", + LOCOMOTIVE_POLICY_FIELD_NEG1_RUNTIME_OBJECT_OFFSET, + 0x0f97usize, + 4usize, + ), + ( + "linked-site removal follow-on gate", + LOCOMOTIVE_POLICY_FIELD_0_RUNTIME_OBJECT_OFFSET, + 0x0f78usize, + 1usize, + ), + ( + "All Steam Locos Avail.", + LOCOMOTIVE_POLICY_FIELD_1_RUNTIME_OBJECT_OFFSET, + 0x0f7cusize, + 1usize, + ), + ( + "All Diesel Locos Avail.", + LOCOMOTIVE_POLICY_FIELD_2_RUNTIME_OBJECT_OFFSET, + 0x0f7dusize, + 1usize, + ), + ( + "All Electric Locos Avail.", + LOCOMOTIVE_POLICY_FIELD_3_RUNTIME_OBJECT_OFFSET, + 0x0f7eusize, + 1usize, + ), + ( + "station-list selected station id", + LOCOMOTIVE_POLICY_FIELD_4_RUNTIME_OBJECT_OFFSET, + 0x0f9fusize, + 4usize, + ), + ( + "cached available-locomotive rating", + LOCOMOTIVE_POLICY_FIELD_5_RUNTIME_OBJECT_OFFSET, + 0x0fa3usize, + 4usize, + ), + ]; + + let grounded_field_observations = exact_fields + .iter() + .map( + |(field_name, runtime_object_offset, file_offset, field_width_bytes)| { + let raw = &bytes[*file_offset..*file_offset + *field_width_bytes]; + let raw_hex = hex_encode(raw); + let (value_u8, value_u8_hex, value_u32, value_u32_hex, probable_f32_le) = + if *field_width_bytes == 1 { + let value = raw[0]; + ( + Some(value), + Some(format!("0x{value:02x}")), + None, + None, + None, + ) + } else { + let value = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); + ( + None, + None, + Some(value), + Some(format!("0x{value:08x}")), + probable_normal_f32_string(value), + ) + }; + SmpLocomotivePolicyFieldObservation { + field_name: (*field_name).to_string(), + runtime_object_offset: *runtime_object_offset, + runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"), + file_offset: *file_offset, + file_offset_hex: format!("0x{file_offset:04x}"), + field_width_bytes: *field_width_bytes, + field_width_bytes_hex: format!("0x{field_width_bytes:x}"), + raw_hex, + value_u8, + value_u8_hex, + value_u32, + value_u32_hex, + probable_f32_le, + } + }, + ) + .collect::>(); + + let three_byte_early_float_candidates = exact_fields + .iter() + .filter(|(_, _, _, width)| *width == 4usize) + .filter_map(|(field_name, runtime_object_offset, file_offset, _)| { + let candidate_offset = file_offset.saturating_sub(3); + let value = read_u32_at(bytes, candidate_offset)?; + let probable_f32_le = probable_normal_f32_string(value)?; + Some(SmpLocomotivePolicyFloatAlignmentCandidate { + grounded_field_name: (*field_name).to_string(), + grounded_field_runtime_object_offset: *runtime_object_offset, + grounded_field_runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"), + grounded_field_file_offset: *file_offset, + grounded_field_file_offset_hex: format!("0x{file_offset:04x}"), + candidate_offset, + candidate_offset_hex: format!("0x{candidate_offset:04x}"), + candidate_value: value, + candidate_value_hex: format!("0x{value:08x}"), + probable_f32_le, + }) + }) + .collect::>(); + + let mut evidence = vec![ + format!( + "locomotive-policy neighborhood spans file offsets 0x{LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET:04x}..0x{LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET:04x}" + ), + "this neighborhood covers the selected-year bucket companion scalar, two startup-reset-owned bands, the linked-site removal gate, the three locomotive-availability policy bytes, the station-list selected-station mirror, and the cached available-locomotive rating".to_string(), + "the exact byte policy lanes live at 0x0f78 and 0x0f7c..0x0f7e, while the earlier grounded dword starts map to 0x0f87, 0x0f93, and 0x0f97 and the later grounded dword starts map to 0x0f9f and 0x0fa3".to_string(), + ]; + if three_byte_early_float_candidates.is_empty() { + evidence.push( + "no three-byte-early little-endian float-looking starts were observed ahead of the grounded dword fields in this file".to_string(), + ); + } else { + evidence.push(format!( + "observed {} float-looking 4-byte starts exactly three bytes before grounded dword fields in this file", + three_byte_early_float_candidates.len() + )); + } + + Some(SmpLocomotivePolicyNeighborhoodProbe { + profile_family, + source_kind, + window_offset: LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET, + window_end_offset: LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET, + window_len: LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET + - LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET, + window_len_hex: format!( + "0x{:x}", + LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET - LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET + ), + grounded_field_observations, + three_byte_early_float_candidates, + evidence, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/special_conditions/mod.rs b/crates/rrt-runtime/src/inspect/smp/special_conditions/mod.rs new file mode 100644 index 0000000..59d6d32 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/special_conditions/mod.rs @@ -0,0 +1,22 @@ +use super::bundle::SmpContainerProfile; +use crate::inspect::smp::*; + +mod aligned_band; +mod locomotive_policy; +mod model; +mod post_scalar; +mod recipe; +mod table; + +pub use model::*; + +pub(in crate::inspect::smp) use aligned_band::parse_smp_aligned_runtime_rule_band_probe; +pub(in crate::inspect::smp) use locomotive_policy::parse_locomotive_policy_neighborhood_probe; +pub(in crate::inspect::smp) use post_scalar::{ + parse_post_special_conditions_scalar_probe, parse_post_text_field_neighborhood_probe, +}; +pub(in crate::inspect::smp) use recipe::{ + classify_name_table_footer_progress_alignment, matches_candidate_availability_table_header, + parse_pre_recipe_scalar_plateau_probe, parse_recipe_book_summary_probe, +}; +pub(in crate::inspect::smp) use table::parse_special_conditions_probe; diff --git a/crates/rrt-runtime/src/inspect/smp/special_conditions/model.rs b/crates/rrt-runtime/src/inspect/smp/special_conditions/model.rs new file mode 100644 index 0000000..f5c85c0 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/special_conditions/model.rs @@ -0,0 +1,355 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSpecialConditionEntry { + pub slot_index: u8, + pub hidden: bool, + pub label_id: u32, + pub help_id: u32, + pub label: String, + pub value: u32, + pub value_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSpecialConditionsProbe { + pub profile_family: String, + pub source_kind: String, + pub table_offset: usize, + pub table_len: usize, + pub enabled_visible_count: usize, + pub enabled_visible_labels: Vec, + pub hidden_sentinel_slot_index: u8, + pub hidden_sentinel_value: u32, + pub hidden_sentinel_value_hex: String, + pub entries: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedSpecialConditionsTable { + pub source_kind: String, + pub table_offset: usize, + pub table_len: usize, + pub enabled_visible_count: usize, + pub enabled_visible_labels: Vec, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpAlignedRuntimeRuleBandLane { + pub band_index: usize, + pub absolute_offset: usize, + pub relative_offset: usize, + pub absolute_offset_hex: String, + pub relative_offset_hex: String, + pub lane_kind: String, + pub known_label: Option, + pub value: u32, + pub value_hex: String, + pub probable_f32_le: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpAlignedRuntimeRuleBandProbe { + pub profile_family: String, + pub source_kind: String, + pub band_offset: usize, + pub band_end_offset: usize, + pub band_len: usize, + pub band_len_hex: String, + pub dword_count: usize, + pub known_editor_rule_dword_count: usize, + pub trailing_scalar_index: usize, + pub trailing_scalar_offset: usize, + pub trailing_scalar_offset_hex: String, + pub post_window_overlap_start_index: usize, + pub post_window_overlap_dword_count: usize, + pub post_window_overlap_end_index: usize, + pub post_window_overlap_post_relative_offset_start_hex: String, + pub post_window_overlap_post_relative_offset_end_hex: String, + pub nonzero_post_window_overlap_band_indices: Vec, + pub nonzero_post_window_overlap_post_relative_offset_hexes: Vec, + pub nonzero_lane_count: usize, + pub nonzero_band_indices: Vec, + pub nonzero_relative_offset_hexes: Vec, + pub nonzero_lanes: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPostSpecialConditionsScalarLane { + pub absolute_offset: usize, + pub relative_offset: usize, + pub absolute_offset_hex: String, + pub relative_offset_hex: String, + pub value: u32, + pub value_hex: String, + pub probable_f32_le: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPostTextGroundedFieldObservation { + pub field_name: String, + pub runtime_object_offset: usize, + pub runtime_object_offset_hex: String, + pub file_offset: usize, + pub file_offset_hex: String, + pub field_width_bytes: usize, + pub field_width_bytes_hex: String, + pub raw_hex: String, + pub value_u8: Option, + pub value_u8_hex: Option, + pub value_u32: Option, + pub value_u32_hex: Option, + pub probable_f32_le: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPostTextFloatAlignmentCandidate { + pub grounded_field_name: String, + pub grounded_field_runtime_object_offset: usize, + pub grounded_field_runtime_object_offset_hex: String, + pub grounded_field_file_offset: usize, + pub grounded_field_file_offset_hex: String, + pub candidate_offset: usize, + pub candidate_offset_hex: String, + pub candidate_value: u32, + pub candidate_value_hex: String, + pub probable_f32_le: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPostTextFieldNeighborhoodProbe { + pub profile_family: String, + pub source_kind: String, + pub window_offset: usize, + pub window_end_offset: usize, + pub window_len: usize, + pub window_len_hex: String, + pub grounded_field_observations: Vec, + pub one_byte_early_float_candidates: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLocomotivePolicyFieldObservation { + pub field_name: String, + pub runtime_object_offset: usize, + pub runtime_object_offset_hex: String, + pub file_offset: usize, + pub file_offset_hex: String, + pub field_width_bytes: usize, + pub field_width_bytes_hex: String, + pub raw_hex: String, + pub value_u8: Option, + pub value_u8_hex: Option, + pub value_u32: Option, + pub value_u32_hex: Option, + pub probable_f32_le: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLocomotivePolicyFloatAlignmentCandidate { + pub grounded_field_name: String, + pub grounded_field_runtime_object_offset: usize, + pub grounded_field_runtime_object_offset_hex: String, + pub grounded_field_file_offset: usize, + pub grounded_field_file_offset_hex: String, + pub candidate_offset: usize, + pub candidate_offset_hex: String, + pub candidate_value: u32, + pub candidate_value_hex: String, + pub probable_f32_le: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLocomotivePolicyNeighborhoodProbe { + pub profile_family: String, + pub source_kind: String, + pub window_offset: usize, + pub window_end_offset: usize, + pub window_len: usize, + pub window_len_hex: String, + pub grounded_field_observations: Vec, + pub three_byte_early_float_candidates: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPreRecipeScalarPlateauLane { + pub absolute_offset: usize, + pub relative_offset: usize, + pub absolute_offset_hex: String, + pub relative_offset_hex: String, + pub value: u32, + pub value_hex: String, + pub probable_f32_le: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPreRecipeScalarPlateauProbe { + pub profile_family: String, + pub source_kind: String, + pub window_offset: usize, + pub window_end_offset: usize, + pub window_len: usize, + pub window_len_hex: String, + pub aligned_dword_count: usize, + pub family_signature: String, + pub nonzero_lanes: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRecipeBookSummaryBook { + pub book_index: usize, + pub book_offset: usize, + pub book_offset_hex: String, + pub head_kind: String, + pub head_nonzero_byte_count: usize, + pub head_cdcd_byte_count: usize, + pub head_first_16_hex: String, + pub max_annual_production_offset: usize, + pub max_annual_production_offset_hex: String, + pub max_annual_production_word: u32, + pub max_annual_production_word_hex: String, + pub max_annual_production_probable_f32_le: Option, + pub line_area_offset: usize, + pub line_area_offset_hex: String, + pub line_area_len: usize, + pub line_area_len_hex: String, + pub line_area_kind: String, + pub line_area_nonzero_byte_count: usize, + pub line_area_cdcd_byte_count: usize, + pub line_area_first_16_hex: String, + pub lines: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRecipeBookLineSummary { + pub line_index: usize, + pub line_offset: usize, + pub line_offset_hex: String, + pub line_kind: String, + pub line_signature_kind: String, + pub imports_to_runtime_descriptor: bool, + pub runtime_import_branch_kind: String, + pub line_nonzero_byte_count: usize, + pub line_cdcd_byte_count: usize, + pub line_first_16_hex: String, + pub mode_word_offset: usize, + pub mode_word_offset_hex: String, + pub mode_word: u32, + pub mode_word_hex: String, + pub annual_amount_offset: usize, + pub annual_amount_offset_hex: String, + pub annual_amount_word: u32, + pub annual_amount_word_hex: String, + pub annual_amount_probable_f32_le: Option, + pub supplied_cargo_token_offset: usize, + pub supplied_cargo_token_offset_hex: String, + pub supplied_cargo_token_word: u32, + pub supplied_cargo_token_word_hex: String, + pub supplied_cargo_token_layout_kind: String, + pub supplied_cargo_token_window_hex: String, + pub supplied_cargo_token_window_ascii: String, + pub supplied_cargo_token_active_in_runtime_import: bool, + pub supplied_cargo_token_probable_high16_ascii_stem: Option, + pub demanded_cargo_token_offset: usize, + pub demanded_cargo_token_offset_hex: String, + pub demanded_cargo_token_word: u32, + pub demanded_cargo_token_word_hex: String, + pub demanded_cargo_token_layout_kind: String, + pub demanded_cargo_token_window_hex: String, + pub demanded_cargo_token_window_ascii: String, + pub demanded_cargo_token_active_in_runtime_import: bool, + pub demanded_cargo_token_probable_high16_ascii_stem: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpRecipeBookSummaryProbe { + pub profile_family: String, + pub source_kind: String, + pub root_offset: usize, + pub root_offset_hex: String, + pub runtime_object_root_offset: usize, + pub runtime_object_root_offset_hex: String, + pub book_count: usize, + pub book_stride: usize, + pub book_stride_hex: String, + pub max_annual_production_relative_offset: usize, + pub max_annual_production_relative_offset_hex: String, + pub line_area_relative_offset: usize, + pub line_area_relative_offset_hex: String, + pub line_count: usize, + pub line_stride: usize, + pub line_stride_hex: String, + pub books: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpPostSpecialConditionsScalarProbe { + pub profile_family: String, + pub source_kind: String, + pub window_offset: usize, + pub window_end_offset: usize, + pub window_len: usize, + pub window_len_hex: String, + pub dword_count: usize, + pub overlap_end_offset: usize, + pub overlap_end_offset_hex: String, + pub overlap_dword_count: usize, + pub overlap_nonzero_dword_count: usize, + pub overlap_nonzero_relative_offset_hexes: Vec, + pub tail_offset: usize, + pub tail_offset_hex: String, + pub tail_len: usize, + pub tail_len_hex: String, + pub tail_dword_count: usize, + pub tail_runtime_object_offset: usize, + pub tail_runtime_object_offset_hex: String, + pub tail_runtime_object_end_offset: usize, + pub tail_runtime_object_end_offset_hex: String, + pub tail_runtime_object_validated_byte_mirror: bool, + pub tail_grounded_live_field_offset: usize, + pub tail_grounded_live_field_offset_hex: String, + pub tail_grounded_live_field_name: String, + pub tail_grounded_live_field_copy_len: usize, + pub tail_grounded_live_field_copy_len_hex: String, + pub tail_grounded_live_field_copy_end_offset: usize, + pub tail_grounded_live_field_copy_end_offset_hex: String, + pub tail_window_cuts_through_grounded_live_field: bool, + pub tail_grounded_live_field_remaining_file_window_offset: usize, + pub tail_grounded_live_field_remaining_file_window_offset_hex: String, + pub tail_grounded_live_field_remaining_file_window_len: usize, + pub tail_grounded_live_field_remaining_file_window_len_hex: String, + pub tail_grounded_live_field_remaining_file_window_nonzero_byte_count: usize, + pub tail_grounded_live_field_remaining_file_window_first_nonzero_offset: Option, + pub tail_grounded_live_field_remaining_file_window_first_nonzero_offset_hex: Option, + pub tail_grounded_live_field_remaining_file_window_last_nonzero_offset: Option, + pub tail_grounded_live_field_remaining_file_window_last_nonzero_offset_hex: Option, + pub tail_next_grounded_dword_field_offset: usize, + pub tail_next_grounded_dword_field_offset_hex: String, + pub tail_next_grounded_dword_field_file_offset: usize, + pub tail_next_grounded_dword_field_file_offset_hex: String, + pub tail_second_grounded_dword_field_offset: usize, + pub tail_second_grounded_dword_field_offset_hex: String, + pub tail_second_grounded_dword_field_file_offset: usize, + pub tail_second_grounded_dword_field_file_offset_hex: String, + pub post_text_field_file_alignment_matches_grounded_dword_fields: bool, + pub tail_nonzero_dword_count: usize, + pub tail_first_nonzero_offset: Option, + pub tail_first_nonzero_offset_hex: Option, + pub tail_last_nonzero_offset: Option, + pub tail_last_nonzero_offset_hex: Option, + pub tail_nonzero_relative_offset_hexes: Vec, + pub nonzero_dword_count: usize, + pub first_nonzero_offset: Option, + pub first_nonzero_offset_hex: Option, + pub last_nonzero_offset: Option, + pub last_nonzero_offset_hex: Option, + pub nonzero_lanes: Vec, + pub evidence: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/special_conditions/post_scalar.rs b/crates/rrt-runtime/src/inspect/smp/special_conditions/post_scalar.rs new file mode 100644 index 0000000..f60588b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/special_conditions/post_scalar.rs @@ -0,0 +1,448 @@ +use crate::inspect::smp::special_conditions::*; + +pub(in crate::inspect::smp) fn parse_post_special_conditions_scalar_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + special_conditions_probe: Option<&SmpSpecialConditionsProbe>, +) -> Option { + special_conditions_probe?; + if POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET > bytes.len() { + return None; + } + + let dword_count = + (POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET) / 4; + let mut nonzero_lanes = Vec::new(); + for index in 0..dword_count { + let absolute_offset = POST_SPECIAL_CONDITIONS_SCALAR_OFFSET + index * 4; + let value = read_u32_at(bytes, absolute_offset)?; + if value == 0 { + continue; + } + nonzero_lanes.push(SmpPostSpecialConditionsScalarLane { + absolute_offset, + relative_offset: absolute_offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET, + absolute_offset_hex: format!("0x{absolute_offset:04x}"), + relative_offset_hex: format!( + "0x{:x}", + absolute_offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET + ), + value, + value_hex: format!("0x{value:08x}"), + probable_f32_le: probable_normal_f32_string(value), + }); + } + + let source_kind = match file_extension_hint.unwrap_or("") { + "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 profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let first_nonzero_offset = nonzero_lanes.first().map(|lane| lane.absolute_offset); + let last_nonzero_offset = nonzero_lanes.last().map(|lane| lane.absolute_offset); + let overlap_nonzero_relative_offset_hexes = nonzero_lanes + .iter() + .filter(|lane| lane.absolute_offset < POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET) + .map(|lane| lane.relative_offset_hex.clone()) + .collect::>(); + let tail_nonzero_lanes = nonzero_lanes + .iter() + .filter(|lane| lane.absolute_offset >= POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET) + .cloned() + .collect::>(); + let tail_first_nonzero_offset = tail_nonzero_lanes.first().map(|lane| lane.absolute_offset); + let tail_last_nonzero_offset = tail_nonzero_lanes.last().map(|lane| lane.absolute_offset); + let tail_nonzero_relative_offset_hexes = tail_nonzero_lanes + .iter() + .map(|lane| lane.relative_offset_hex.clone()) + .collect::>(); + let grounded_text_field_remaining_file_window = &bytes[POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + ..POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET]; + let mut grounded_text_field_remaining_nonzero_offsets = Vec::new(); + for (index, byte) in grounded_text_field_remaining_file_window.iter().enumerate() { + if *byte != 0 { + grounded_text_field_remaining_nonzero_offsets + .push(POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + index); + } + } + let grounded_text_field_remaining_first_nonzero_offset = + grounded_text_field_remaining_nonzero_offsets + .first() + .copied(); + let grounded_text_field_remaining_last_nonzero_offset = + grounded_text_field_remaining_nonzero_offsets + .last() + .copied(); + let mut evidence = vec![ + format!( + "fixed post-sentinel dword window at 0x{POST_SPECIAL_CONDITIONS_SCALAR_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}" + ), + "window starts immediately after the hidden special-conditions sentinel slot at 0x0df0" + .to_string(), + format!( + "leading overlap prefix 0x{POST_SPECIAL_CONDITIONS_SCALAR_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET:04x} aliases aligned runtime-rule band indices {}..{}", + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX, + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX + + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT + - 1 + ), + format!("save-only tail begins at 0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET:04x}"), + format!( + "that tail is offset-aligned with live runtime object bytes [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET:04x}..+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET:04x}]" + ), + format!( + "the tail start lands on the grounded live field [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET:04x}], a 0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN:x}-byte status-text buffer written by win/lose and winner-announcement helpers" + ), + format!( + "current dword scan stops at 0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}, leaving one byte-aligned continuation window 0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET:04x} before the next clean live-field edge at [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET:04x}]" + ), + format!( + "the next exact grounded fields after that edge begin at [world+0x{POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}], and [world+0x{POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET:04x}], which map to file offsets 0x{POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET:04x}, 0x0f5d, 0x0f61, 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x}, and 0x0f6d" + ), + format!( + "the first grounded dword-sized fields after that edge are [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}] and [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET:04x}], which would land at file offsets 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x} and 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET:04x}" + ), + ]; + if nonzero_lanes.is_empty() { + evidence.push( + "all observed dwords in this post-sentinel window are zero for this file".to_string(), + ); + } else { + evidence.push(format!( + "observed {} nonzero dword lanes between {} and {}", + nonzero_lanes.len(), + nonzero_lanes + .first() + .map(|lane| lane.absolute_offset_hex.as_str()) + .unwrap_or("n/a"), + nonzero_lanes + .last() + .map(|lane| lane.absolute_offset_hex.as_str()) + .unwrap_or("n/a") + )); + if nonzero_lanes + .iter() + .all(|lane| lane.probable_f32_le.is_some()) + { + evidence.push( + "every nonzero lane in this window also decodes as a normal finite little-endian f32" + .to_string(), + ); + } + evidence.push(format!( + "{} nonzero lanes fall inside the aligned-band overlap prefix and {} fall inside the later tail", + overlap_nonzero_relative_offset_hexes.len(), + tail_nonzero_lanes.len() + )); + } + evidence.push( + "checked file bytes in the later tail are not yet validated as a byte-for-byte mirror of the live object, because the region aligned to [world+0x4b47] does not currently decode as preserved text in the checked saves" + .to_string(), + ); + if grounded_text_field_remaining_nonzero_offsets.is_empty() { + evidence.push( + "the remaining file window through the grounded text-field edge is all zero in this file" + .to_string(), + ); + } else { + evidence.push(format!( + "the remaining file window through the grounded text-field edge still has {} nonzero bytes between 0x{:04x} and 0x{:04x}", + grounded_text_field_remaining_nonzero_offsets.len(), + grounded_text_field_remaining_first_nonzero_offset.unwrap_or(0), + grounded_text_field_remaining_last_nonzero_offset.unwrap_or(0) + )); + } + + Some(SmpPostSpecialConditionsScalarProbe { + profile_family, + source_kind, + window_offset: POST_SPECIAL_CONDITIONS_SCALAR_OFFSET, + window_end_offset: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, + window_len: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET, + window_len_hex: format!( + "0x{:x}", + POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET + ), + dword_count, + overlap_end_offset: POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET, + overlap_end_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET:04x}" + ), + overlap_dword_count: (POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET + - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET) + / 4, + overlap_nonzero_dword_count: overlap_nonzero_relative_offset_hexes.len(), + overlap_nonzero_relative_offset_hexes, + tail_offset: POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET, + tail_offset_hex: format!("0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET:04x}"), + tail_len: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET, + tail_len_hex: format!( + "0x{:x}", + POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET + ), + tail_dword_count: (POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET) + / 4, + tail_runtime_object_offset: POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET, + tail_runtime_object_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET:04x}" + ), + tail_runtime_object_end_offset: + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET, + tail_runtime_object_end_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET:04x}" + ), + tail_runtime_object_validated_byte_mirror: false, + tail_grounded_live_field_offset: + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET, + tail_grounded_live_field_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET:04x}" + ), + tail_grounded_live_field_name: "victory-or-outcome status text buffer".to_string(), + tail_grounded_live_field_copy_len: + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN, + tail_grounded_live_field_copy_len_hex: format!( + "0x{:x}", + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN + ), + tail_grounded_live_field_copy_end_offset: + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET, + tail_grounded_live_field_copy_end_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET:04x}" + ), + tail_window_cuts_through_grounded_live_field: + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET + < POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET, + tail_grounded_live_field_remaining_file_window_offset: + POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, + tail_grounded_live_field_remaining_file_window_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}" + ), + tail_grounded_live_field_remaining_file_window_len: + POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET + - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, + tail_grounded_live_field_remaining_file_window_len_hex: format!( + "0x{:x}", + POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET + - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + ), + tail_grounded_live_field_remaining_file_window_nonzero_byte_count: + grounded_text_field_remaining_nonzero_offsets.len(), + tail_grounded_live_field_remaining_file_window_first_nonzero_offset: + grounded_text_field_remaining_first_nonzero_offset, + tail_grounded_live_field_remaining_file_window_first_nonzero_offset_hex: + grounded_text_field_remaining_first_nonzero_offset + .map(|offset| format!("0x{offset:04x}")), + tail_grounded_live_field_remaining_file_window_last_nonzero_offset: + grounded_text_field_remaining_last_nonzero_offset, + tail_grounded_live_field_remaining_file_window_last_nonzero_offset_hex: + grounded_text_field_remaining_last_nonzero_offset + .map(|offset| format!("0x{offset:04x}")), + tail_next_grounded_dword_field_offset: + POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET, + tail_next_grounded_dword_field_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}" + ), + tail_next_grounded_dword_field_file_offset: + POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET, + tail_next_grounded_dword_field_file_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x}" + ), + tail_second_grounded_dword_field_offset: + POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET, + tail_second_grounded_dword_field_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET:04x}" + ), + tail_second_grounded_dword_field_file_offset: + POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET, + tail_second_grounded_dword_field_file_offset_hex: format!( + "0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET:04x}" + ), + post_text_field_file_alignment_matches_grounded_dword_fields: false, + tail_nonzero_dword_count: tail_nonzero_lanes.len(), + tail_first_nonzero_offset, + tail_first_nonzero_offset_hex: tail_first_nonzero_offset + .map(|offset| format!("0x{offset:04x}")), + tail_last_nonzero_offset, + tail_last_nonzero_offset_hex: tail_last_nonzero_offset + .map(|offset| format!("0x{offset:04x}")), + tail_nonzero_relative_offset_hexes, + nonzero_dword_count: nonzero_lanes.len(), + first_nonzero_offset, + first_nonzero_offset_hex: first_nonzero_offset.map(|offset| format!("0x{offset:04x}")), + last_nonzero_offset, + last_nonzero_offset_hex: last_nonzero_offset.map(|offset| format!("0x{offset:04x}")), + nonzero_lanes, + evidence, + }) +} + +pub(in crate::inspect::smp) fn parse_post_text_field_neighborhood_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + special_conditions_probe: Option<&SmpSpecialConditionsProbe>, +) -> Option { + special_conditions_probe?; + if POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET > bytes.len() { + return None; + } + + let profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let source_kind = match file_extension_hint.unwrap_or("") { + "gmp" => "post-text-grounded-field-neighborhood", + "gms" => "post-text-grounded-field-neighborhood", + "gmx" => "post-text-grounded-field-neighborhood", + _ => "post-text-grounded-field-neighborhood", + } + .to_string(); + + let exact_fields = [ + ( + "Auto-Show Grade During Track Lay", + POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET, + POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET, + 1usize, + ), + ( + "Starting Building Density Level", + POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET, + 0x0f5dusize, + 1usize, + ), + ( + "Building Density Growth", + POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET, + 0x0f61usize, + 1usize, + ), + ( + "leftover simulation time accumulator", + POST_TEXT_FIELD_3_RUNTIME_OBJECT_OFFSET, + 0x0f65usize, + 4usize, + ), + ( + "selected-year lane snapshot", + POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET, + 0x0f6dusize, + 1usize, + ), + ( + "late locomotive policy gate dword", + POST_TEXT_FIELD_5_RUNTIME_OBJECT_OFFSET, + 0x0f71usize, + 4usize, + ), + ]; + + let grounded_field_observations = exact_fields + .iter() + .map( + |(field_name, runtime_object_offset, file_offset, field_width_bytes)| { + let raw = &bytes[*file_offset..*file_offset + *field_width_bytes]; + let raw_hex = hex_encode(raw); + let (value_u8, value_u8_hex, value_u32, value_u32_hex, probable_f32_le) = + if *field_width_bytes == 1 { + let value = raw[0]; + ( + Some(value), + Some(format!("0x{value:02x}")), + None, + None, + None, + ) + } else { + let value = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); + ( + None, + None, + Some(value), + Some(format!("0x{value:08x}")), + probable_normal_f32_string(value), + ) + }; + SmpPostTextGroundedFieldObservation { + field_name: (*field_name).to_string(), + runtime_object_offset: *runtime_object_offset, + runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"), + file_offset: *file_offset, + file_offset_hex: format!("0x{file_offset:04x}"), + field_width_bytes: *field_width_bytes, + field_width_bytes_hex: format!("0x{field_width_bytes:x}"), + raw_hex, + value_u8, + value_u8_hex, + value_u32, + value_u32_hex, + probable_f32_le, + } + }, + ) + .collect::>(); + + let one_byte_early_float_candidates = exact_fields + .iter() + .filter(|(_, _, file_offset, _)| *file_offset > 0) + .filter_map(|(field_name, runtime_object_offset, file_offset, _)| { + let candidate_offset = file_offset - 1; + let value = read_u32_at(bytes, candidate_offset)?; + let probable_f32_le = probable_normal_f32_string(value)?; + Some(SmpPostTextFloatAlignmentCandidate { + grounded_field_name: (*field_name).to_string(), + grounded_field_runtime_object_offset: *runtime_object_offset, + grounded_field_runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"), + grounded_field_file_offset: *file_offset, + grounded_field_file_offset_hex: format!("0x{file_offset:04x}"), + candidate_offset, + candidate_offset_hex: format!("0x{candidate_offset:04x}"), + candidate_value: value, + candidate_value_hex: format!("0x{value:08x}"), + probable_f32_le, + }) + }) + .collect::>(); + + let mut evidence = vec![ + format!( + "post-text grounded-field neighborhood spans file offsets 0x{POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET:04x}..0x{POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET:04x}" + ), + "this neighborhood starts at the first grounded post-text field [world+0x4c74] and extends through the later dword at [world+0x4c8c]".to_string(), + "the exact grounded field offsets here are byte-oriented at 0x0f59, 0x0f5d, 0x0f61, and 0x0f6d, with dword-sized fields only at 0x0f65 and 0x0f71".to_string(), + ]; + if one_byte_early_float_candidates.is_empty() { + evidence.push( + "no one-byte-early little-endian float-looking starts were observed ahead of the grounded fields in this file".to_string(), + ); + } else { + evidence.push(format!( + "observed {} float-looking 4-byte starts exactly one byte before grounded field offsets in this file", + one_byte_early_float_candidates.len() + )); + } + + Some(SmpPostTextFieldNeighborhoodProbe { + profile_family, + source_kind, + window_offset: POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET, + window_end_offset: POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET, + window_len: POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET - POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET, + window_len_hex: format!( + "0x{:x}", + POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET - POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET + ), + grounded_field_observations, + one_byte_early_float_candidates, + evidence, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/special_conditions/recipe.rs b/crates/rrt-runtime/src/inspect/smp/special_conditions/recipe.rs new file mode 100644 index 0000000..808dbb9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/special_conditions/recipe.rs @@ -0,0 +1,322 @@ +use crate::inspect::smp::special_conditions::*; + +pub(in crate::inspect::smp) fn parse_pre_recipe_scalar_plateau_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + special_conditions_probe: Option<&SmpSpecialConditionsProbe>, +) -> Option { + special_conditions_probe?; + if PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET > bytes.len() { + return None; + } + + let profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let source_kind = match file_extension_hint.unwrap_or("") { + "gmp" => "pre-recipe-scalar-plateau", + "gms" => "pre-recipe-scalar-plateau", + "gmx" => "pre-recipe-scalar-plateau", + _ => "pre-recipe-scalar-plateau", + } + .to_string(); + + let aligned_dword_count = + (PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET) / 4; + let mut nonzero_lanes = Vec::new(); + for index in 0..aligned_dword_count { + let absolute_offset = PRE_RECIPE_SCALAR_PLATEAU_OFFSET + index * 4; + let value = read_u32_at(bytes, absolute_offset)?; + if value == 0 { + continue; + } + nonzero_lanes.push(SmpPreRecipeScalarPlateauLane { + absolute_offset, + relative_offset: absolute_offset - PRE_RECIPE_SCALAR_PLATEAU_OFFSET, + absolute_offset_hex: format!("0x{absolute_offset:04x}"), + relative_offset_hex: format!( + "0x{:x}", + absolute_offset - PRE_RECIPE_SCALAR_PLATEAU_OFFSET + ), + value, + value_hex: format!("0x{value:08x}"), + probable_f32_le: probable_normal_f32_string(value), + }); + } + + let family_signature = match ( + read_u32_at(bytes, 0x0faf), + read_u32_at(bytes, 0x0fb3), + read_u32_at(bytes, 0x0fcb), + ) { + (Some(0x4000003f), Some(0xe560423f), Some(0x00000000)) => { + "rt3-105-scenario-pre-recipe-plateau-v1" + } + (Some(0x8000003f), Some(0x75c28f3f), Some(0x00300000)) => { + "rt3-105-base-pre-recipe-plateau-v1" + } + (Some(0x8000003f), Some(0x75c28f3f), Some(0xcdcdcd00)) => { + "rt3-105-alt-pre-recipe-plateau-v1" + } + _ => "unknown", + } + .to_string(); + + let mut evidence = vec![ + format!( + "aligned scalar plateau spans file offsets 0x{PRE_RECIPE_SCALAR_PLATEAU_OFFSET:04x}..0x{PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET:04x}" + ), + "this plateau ends immediately before the grounded recipe-book root at [world+0x0fe7]".to_string(), + "current grounding inside this span is still structural rather than semantic, so the probe only records aligned dword lanes and observed family signatures".to_string(), + ]; + if !nonzero_lanes.is_empty() { + evidence.push(format!( + "observed {} nonzero aligned dword lanes in the pre-recipe plateau", + nonzero_lanes.len() + )); + } + if family_signature != "unknown" { + evidence.push(format!( + "matched observed family signature {family_signature}" + )); + } + + Some(SmpPreRecipeScalarPlateauProbe { + profile_family, + source_kind, + window_offset: PRE_RECIPE_SCALAR_PLATEAU_OFFSET, + window_end_offset: PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET, + window_len: PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET, + window_len_hex: format!( + "0x{:x}", + PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET + ), + aligned_dword_count, + family_signature, + nonzero_lanes, + evidence, + }) +} + +pub(in crate::inspect::smp) fn parse_recipe_book_summary_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, + special_conditions_probe: Option<&SmpSpecialConditionsProbe>, +) -> Option { + special_conditions_probe?; + if RECIPE_BOOK_SUMMARY_END_OFFSET > bytes.len() { + return None; + } + + let profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let source_kind = match file_extension_hint.unwrap_or("") { + "gmp" => "recipe-book-summary", + "gms" => "recipe-book-summary", + "gmx" => "recipe-book-summary", + _ => "recipe-book-summary", + } + .to_string(); + + let mut books = Vec::with_capacity(RECIPE_BOOK_COUNT); + let mut mixed_head_count = 0usize; + let mut mixed_line_area_count = 0usize; + let mut cdcd_line_area_count = 0usize; + let mut zero_line_area_count = 0usize; + + for book_index in 0..RECIPE_BOOK_COUNT { + let book_offset = RECIPE_BOOK_ROOT_OFFSET + book_index * RECIPE_BOOK_STRIDE; + let head = &bytes[book_offset..book_offset + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET]; + let line_area_offset = book_offset + RECIPE_BOOK_LINE_AREA_OFFSET; + let line_area = &bytes[line_area_offset..line_area_offset + RECIPE_BOOK_LINE_AREA_LEN]; + let max_annual_production_offset = book_offset + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET; + let max_annual_production_word = read_u32_at(bytes, max_annual_production_offset)?; + let mut lines = Vec::with_capacity(RECIPE_BOOK_LINE_COUNT); + for line_index in 0..RECIPE_BOOK_LINE_COUNT { + let line_offset = line_area_offset + line_index * RECIPE_BOOK_LINE_STRIDE; + let line = &bytes[line_offset..line_offset + RECIPE_BOOK_LINE_STRIDE]; + let supplied_cargo_token_window = &line[0x08..0x20]; + let demanded_cargo_token_window = &line[0x1c..0x30]; + let mode_word = read_u32_at(bytes, line_offset)?; + let annual_amount_word = read_u32_at(bytes, line_offset + 0x04)?; + let supplied_cargo_token_word = read_u32_at(bytes, line_offset + 0x08)?; + let demanded_cargo_token_word = read_u32_at(bytes, line_offset + 0x1c)?; + lines.push(SmpRecipeBookLineSummary { + line_index, + line_offset, + line_offset_hex: format!("0x{line_offset:04x}"), + line_kind: classify_recipe_book_region_kind(line).to_string(), + line_signature_kind: classify_recipe_line_signature( + mode_word, + supplied_cargo_token_word, + demanded_cargo_token_word, + ) + .to_string(), + imports_to_runtime_descriptor: mode_word != 0, + runtime_import_branch_kind: classify_recipe_runtime_import_branch(mode_word) + .to_string(), + line_nonzero_byte_count: line.iter().filter(|byte| **byte != 0).count(), + line_cdcd_byte_count: line.iter().filter(|byte| **byte == 0xcd).count(), + line_first_16_hex: hex_encode(&line[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(line.len())]), + mode_word_offset: line_offset, + mode_word_offset_hex: format!("0x{line_offset:04x}"), + mode_word, + mode_word_hex: format!("0x{mode_word:08x}"), + annual_amount_offset: line_offset + 0x04, + annual_amount_offset_hex: format!("0x{:04x}", line_offset + 0x04), + annual_amount_word, + annual_amount_word_hex: format!("0x{annual_amount_word:08x}"), + annual_amount_probable_f32_le: probable_normal_f32_string(annual_amount_word), + supplied_cargo_token_offset: line_offset + 0x08, + supplied_cargo_token_offset_hex: format!("0x{:04x}", line_offset + 0x08), + supplied_cargo_token_word, + supplied_cargo_token_word_hex: format!("0x{supplied_cargo_token_word:08x}"), + supplied_cargo_token_layout_kind: classify_recipe_token_layout( + supplied_cargo_token_word, + ) + .to_string(), + supplied_cargo_token_window_hex: hex_encode(supplied_cargo_token_window), + supplied_cargo_token_window_ascii: ascii_preview(supplied_cargo_token_window), + supplied_cargo_token_active_in_runtime_import: mode_word != 0 && mode_word != 1, + supplied_cargo_token_probable_high16_ascii_stem: + probable_recipe_token_high16_ascii_stem(supplied_cargo_token_word), + demanded_cargo_token_offset: line_offset + 0x1c, + demanded_cargo_token_offset_hex: format!("0x{:04x}", line_offset + 0x1c), + demanded_cargo_token_word, + demanded_cargo_token_word_hex: format!("0x{demanded_cargo_token_word:08x}"), + demanded_cargo_token_layout_kind: classify_recipe_token_layout( + demanded_cargo_token_word, + ) + .to_string(), + demanded_cargo_token_window_hex: hex_encode(demanded_cargo_token_window), + demanded_cargo_token_window_ascii: ascii_preview(demanded_cargo_token_window), + demanded_cargo_token_active_in_runtime_import: mode_word == 1 || mode_word == 3, + demanded_cargo_token_probable_high16_ascii_stem: + probable_recipe_token_high16_ascii_stem(demanded_cargo_token_word), + }); + } + + let head_kind = classify_recipe_book_region_kind(head).to_string(); + let line_area_kind = classify_recipe_book_region_kind(line_area).to_string(); + if head_kind == "mixed" { + mixed_head_count += 1; + } + match line_area_kind.as_str() { + "zero" => zero_line_area_count += 1, + "cdcd" => cdcd_line_area_count += 1, + _ => mixed_line_area_count += 1, + } + + books.push(SmpRecipeBookSummaryBook { + book_index, + book_offset, + book_offset_hex: format!("0x{book_offset:04x}"), + head_kind, + head_nonzero_byte_count: head.iter().filter(|byte| **byte != 0).count(), + head_cdcd_byte_count: head.iter().filter(|byte| **byte == 0xcd).count(), + head_first_16_hex: hex_encode(&head[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(head.len())]), + max_annual_production_offset, + max_annual_production_offset_hex: format!("0x{max_annual_production_offset:04x}"), + max_annual_production_word, + max_annual_production_word_hex: format!("0x{max_annual_production_word:08x}"), + max_annual_production_probable_f32_le: probable_normal_f32_string( + max_annual_production_word, + ), + line_area_offset, + line_area_offset_hex: format!("0x{line_area_offset:04x}"), + line_area_len: RECIPE_BOOK_LINE_AREA_LEN, + line_area_len_hex: format!("0x{:x}", RECIPE_BOOK_LINE_AREA_LEN), + line_area_kind, + line_area_nonzero_byte_count: line_area.iter().filter(|byte| **byte != 0).count(), + line_area_cdcd_byte_count: line_area.iter().filter(|byte| **byte == 0xcd).count(), + line_area_first_16_hex: hex_encode( + &line_area[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(line_area.len())], + ), + lines, + }); + } + + let mut evidence = vec![ + format!( + "grounded recipe-book root begins at file offset 0x{RECIPE_BOOK_ROOT_OFFSET:04x} and runtime offset [world+0x{RECIPE_BOOK_ROOT_OFFSET:04x}]" + ), + format!( + "parsed {RECIPE_BOOK_COUNT} fixed books with stride 0x{RECIPE_BOOK_STRIDE:x}, shared cap lane at +0x{RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET:x}, and five line slots at +0x{RECIPE_BOOK_LINE_AREA_OFFSET:x} with stride 0x{RECIPE_BOOK_LINE_STRIDE:x}" + ), + "this probe is structural only: it summarizes per-book heads plus five raw line records without decoding the mode or cargo-token semantics beyond the grounded offsets".to_string(), + ]; + evidence.push(format!( + "{mixed_head_count} books have mixed pre-line heads; line areas split into {zero_line_area_count} zero, {cdcd_line_area_count} cdcd, and {mixed_line_area_count} mixed books" + )); + + Some(SmpRecipeBookSummaryProbe { + profile_family, + source_kind, + root_offset: RECIPE_BOOK_ROOT_OFFSET, + root_offset_hex: format!("0x{RECIPE_BOOK_ROOT_OFFSET:04x}"), + runtime_object_root_offset: RECIPE_BOOK_ROOT_OFFSET, + runtime_object_root_offset_hex: format!("0x{RECIPE_BOOK_ROOT_OFFSET:04x}"), + book_count: RECIPE_BOOK_COUNT, + book_stride: RECIPE_BOOK_STRIDE, + book_stride_hex: format!("0x{:x}", RECIPE_BOOK_STRIDE), + max_annual_production_relative_offset: RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET, + max_annual_production_relative_offset_hex: format!( + "0x{:x}", + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + ), + line_area_relative_offset: RECIPE_BOOK_LINE_AREA_OFFSET, + line_area_relative_offset_hex: format!("0x{:x}", RECIPE_BOOK_LINE_AREA_OFFSET), + line_count: RECIPE_BOOK_LINE_COUNT, + line_stride: RECIPE_BOOK_LINE_STRIDE, + line_stride_hex: format!("0x{:x}", RECIPE_BOOK_LINE_STRIDE), + books, + evidence, + }) +} + +pub(in crate::inspect::smp) fn matches_candidate_availability_table_header( + bytes: &[u8], + header_offset: usize, +) -> bool { + matches!( + ( + read_u32_at(bytes, header_offset + 0x08), + read_u32_at(bytes, header_offset + 0x0c), + read_u32_at(bytes, header_offset + 0x10), + read_u32_at(bytes, header_offset + 0x14), + read_u32_at(bytes, header_offset + 0x18), + read_u32_at(bytes, header_offset + 0x1c), + read_u32_at(bytes, header_offset + 0x20), + read_u32_at(bytes, header_offset + 0x24), + read_u32_at(bytes, header_offset + 0x28), + ), + ( + Some(0x0000332e), + Some(0x00000001), + Some(0x00000022), + Some(0x00000002), + Some(0x00000002), + Some(_), + Some(_), + Some(0x00000000), + Some(0x00000001), + ) + ) +} + +pub(in crate::inspect::smp) fn classify_name_table_footer_progress_alignment( + value: u32, +) -> Option<&'static str> { + match value { + 0x32dc => Some( + "Footer progress word 0x000032dc matches the grounded late rehydrate progress id 0x32dc.", + ), + 0x3714 => Some( + "Footer progress word 0x00003714 matches the grounded late rehydrate progress id 0x3714.", + ), + _ => None, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/special_conditions/table.rs b/crates/rrt-runtime/src/inspect/smp/special_conditions/table.rs new file mode 100644 index 0000000..c1c7d70 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/special_conditions/table.rs @@ -0,0 +1,85 @@ +use crate::inspect::smp::special_conditions::*; + +pub(in crate::inspect::smp) fn parse_special_conditions_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + let table_len = SPECIAL_CONDITION_COUNT.checked_mul(4)?; + let table_end = SPECIAL_CONDITIONS_OFFSET.checked_add(table_len)?; + if table_end > bytes.len() { + return None; + } + + let mut entries = Vec::with_capacity(SPECIAL_CONDITION_COUNT); + for definition in KNOWN_SPECIAL_CONDITION_DEFINITIONS { + let value = read_u32_at( + bytes, + SPECIAL_CONDITIONS_OFFSET + (definition.slot_index as usize) * 4, + )?; + if value > 1 { + return None; + } + entries.push(SmpSpecialConditionEntry { + slot_index: definition.slot_index, + hidden: definition.hidden, + label_id: definition.label_id, + help_id: definition.help_id, + label: definition.label.to_string(), + value, + value_hex: format!("0x{value:08x}"), + }); + } + + let hidden_sentinel = entries + .iter() + .find(|entry| entry.slot_index == SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT as u8)?; + if hidden_sentinel.value != 1 { + return None; + } + + let enabled_visible_labels = entries + .iter() + .filter(|entry| !entry.hidden && entry.value != 0) + .map(|entry| entry.label.clone()) + .collect::>(); + let source_kind = match file_extension_hint.unwrap_or("") { + "gmp" => "map-fixed-special-conditions-range", + "gms" => "save-fixed-special-conditions-range", + "gmx" => "sandbox-fixed-special-conditions-range", + _ => "fixed-special-conditions-range", + } + .to_string(); + let profile_family = container_profile + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()); + let mut evidence = vec![ + format!("fixed 36-dword range at 0x{SPECIAL_CONDITIONS_OFFSET:04x}"), + "all observed lanes are boolean dwords".to_string(), + "hidden slot 35 carries the expected sentinel value 1".to_string(), + "slot metadata matches the grounded editor special-conditions table at 0x005f3ab0" + .to_string(), + ]; + if enabled_visible_labels.is_empty() { + evidence.push("no visible special conditions enabled in this file".to_string()); + } else { + evidence.push(format!( + "enabled visible conditions: {}", + enabled_visible_labels.join(", ") + )); + } + + Some(SmpSpecialConditionsProbe { + profile_family, + source_kind, + table_offset: SPECIAL_CONDITIONS_OFFSET, + table_len, + enabled_visible_count: enabled_visible_labels.len(), + enabled_visible_labels, + hidden_sentinel_slot_index: SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT as u8, + hidden_sentinel_value: hidden_sentinel.value, + hidden_sentinel_value_hex: hidden_sentinel.value_hex.clone(), + entries, + evidence, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/alignment.rs b/crates/rrt-runtime/src/inspect/smp/structures/alignment.rs new file mode 100644 index 0000000..4ecfa19 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/alignment.rs @@ -0,0 +1,92 @@ +use crate::inspect::smp::structures::*; +use std::collections::BTreeSet; + +pub(in crate::inspect::smp) fn summarize_placed_structure_dynamic_side_buffer_alignment( + side_buffer: &SmpSavePlacedStructureDynamicSideBufferProbe, + triplets: &SmpSavePlacedStructureRecordTripletProbe, +) -> SmpSavePlacedStructureDynamicSideBufferAlignmentProbe { + let triplet_name_pairs = triplets + .entries + .iter() + .map(|entry| (entry.primary_name.clone(), entry.secondary_name.clone())) + .collect::>(); + let matched_name_pair_samples = side_buffer + .name_pair_summaries + .iter() + .filter(|summary| { + triplet_name_pairs + .contains(&(summary.primary_name.clone(), summary.secondary_name.clone())) + }) + .take(5) + .cloned() + .collect::>(); + let unmatched_side_buffer_name_pair_samples = side_buffer + .name_pair_summaries + .iter() + .filter(|summary| { + !triplet_name_pairs + .contains(&(summary.primary_name.clone(), summary.secondary_name.clone())) + }) + .take(5) + .cloned() + .collect::>(); + let side_buffer_rows_with_matching_triplet_name_pair_count = side_buffer + .name_pair_summaries + .iter() + .filter(|summary| { + triplet_name_pairs + .contains(&(summary.primary_name.clone(), summary.secondary_name.clone())) + }) + .map(|summary| summary.count) + .sum::(); + let unique_side_buffer_name_pair_count = side_buffer.name_pair_summaries.len(); + let overlapping_name_pair_count = side_buffer + .name_pair_summaries + .iter() + .filter(|summary| { + triplet_name_pairs + .contains(&(summary.primary_name.clone(), summary.secondary_name.clone())) + }) + .count(); + SmpSavePlacedStructureDynamicSideBufferAlignmentProbe { + unique_side_buffer_name_pair_count, + unique_triplet_name_pair_count: triplet_name_pairs.len(), + overlapping_name_pair_count, + side_buffer_row_count: side_buffer.decoded_embedded_name_row_count, + side_buffer_rows_with_matching_triplet_name_pair_count, + side_buffer_rows_without_matching_triplet_name_pair_count: side_buffer + .decoded_embedded_name_row_count + .saturating_sub(side_buffer_rows_with_matching_triplet_name_pair_count), + triplet_name_pairs_without_side_buffer_match_count: triplet_name_pairs + .len() + .saturating_sub(overlapping_name_pair_count), + matched_name_pair_samples, + unmatched_side_buffer_name_pair_samples, + evidence: vec![ + "placed-structure dynamic side-buffer alignment compares decoded 0x38a5 embedded name pairs against the grounded 0x36b1 triplet name-pair corpus".to_string(), + format!( + "{} of {} decoded side-buffer rows currently reuse name pairs already present in the placed-structure triplet owner seam", + side_buffer_rows_with_matching_triplet_name_pair_count, + side_buffer.decoded_embedded_name_row_count + ), + format!( + "{} of {} unique side-buffer name pairs overlap the grounded triplet name-pair corpus", + overlapping_name_pair_count, + unique_side_buffer_name_pair_count + ), + ], + } +} + +#[derive(Clone, Copy)] +pub(in crate::inspect::smp) struct IndexedCollectionHeaderSummary { + pub(in crate::inspect::smp) metadata_tag_offset: usize, + pub(in crate::inspect::smp) records_tag_offset: usize, + pub(in crate::inspect::smp) close_tag_offset: usize, + pub(in crate::inspect::smp) direct_collection_flag: u32, + pub(in crate::inspect::smp) direct_record_stride: u32, + pub(in crate::inspect::smp) live_id_bound: u32, + pub(in crate::inspect::smp) live_record_count: u32, + pub(in crate::inspect::smp) header_words: + [u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT], +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/embedded_names.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/embedded_names.rs new file mode 100644 index 0000000..e0e01da --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/embedded_names.rs @@ -0,0 +1,61 @@ +use crate::inspect::smp::structures::*; + +#[derive(Clone)] +pub(super) struct EmbeddedNameRow { + pub(super) name_tag_relative_offset: usize, + pub(super) prefix_leading_dword: u32, + pub(super) prefix_trailing_word: u16, + pub(super) prefix_separator_byte: u8, + pub(super) name_payload_relative_offset: usize, + pub(super) name_payload_len: usize, + pub(super) primary_name: Option, + pub(super) secondary_name: Option, + pub(super) tertiary_name: Option, +} + +pub(super) fn parse_embedded_name_rows( + records_payload: &[u8], + embedded_name_tag_offsets: &[usize], +) -> Vec { + embedded_name_tag_offsets + .iter() + .copied() + .filter_map(|name_tag_relative_offset| { + let prefix_payload = records_payload.get(..name_tag_relative_offset)?; + if prefix_payload.len() < 7 { + return None; + } + let prefix_leading_dword = read_u32_at(prefix_payload, prefix_payload.len() - 7)?; + let prefix_trailing_word = read_u16_at(prefix_payload, prefix_payload.len() - 3)?; + let prefix_separator_byte = *prefix_payload.last()?; + let mut parsed_names = None; + for relative_name_offset in [4usize, 6usize] { + let Some(name_payload) = + records_payload.get(name_tag_relative_offset + relative_name_offset..) + else { + continue; + }; + if let Some((names, name_payload_len)) = + parse_save_len_prefixed_ascii_name_triplet_and_consumed_len(name_payload) + { + parsed_names = Some((relative_name_offset, name_payload_len, names)); + break; + } + } + let (name_payload_relative_offset, name_payload_len, names) = + parsed_names.unwrap_or((4usize, 0usize, Default::default())); + let (primary_name, secondary_name, tertiary_name) = names; + Some(EmbeddedNameRow { + name_tag_relative_offset, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + name_payload_relative_offset, + name_payload_len, + primary_name: (!primary_name.is_empty()).then_some(primary_name), + secondary_name: (!secondary_name.is_empty()).then_some(secondary_name), + tertiary_name, + }) + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/evidence.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/evidence.rs new file mode 100644 index 0000000..6522134 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/evidence.rs @@ -0,0 +1,161 @@ +use crate::inspect::smp::structures::*; + +pub(super) struct DynamicSideBufferEvidenceInputs<'a> { + pub(super) owner_shared_dword: u32, + pub(super) first_record_child_count_after_owner_shared: Option, + pub(super) first_record_saved_primary_child_byte_after_owner_shared_hex: Option<&'a str>, + pub(super) first_record_first_name_tag_relative_offset_after_owner_shared: Option, + pub(super) first_embedded_primary_name: &'a str, + pub(super) first_embedded_secondary_name: &'a str, + pub(super) first_embedded_tertiary_name: Option<&'a String>, + pub(super) embedded_name_tag_count: usize, + pub(super) embedded_name_row_count: usize, + pub(super) prefix_leading_dword_matching_embedded_profile_tag_count: usize, + pub(super) decoded_embedded_name_row_count: usize, + pub(super) unique_embedded_name_pair_count: usize, + pub(super) decoded_embedded_name_row_with_tertiary_name_count: usize, + pub(super) row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope: usize, + pub(super) row_count_missing_policy_tag_before_next_name: usize, + pub(super) row_count_missing_profile_tag_after_policy: usize, + pub(super) dominant_policy_chunk_len: Option<(usize, usize)>, + pub(super) dominant_profile_chunk_len: Option<(usize, usize)>, + pub(super) short_profile_flag_pair_summary: + Option<&'a SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSummary>, + pub(super) fixed_policy_summary: + Option<&'a SmpSavePlacedStructureDynamicSideBufferFixedPolicySummary>, + pub(super) live_entry_prelude_summary: + Option<&'a SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSummary>, + pub(super) dominant_compact_prefix_pattern: + Option<&'a SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary>, +} + +pub(super) fn build_dynamic_side_buffer_evidence( + inputs: DynamicSideBufferEvidenceInputs<'_>, +) -> Vec { + vec![ + "exact little-endian u32 tag family 0x38a5/0x38a6/0x38a7 appears as a separate save-side tagged collection on grounded saves".to_string(), + format!( + "direct disassembly now shows 0x00493be0 consuming shared owner-local dword 0x{:08x} from the 0x38a6 stream before iterating live infrastructure records", + inputs.owner_shared_dword + ), + format!( + "grounded 0x38a6 record stream then starts the first infrastructure payload with child_count={}, saved_primary_child_byte={}, and first 0x55f1 at relative offset {:?} after that shared dword", + inputs.first_record_child_count_after_owner_shared.unwrap_or_default(), + inputs + .first_record_saved_primary_child_byte_after_owner_shared_hex + .unwrap_or("0x00"), + inputs.first_record_first_name_tag_relative_offset_after_owner_shared + ), + "records payload begins with a compact 6-byte prefix plus one separator byte before the first embedded 0x55f1 name row".to_string(), + "first embedded 0x55f1 row decodes with placed-structure-style dual names, which makes this the strongest current candidate for the separate placed-structure dynamic side-buffer owner seam".to_string(), + format!( + "grounded first embedded names are {:?}/{:?}/{:?} with {} embedded 0x55f1 name rows in the tagged records span", + Some(inputs.first_embedded_primary_name), + Some(inputs.first_embedded_secondary_name), + inputs.first_embedded_tertiary_name, + inputs.embedded_name_tag_count + ), + format!( + "{} of {} embedded name rows use compact leading dword 0x{:08x}, matching the placed-structure embedded profile tag", + inputs.prefix_leading_dword_matching_embedded_profile_tag_count, + inputs.embedded_name_row_count, + u32::from(SAVE_REGION_RECORD_PROFILE_TAG) + ), + format!( + "{} decoded embedded name rows collapse into {} unique placed-structure name pairs; {} rows also expose a third embedded 0x55f1 string", + inputs.decoded_embedded_name_row_count, + inputs.unique_embedded_name_pair_count, + inputs.decoded_embedded_name_row_with_tertiary_name_count + ), + format!( + "{} of {} embedded 0x55f1 rows currently have a complete 0x55f1/0x55f2/0x55f3 envelope before the next name row; {} rows are still missing 0x55f2 and {} rows have 0x55f2 without a later 0x55f3", + inputs.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, + inputs.embedded_name_row_count, + inputs.row_count_missing_policy_tag_before_next_name, + inputs.row_count_missing_profile_tag_after_policy + ), + inputs + .dominant_policy_chunk_len + .map(|(policy_chunk_len, count)| { + format!( + "dominant embedded 0x55f2 policy chunk length is 0x{policy_chunk_len:x} bytes across {count} rows" + ) + }) + .unwrap_or_else(|| "no dominant embedded 0x55f2 policy chunk length was available".to_string()), + inputs + .dominant_profile_chunk_len + .map(|(profile_chunk_len, count)| { + format!( + "dominant embedded 0x55f3 payload-to-next-name span is 0x{profile_chunk_len:x} bytes across {count} rows" + ) + }) + .unwrap_or_else(|| "no dominant embedded 0x55f3 payload-to-next-name span was available".to_string()), + inputs + .short_profile_flag_pair_summary + .and_then(|summary| summary.dominant_flag_pair.as_ref()) + .map(|pair| { + format!( + "direct disassembly now bounds the short trailing lane through 0x52ebd0/0x52ec50 as two serialized flag bytes folded into [this+0x20] bits 0x20/0x40; grounded 0x06-byte rows currently favor {}/{} across {} rows ({} rows total with the short span)", + pair.first_flag_byte_hex, + pair.second_flag_byte_hex, + pair.count, + inputs + .short_profile_flag_pair_summary + .map(|summary| summary.row_count_with_0x06_profile_span) + .unwrap_or_default() + ) + }) + .unwrap_or_else(|| "no dominant short trailing flag-byte pair was available".to_string()), + inputs + .fixed_policy_summary + .map(|summary| { + format!( + "direct disassembly now bounds the fixed 0x55f2 lane through 0x455870/0x455930 as six serialized dwords plus one trailing u16; grounded rows currently keep trailing word {} across {} of {} fixed-policy rows", + summary + .dominant_trailing_word_hex + .as_deref() + .unwrap_or("0x0000"), + summary.dominant_trailing_word_count, + summary.row_count_with_0x1a_policy_chunk + ) + }) + .unwrap_or_else(|| "no fixed 0x55f2 policy summary was available".to_string()), + inputs + .live_entry_prelude_summary + .map(|summary| { + format!( + "decoded {} live-entry directory rows with payload pointers inside the records span; dominant child count={} x{}, dominant saved primary-child byte={} x{}, dominant first 0x55f1 offset={} x{}, and {} rows start the first child callback immediately at payload +0x3", + summary.rows_with_payload_pointer_inside_records_span, + summary.dominant_child_count.unwrap_or_default(), + summary.dominant_child_count_count, + summary + .dominant_saved_primary_child_byte_hex + .as_deref() + .unwrap_or("0x00"), + summary.dominant_saved_primary_child_byte_count, + summary + .dominant_first_name_tag_relative_offset + .unwrap_or_default(), + summary.dominant_first_name_tag_relative_offset_count, + summary.rows_with_first_name_tag_at_offset_3 + ) + }) + .unwrap_or_else(|| "no live-entry prelude summary was available".to_string()), + inputs + .dominant_compact_prefix_pattern + .map(|pattern| { + format!( + "dominant compact prefix pattern {} / {} / {} occurs {} times; section-like rows={}, cap-like rows={}, first names={:?}/{:?}", + pattern.prefix_leading_dword_hex, + pattern.prefix_trailing_word_hex, + pattern.prefix_separator_byte_hex, + pattern.count, + pattern.section_like_primary_name_count, + pattern.cap_like_primary_name_count, + pattern.first_primary_name, + pattern.first_secondary_name + ) + }) + .unwrap_or_else(|| "no dominant compact prefix pattern summary was available".to_string()), + ] +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/live_entry_prelude.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/live_entry_prelude.rs new file mode 100644 index 0000000..144735e --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/live_entry_prelude.rs @@ -0,0 +1,191 @@ +use std::collections::BTreeMap; + +use crate::inspect::smp::structures::*; + +pub(super) fn build_live_entry_prelude_summary( + payload: &[u8], + records_payload: &[u8], + live_id_bound: u32, + live_record_count: u32, +) -> Option { + let bitset_len = ((usize::try_from(live_id_bound).ok()?).saturating_add(8)) / 8; + payload + .get( + INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + ..INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + bitset_len, + ) + .and_then(|bitset| { + let live_entry_ids = + decode_live_entry_ids_from_tombstone_bitset(bitset, live_id_bound)?; + if live_entry_ids.len() != usize::try_from(live_record_count).ok()? { + return None; + } + #[derive(Clone)] + struct LiveEntryPreludeRow { + live_entry_id: u32, + payload_relative_offset: usize, + child_count: u16, + saved_primary_child_byte: u8, + first_payload_dword: u32, + first_name_tag_relative_offset: Option, + first_primary_name: Option, + first_secondary_name: Option, + first_tertiary_name: Option, + } + let mut rows = Vec::new(); + let mut cursor = 0usize; + for live_entry_id in live_entry_ids.iter().copied() { + let payload_len = usize::from(read_u16_at(records_payload, cursor)?); + let payload_relative_offset = cursor.checked_add(2)?; + let payload_end = payload_relative_offset.checked_add(payload_len)?; + let payload_bytes = records_payload.get(payload_relative_offset..payload_end)?; + let child_count = read_u16_at(payload_bytes, 0)?; + let saved_primary_child_byte = read_u8_at(payload_bytes, 2)?; + let first_payload_dword = read_u32_at(payload_bytes, 0)?; + let first_name_tag_relative_offset = (0usize..=16usize).find(|offset| { + read_u16_at(payload_bytes, *offset) == Some(SAVE_REGION_RECORD_NAME_TAG) + }); + let (first_primary_name, first_secondary_name, first_tertiary_name) = + first_name_tag_relative_offset + .and_then(|relative_offset| { + let name_payload = + payload_bytes.get(relative_offset.checked_add(4)?..)?; + parse_save_len_prefixed_ascii_name_triplet(name_payload) + }) + .unwrap_or_default(); + rows.push(LiveEntryPreludeRow { + live_entry_id, + payload_relative_offset, + child_count, + saved_primary_child_byte, + first_payload_dword, + first_name_tag_relative_offset, + first_primary_name: (!first_primary_name.is_empty()) + .then_some(first_primary_name), + first_secondary_name: (!first_secondary_name.is_empty()) + .then_some(first_secondary_name), + first_tertiary_name, + }); + cursor = payload_end; + } + let payload_relative_offset_monotonic = rows.windows(2).all(|window| { + window[0].payload_relative_offset < window[1].payload_relative_offset + }); + let mut child_count_counts = BTreeMap::::new(); + let mut saved_primary_child_byte_counts = BTreeMap::::new(); + let mut first_name_tag_relative_offset_counts = BTreeMap::::new(); + for row in &rows { + *child_count_counts.entry(row.child_count).or_default() += 1; + *saved_primary_child_byte_counts + .entry(row.saved_primary_child_byte) + .or_default() += 1; + if let Some(first_name_tag_relative_offset) = row.first_name_tag_relative_offset { + *first_name_tag_relative_offset_counts + .entry(first_name_tag_relative_offset) + .or_default() += 1; + } + } + let dominant_child_count = child_count_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(value, count)| (*value, *count)); + let dominant_saved_primary_child_byte = saved_primary_child_byte_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(value, count)| (*value, *count)); + let dominant_first_name_tag_relative_offset = first_name_tag_relative_offset_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(value, count)| (*value, *count)); + Some( + SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSummary { + live_entry_directory_row_count: rows.len(), + decoded_live_entry_id_count: live_entry_ids.len(), + payload_relative_offset_monotonic, + rows_with_payload_pointer_inside_records_span: rows.len(), + rows_with_zero_child_count: rows + .iter() + .filter(|row| row.child_count == 0) + .count(), + rows_with_nonzero_child_count: rows + .iter() + .filter(|row| row.child_count != 0) + .count(), + rows_with_first_name_tag_after_prelude: rows + .iter() + .filter(|row| row.first_name_tag_relative_offset.is_some()) + .count(), + rows_with_first_name_tag_at_offset_3: rows + .iter() + .filter(|row| row.first_name_tag_relative_offset == Some(3)) + .count(), + unique_child_count_values: child_count_counts.keys().copied().collect(), + unique_first_name_tag_relative_offsets: first_name_tag_relative_offset_counts + .keys() + .copied() + .collect(), + dominant_child_count: dominant_child_count.map(|(value, _)| value), + dominant_child_count_count: dominant_child_count + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_saved_primary_child_byte: dominant_saved_primary_child_byte + .map(|(value, _)| value), + dominant_saved_primary_child_byte_hex: dominant_saved_primary_child_byte + .map(|(value, _)| format!("0x{value:02x}")), + dominant_saved_primary_child_byte_count: dominant_saved_primary_child_byte + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_first_name_tag_relative_offset: + dominant_first_name_tag_relative_offset.map(|(value, _)| value), + dominant_first_name_tag_relative_offset_count: + dominant_first_name_tag_relative_offset + .map(|(_, count)| count) + .unwrap_or_default(), + sample_rows: rows + .iter() + .take(8) + .enumerate() + .map(|(sample_index, row)| { + SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSample { + sample_index, + live_entry_id: row.live_entry_id, + payload_relative_offset: row.payload_relative_offset as u32, + payload_relative_offset_hex: format!( + "0x{:08x}", + row.payload_relative_offset + ), + payload_relative_to_records: row.payload_relative_offset, + child_count: row.child_count, + child_count_hex: format!("0x{:04x}", row.child_count), + saved_primary_child_byte: row.saved_primary_child_byte, + saved_primary_child_byte_hex: format!( + "0x{:02x}", + row.saved_primary_child_byte + ), + first_payload_dword_hex: format!( + "0x{:08x}", + row.first_payload_dword + ), + first_name_tag_relative_offset: row.first_name_tag_relative_offset, + first_primary_name: row.first_primary_name.clone(), + first_secondary_name: row.first_secondary_name.clone(), + first_tertiary_name: row.first_tertiary_name.clone(), + } + }) + .collect(), + }, + ) + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/mod.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/mod.rs new file mode 100644 index 0000000..2deb064 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/mod.rs @@ -0,0 +1,12 @@ +mod embedded_names; +mod evidence; +mod live_entry_prelude; +mod mode_family; +mod name_prelude; +mod payload_envelope; +mod prefix_patterns; +mod scan; +mod summary; + +pub(in crate::inspect::smp) use scan::parse_save_placed_structure_dynamic_side_buffer_probe; +pub(in crate::inspect::smp) use summary::derive_loaded_placed_structure_dynamic_side_buffer_summary; diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/mode_family/mod.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/mode_family/mod.rs new file mode 100644 index 0000000..4d7e049 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/mode_family/mod.rs @@ -0,0 +1,21 @@ +pub(super) fn classify_side_buffer_mode_family( + primary_name: Option<&str>, + secondary_name: Option<&str>, +) -> &'static str { + let name = primary_name.or(secondary_name).unwrap_or_default(); + if name.starts_with("Bridge") { + "bridge" + } else if name.starts_with("Tunnel") { + "tunnel" + } else if name.starts_with("BallastCap") { + "ballast_cap" + } else if name.starts_with("TrackCap") { + "track_cap" + } else if name.starts_with("Overpass") { + "overpass" + } else if name.is_empty() { + "unknown" + } else { + "other" + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/candidate_rows.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/candidate_rows.rs new file mode 100644 index 0000000..3d3af9c --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/candidate_rows.rs @@ -0,0 +1,54 @@ +use super::super::super::*; +use super::super::embedded_names::EmbeddedNameRow; +use super::super::payload_envelope::PayloadEnvelopeRow; + +pub(super) type NamePreludeCandidateRow = ( + usize, + Option, + Option, + u32, + u16, + u8, + u16, + u8, + Option, + Option<(u8, u8)>, +); + +pub(super) fn build_name_prelude_candidate_rows( + records_payload: &[u8], + payload_envelope_rows: &[PayloadEnvelopeRow], + embedded_name_rows: &[EmbeddedNameRow], +) -> Vec { + embedded_name_rows + .iter() + .enumerate() + .filter_map(|(row_index, row)| { + let candidate_offset = row.name_tag_relative_offset.checked_sub(3)?; + let child_count_candidate = read_u16_at(records_payload, candidate_offset)?; + let saved_primary_child_byte_candidate = + read_u8_at(records_payload, candidate_offset + 2)?; + Some(( + row.name_tag_relative_offset, + row.primary_name.clone(), + row.secondary_name.clone(), + row.prefix_leading_dword, + row.prefix_trailing_word, + row.prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + row_index.checked_sub(1).and_then(|previous_index| { + payload_envelope_rows + .get(previous_index) + .and_then(|row| row.profile_chunk_len_to_next_name_or_end) + }), + row_index.checked_sub(1).and_then(|previous_index| { + payload_envelope_rows.get(previous_index).and_then(|row| { + row.short_profile_first_flag_byte + .zip(row.short_profile_second_flag_byte) + }) + }), + )) + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/candidate_summary.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/candidate_summary.rs new file mode 100644 index 0000000..6932a1c --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/candidate_summary.rs @@ -0,0 +1,139 @@ +use super::super::super::*; +use super::super::embedded_names::EmbeddedNameRow; +use super::super::payload_envelope::PayloadEnvelopeRow; +use super::candidate_rows::build_name_prelude_candidate_rows; +use super::correlations::{ + build_candidate_pattern_correlations, build_compact_prefix_correlations, + build_profile_span_correlations, +}; +use std::collections::BTreeMap; + +pub(in crate::inspect::smp) fn build_name_prelude_candidate_summary( + records_payload: &[u8], + payload_envelope_rows: &[PayloadEnvelopeRow], + embedded_name_rows: &[EmbeddedNameRow], +) -> Option { + let name_prelude_candidate_rows = build_name_prelude_candidate_rows( + records_payload, + payload_envelope_rows, + embedded_name_rows, + ); + let mut name_prelude_candidate_pattern_counts = BTreeMap::<(u16, u8), usize>::new(); + let mut name_prelude_child_count_counts = BTreeMap::::new(); + let mut name_prelude_saved_primary_counts = BTreeMap::::new(); + for (_, _, _, _, _, _, child_count_candidate, saved_primary_child_byte_candidate, _, _) in + &name_prelude_candidate_rows + { + *name_prelude_candidate_pattern_counts + .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) + .or_default() += 1; + *name_prelude_child_count_counts + .entry(*child_count_candidate) + .or_default() += 1; + *name_prelude_saved_primary_counts + .entry(*saved_primary_child_byte_candidate) + .or_default() += 1; + } + let dominant_name_prelude_candidate_pattern = name_prelude_candidate_pattern_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map( + |((child_count_candidate, saved_primary_child_byte_candidate), count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), + saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + count: *count, + } + }, + ); + let dominant_name_prelude_child_count = name_prelude_child_count_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(value, count)| (*value, *count)); + let dominant_name_prelude_saved_primary = name_prelude_saved_primary_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(value, count)| (*value, *count)); + + Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSummary { + row_count_with_candidate_window: name_prelude_candidate_rows.len(), + unique_candidate_pattern_count: name_prelude_candidate_pattern_counts.len(), + dominant_child_count_candidate: dominant_name_prelude_child_count + .map(|(value, _)| value), + dominant_child_count_candidate_count: dominant_name_prelude_child_count + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_saved_primary_child_byte_candidate: dominant_name_prelude_saved_primary + .map(|(value, _)| value), + dominant_saved_primary_child_byte_candidate_hex: dominant_name_prelude_saved_primary + .map(|(value, _)| format!("0x{value:02x}")), + dominant_saved_primary_child_byte_candidate_count: dominant_name_prelude_saved_primary + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_candidate_pattern: dominant_name_prelude_candidate_pattern.clone(), + candidate_pattern_correlations: build_candidate_pattern_correlations( + &name_prelude_candidate_rows, + ), + profile_span_correlations: build_profile_span_correlations( + &name_prelude_candidate_rows, + ), + compact_prefix_correlations: build_compact_prefix_correlations( + &name_prelude_candidate_rows, + ), + sample_rows: name_prelude_candidate_rows + .iter() + .take(8) + .enumerate() + .map( + |( + sample_index, + ( + name_tag_relative_offset, + primary_name, + secondary_name, + _, + _, + _, + child_count_candidate, + saved_primary_child_byte_candidate, + previous_profile_chunk_len_to_next_name_or_end, + _, + ), + )| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSample { + sample_index, + name_tag_relative_offset: *name_tag_relative_offset, + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), + saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + previous_profile_chunk_len_to_next_name_or_end: + *previous_profile_chunk_len_to_next_name_or_end, + } + }, + ) + .collect(), + }, + ) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/candidate_pattern.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/candidate_pattern.rs new file mode 100644 index 0000000..c72e1f2 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/candidate_pattern.rs @@ -0,0 +1,162 @@ +use super::super::super::super::*; +use super::super::super::mode_family::classify_side_buffer_mode_family; +use super::super::candidate_rows::NamePreludeCandidateRow; +use std::collections::BTreeMap; + +pub(in crate::inspect::smp) fn build_candidate_pattern_correlations( + name_prelude_candidate_rows: &[NamePreludeCandidateRow], +) -> Vec { + let mut name_prelude_pattern_groups = + BTreeMap::<(u16, u8), Vec<(usize, Option, Option, Option)>>::new(); + for ( + name_tag_relative_offset, + primary_name, + secondary_name, + _prefix_leading_dword, + _prefix_trailing_word, + _prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + previous_span, + _previous_short_profile_flag_pair, + ) in name_prelude_candidate_rows + { + name_prelude_pattern_groups + .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) + .or_default() + .push(( + *name_tag_relative_offset, + primary_name.clone(), + secondary_name.clone(), + *previous_span, + )); + } + name_prelude_pattern_groups + .into_iter() + .map( + |((child_count_candidate, saved_primary_child_byte_candidate), rows)| { + let mut name_pair_counts = + BTreeMap::<(Option, Option), usize>::new(); + let mut profile_span_counts = BTreeMap::::new(); + let mut mode_family_counts = BTreeMap::::new(); + for (_, primary_name, secondary_name, previous_span) in &rows { + *name_pair_counts + .entry((primary_name.clone(), secondary_name.clone())) + .or_default() += 1; + *mode_family_counts + .entry( + classify_side_buffer_mode_family( + primary_name.as_deref(), + secondary_name.as_deref(), + ) + .to_string(), + ) + .or_default() += 1; + if let Some(previous_span) = previous_span { + *profile_span_counts.entry(*previous_span).or_default() += 1; + } + } + let dominant_name_pair = name_pair_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|((primary_name, secondary_name), count)| { + (primary_name.clone(), secondary_name.clone(), *count) + }); + let dominant_profile_span = profile_span_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(span, count)| (*span, *count)); + let dominant_mode_family = mode_family_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(mode_family, count)| (mode_family.clone(), *count)); + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePatternCorrelation { + child_count_candidate, + child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), + saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + row_count: rows.len(), + unique_name_pair_count: name_pair_counts.len(), + unique_profile_span_count: profile_span_counts.len(), + dominant_primary_name: dominant_name_pair + .as_ref() + .and_then(|(primary_name, _, _)| primary_name.clone()), + dominant_secondary_name: dominant_name_pair + .as_ref() + .and_then(|(_, secondary_name, _)| secondary_name.clone()), + dominant_name_pair_count: dominant_name_pair + .map(|(_, _, count)| count) + .unwrap_or_default(), + dominant_profile_span: dominant_profile_span + .map(|(profile_span, _)| profile_span), + dominant_profile_span_count: dominant_profile_span + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_mode_family: dominant_mode_family + .as_ref() + .map(|(mode_family, _)| mode_family.clone()), + dominant_mode_family_count: dominant_mode_family + .map(|(_, count)| count) + .unwrap_or_default(), + mode_family_counts: mode_family_counts + .into_iter() + .map(|(mode_family, count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family, + count, + } + }) + .collect(), + sample_rows: rows + .iter() + .take(8) + .enumerate() + .map( + |( + sample_index, + ( + name_tag_relative_offset, + primary_name, + secondary_name, + previous_profile_chunk_len_to_next_name_or_end, + ), + )| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSample { + sample_index, + name_tag_relative_offset: *name_tag_relative_offset, + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + child_count_candidate, + child_count_candidate_hex: format!( + "0x{child_count_candidate:04x}" + ), + saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + previous_profile_chunk_len_to_next_name_or_end: + *previous_profile_chunk_len_to_next_name_or_end, + } + }, + ) + .collect(), + } + }, + ) + .take(8) + .collect::>() +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/compact_prefix.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/compact_prefix.rs new file mode 100644 index 0000000..01a2bd5 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/compact_prefix.rs @@ -0,0 +1,234 @@ +use super::super::super::super::*; +use super::super::super::mode_family::classify_side_buffer_mode_family; +use super::super::candidate_rows::NamePreludeCandidateRow; +use std::collections::BTreeMap; + +pub(in crate::inspect::smp) fn build_compact_prefix_correlations( + name_prelude_candidate_rows: &[NamePreludeCandidateRow], +) -> Vec { + let mut name_prelude_compact_prefix_groups = BTreeMap::< + (u32, u16, u8), + Vec<( + usize, + Option, + Option, + u16, + u8, + Option, + Option<(u8, u8)>, + )>, + >::new(); + for ( + name_tag_relative_offset, + primary_name, + secondary_name, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + previous_span, + previous_short_profile_flag_pair, + ) in name_prelude_candidate_rows + { + name_prelude_compact_prefix_groups + .entry(( + *prefix_leading_dword, + *prefix_trailing_word, + *prefix_separator_byte, + )) + .or_default() + .push(( + *name_tag_relative_offset, + primary_name.clone(), + secondary_name.clone(), + *child_count_candidate, + *saved_primary_child_byte_candidate, + *previous_span, + *previous_short_profile_flag_pair, + )); + } + name_prelude_compact_prefix_groups + .into_iter() + .map(|((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), rows)| { + let mut name_pair_counts = BTreeMap::<(Option, Option), usize>::new(); + let mut profile_span_counts = BTreeMap::::new(); + let mut candidate_pattern_counts = BTreeMap::<(u16, u8), usize>::new(); + let mut mode_family_counts = BTreeMap::::new(); + let mut flag_pair_counts = BTreeMap::<(u8, u8), usize>::new(); + for ( + _name_tag_relative_offset, + primary_name, + secondary_name, + child_count_candidate, + saved_primary_child_byte_candidate, + previous_span, + previous_short_profile_flag_pair, + ) in &rows + { + *name_pair_counts + .entry((primary_name.clone(), secondary_name.clone())) + .or_default() += 1; + if let Some(previous_span) = previous_span { + *profile_span_counts.entry(*previous_span).or_default() += 1; + } + *candidate_pattern_counts + .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) + .or_default() += 1; + *mode_family_counts + .entry( + classify_side_buffer_mode_family( + primary_name.as_deref(), + secondary_name.as_deref(), + ) + .to_string(), + ) + .or_default() += 1; + if let Some((first_flag_byte, second_flag_byte)) = previous_short_profile_flag_pair + { + *flag_pair_counts + .entry((*first_flag_byte, *second_flag_byte)) + .or_default() += 1; + } + } + let dominant_name_pair = name_pair_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|((primary_name, secondary_name), count)| { + (primary_name.clone(), secondary_name.clone(), *count) + }); + let dominant_profile_span = profile_span_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(span, count)| (*span, *count)); + let dominant_candidate_pattern = candidate_pattern_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|((child_count_candidate, saved_primary_child_byte_candidate), count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), + saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + count: *count, + } + }); + let dominant_mode_family = mode_family_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(mode_family, count)| (mode_family.clone(), *count)); + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { + prefix_leading_dword, + prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), + prefix_trailing_word, + prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), + prefix_separator_byte, + prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), + row_count: rows.len(), + unique_name_pair_count: name_pair_counts.len(), + unique_profile_span_count: profile_span_counts.len(), + dominant_primary_name: dominant_name_pair + .as_ref() + .and_then(|(primary_name, _, _)| primary_name.clone()), + dominant_secondary_name: dominant_name_pair + .as_ref() + .and_then(|(_, secondary_name, _)| secondary_name.clone()), + dominant_name_pair_count: dominant_name_pair + .map(|(_, _, count)| count) + .unwrap_or_default(), + dominant_profile_span: dominant_profile_span.map(|(profile_span, _)| profile_span), + dominant_profile_span_count: dominant_profile_span + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_candidate_pattern, + dominant_mode_family: dominant_mode_family + .as_ref() + .map(|(mode_family, _)| mode_family.clone()), + dominant_mode_family_count: dominant_mode_family + .map(|(_, count)| count) + .unwrap_or_default(), + mode_family_counts: mode_family_counts + .into_iter() + .map(|(mode_family, count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family, + count, + } + }) + .collect(), + name_pair_summaries: name_pair_counts + .into_iter() + .map(|((primary_name, secondary_name), count)| { + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { + primary_name, + secondary_name, + count, + } + }) + .collect(), + profile_span_counts: profile_span_counts + .into_iter() + .map(|(previous_profile_chunk_len_to_next_name_or_end, count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { + previous_profile_chunk_len_to_next_name_or_end, + count, + } + }) + .collect(), + rows_with_previous_short_profile_flag_pair: flag_pair_counts.values().copied().sum(), + previous_short_profile_flag_pair_counts: flag_pair_counts + .into_iter() + .map(|((first_flag_byte, second_flag_byte), count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { + first_flag_byte, + first_flag_byte_hex: format!("0x{first_flag_byte:02x}"), + second_flag_byte, + second_flag_byte_hex: format!("0x{second_flag_byte:02x}"), + count, + } + }) + .collect(), + sample_rows: rows + .iter() + .take(8) + .enumerate() + .map(|(sample_index, (name_tag_relative_offset, primary_name, secondary_name, child_count_candidate, saved_primary_child_byte_candidate, previous_profile_chunk_len_to_next_name_or_end, _))| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { + sample_index, + name_tag_relative_offset: *name_tag_relative_offset, + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), + saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + previous_profile_chunk_len_to_next_name_or_end: + *previous_profile_chunk_len_to_next_name_or_end, + } + }) + .collect(), + } + }) + .take(8) + .collect::>() +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/mod.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/mod.rs new file mode 100644 index 0000000..0518354 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/mod.rs @@ -0,0 +1,7 @@ +mod candidate_pattern; +mod compact_prefix; +mod profile_span; + +pub(super) use candidate_pattern::build_candidate_pattern_correlations; +pub(super) use compact_prefix::build_compact_prefix_correlations; +pub(super) use profile_span::build_profile_span_correlations; diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/profile_span.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/profile_span.rs new file mode 100644 index 0000000..f662f07 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/correlations/profile_span.rs @@ -0,0 +1,219 @@ +use super::super::super::super::*; +use super::super::super::mode_family::classify_side_buffer_mode_family; +use super::super::candidate_rows::NamePreludeCandidateRow; +use std::collections::BTreeMap; + +pub(in crate::inspect::smp) fn build_profile_span_correlations( + name_prelude_candidate_rows: &[NamePreludeCandidateRow], +) -> Vec { + let mut name_prelude_profile_span_groups = BTreeMap::< + usize, + Vec<(usize, Option, Option, u32, u16, u8, u16, u8)>, + >::new(); + for ( + name_tag_relative_offset, + primary_name, + secondary_name, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + previous_span, + _previous_short_profile_flag_pair, + ) in name_prelude_candidate_rows + { + if let Some(previous_span) = previous_span { + name_prelude_profile_span_groups + .entry(*previous_span) + .or_default() + .push(( + *name_tag_relative_offset, + primary_name.clone(), + secondary_name.clone(), + *prefix_leading_dword, + *prefix_trailing_word, + *prefix_separator_byte, + *child_count_candidate, + *saved_primary_child_byte_candidate, + )); + } + } + name_prelude_profile_span_groups + .into_iter() + .map(|(previous_profile_chunk_len_to_next_name_or_end, rows)| { + let mut pattern_counts = BTreeMap::<(u16, u8), usize>::new(); + let mut child_count_counts = BTreeMap::::new(); + let mut saved_primary_counts = BTreeMap::::new(); + let mut mode_family_counts = BTreeMap::::new(); + let mut prefix_counts = BTreeMap::<(u32, u16, u8), usize>::new(); + for ( + _name_tag_relative_offset, + primary_name, + secondary_name, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + ) in &rows + { + *pattern_counts + .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) + .or_default() += 1; + *child_count_counts.entry(*child_count_candidate).or_default() += 1; + *saved_primary_counts.entry(*saved_primary_child_byte_candidate).or_default() += 1; + *prefix_counts + .entry((*prefix_leading_dword, *prefix_trailing_word, *prefix_separator_byte)) + .or_default() += 1; + *mode_family_counts + .entry( + classify_side_buffer_mode_family( + primary_name.as_deref(), + secondary_name.as_deref(), + ) + .to_string(), + ) + .or_default() += 1; + } + let dominant_pattern = pattern_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|((child_count_candidate, saved_primary_child_byte_candidate), count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), + saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + count: *count, + } + }); + let dominant_child_count = child_count_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(value, count)| (*value, *count)); + let dominant_saved_primary = saved_primary_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(value, count)| (*value, *count)); + let dominant_mode_family = mode_family_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(mode_family, count)| (mode_family.clone(), *count)); + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { + previous_profile_chunk_len_to_next_name_or_end, + row_count: rows.len(), + dominant_child_count_candidate: dominant_child_count.map(|(value, _)| value), + dominant_child_count_candidate_count: dominant_child_count + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_saved_primary_child_byte_candidate: dominant_saved_primary + .map(|(value, _)| value), + dominant_saved_primary_child_byte_candidate_hex: dominant_saved_primary + .map(|(value, _)| format!("0x{value:02x}")), + dominant_saved_primary_child_byte_candidate_count: dominant_saved_primary + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_candidate_pattern: dominant_pattern, + dominant_mode_family: dominant_mode_family + .as_ref() + .map(|(mode_family, _)| mode_family.clone()), + dominant_mode_family_count: dominant_mode_family + .map(|(_, count)| count) + .unwrap_or_default(), + mode_family_counts: mode_family_counts + .into_iter() + .map(|(mode_family, count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family, + count, + } + }) + .collect(), + compact_prefix_pattern_summaries: prefix_counts + .into_iter() + .map( + |((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { + prefix_leading_dword, + prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), + prefix_trailing_word, + prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), + prefix_separator_byte, + prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), + count, + } + }, + ) + .collect(), + sample_rows: rows + .iter() + .take(8) + .enumerate() + .map( + |( + sample_index, + ( + name_tag_relative_offset, + primary_name, + secondary_name, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + ), + )| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { + sample_index, + name_tag_relative_offset: *name_tag_relative_offset, + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + prefix_leading_dword: *prefix_leading_dword, + prefix_leading_dword_hex: format!( + "0x{prefix_leading_dword:08x}" + ), + prefix_trailing_word: *prefix_trailing_word, + prefix_trailing_word_hex: format!( + "0x{prefix_trailing_word:04x}" + ), + prefix_separator_byte: *prefix_separator_byte, + prefix_separator_byte_hex: format!( + "0x{prefix_separator_byte:02x}" + ), + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: format!( + "0x{child_count_candidate:04x}" + ), + saved_primary_child_byte_candidate: + *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + } + }, + ) + .collect(), + } + }) + .take(8) + .collect::>() +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/mod.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/mod.rs new file mode 100644 index 0000000..651a780 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/mod.rs @@ -0,0 +1,9 @@ +mod candidate_rows; +mod candidate_summary; +mod correlations; +mod parse; +mod profile_span; + +pub(super) use candidate_summary::build_name_prelude_candidate_summary; +pub(super) use parse::{FirstRecordNamePrelude, parse_first_record_name_prelude}; +pub(super) use profile_span::build_dominant_profile_span_class_summary; diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/parse.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/parse.rs new file mode 100644 index 0000000..c5cb34b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/parse.rs @@ -0,0 +1,65 @@ +use super::super::super::*; + +pub(in crate::inspect::smp) struct FirstRecordNamePrelude { + pub(in crate::inspect::smp) owner_shared_dword: u32, + pub(in crate::inspect::smp) owner_shared_dword_relative_offset: usize, + pub(in crate::inspect::smp) first_record_child_count_after_owner_shared: Option, + pub(in crate::inspect::smp) first_record_saved_primary_child_byte_after_owner_shared: + Option, + pub(in crate::inspect::smp) first_record_first_name_tag_relative_offset_after_owner_shared: + Option, + pub(in crate::inspect::smp) prefix_leading_dword: u32, + pub(in crate::inspect::smp) prefix_trailing_word: u16, + pub(in crate::inspect::smp) prefix_separator_byte: u8, + pub(in crate::inspect::smp) first_embedded_primary_name: String, + pub(in crate::inspect::smp) first_embedded_secondary_name: String, + pub(in crate::inspect::smp) first_embedded_tertiary_name: Option, +} + +pub(in crate::inspect::smp) fn parse_first_record_name_prelude( + records_payload: &[u8], + first_embedded_name_tag_relative_offset: usize, +) -> Option { + let prefix_payload = records_payload.get(..first_embedded_name_tag_relative_offset)?; + if prefix_payload.len() < 7 { + return None; + } + let owner_shared_dword = read_u32_at(prefix_payload, 0)?; + let owner_shared_dword_relative_offset = 0usize; + let first_record_child_count_after_owner_shared = read_u16_at(records_payload, 4); + let first_record_saved_primary_child_byte_after_owner_shared = read_u8_at(records_payload, 6); + let first_record_first_name_tag_relative_offset_after_owner_shared = + (3usize..=16usize).find(|offset| { + read_u16_at(records_payload, 4 + *offset) == Some(SAVE_REGION_RECORD_NAME_TAG) + }); + let prefix_leading_dword = owner_shared_dword; + let prefix_trailing_word = read_u16_at(prefix_payload, 4)?; + let prefix_separator_byte = prefix_payload.get(6).copied()?; + let mut parsed_embedded_names = None; + for relative_name_offset in [4usize, 6usize] { + let Some(name_payload) = + records_payload.get(first_embedded_name_tag_relative_offset + relative_name_offset..) + else { + continue; + }; + if let Some(names) = parse_save_len_prefixed_ascii_name_triplet(name_payload) { + parsed_embedded_names = Some(names); + break; + } + } + let (first_embedded_primary_name, first_embedded_secondary_name, first_embedded_tertiary_name) = + parsed_embedded_names?; + Some(FirstRecordNamePrelude { + owner_shared_dword, + owner_shared_dword_relative_offset, + first_record_child_count_after_owner_shared, + first_record_saved_primary_child_byte_after_owner_shared, + first_record_first_name_tag_relative_offset_after_owner_shared, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + first_embedded_primary_name, + first_embedded_secondary_name, + first_embedded_tertiary_name, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/profile_span.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/profile_span.rs new file mode 100644 index 0000000..7de7179 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/name_prelude/profile_span.rs @@ -0,0 +1,237 @@ +use super::super::super::*; +use super::super::embedded_names::EmbeddedNameRow; +use super::super::payload_envelope::PayloadEnvelopeRow; +use std::collections::BTreeMap; + +pub(in crate::inspect::smp) fn build_dominant_profile_span_class_summary( + records_payload: &[u8], + payload_envelope_rows: &[PayloadEnvelopeRow], + embedded_name_rows: &[EmbeddedNameRow], + dominant_profile_chunk_len: Option<(usize, usize)>, +) -> Option { + let (dominant_profile_span_len, _) = dominant_profile_chunk_len?; + let dominant_rows = embedded_name_rows + .iter() + .zip(payload_envelope_rows.iter()) + .filter_map(|(name_row, envelope_row)| { + (envelope_row.profile_chunk_len_to_next_name_or_end == Some(dominant_profile_span_len)) + .then(|| { + let candidate_offset = name_row.name_tag_relative_offset.checked_sub(3); + let child_count_candidate = + candidate_offset.and_then(|offset| read_u16_at(records_payload, offset)); + let saved_primary_child_byte_candidate = + candidate_offset.and_then(|offset| read_u8_at(records_payload, offset + 2)); + ( + name_row.name_tag_relative_offset, + name_row.primary_name.clone(), + name_row.secondary_name.clone(), + name_row.prefix_leading_dword, + name_row.prefix_trailing_word, + name_row.prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + ) + }) + }) + .collect::>(); + let mut dominant_name_pair_counts = BTreeMap::<(Option, Option), usize>::new(); + let mut dominant_prefix_counts = BTreeMap::<(u32, u16, u8), usize>::new(); + let mut dominant_candidate_pattern_counts = BTreeMap::<(u16, u8), usize>::new(); + for ( + _, + primary_name, + secondary_name, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + ) in &dominant_rows + { + *dominant_name_pair_counts + .entry((primary_name.clone(), secondary_name.clone())) + .or_default() += 1; + *dominant_prefix_counts + .entry(( + *prefix_leading_dword, + *prefix_trailing_word, + *prefix_separator_byte, + )) + .or_default() += 1; + if let (Some(child_count_candidate), Some(saved_primary_child_byte_candidate)) = + (child_count_candidate, saved_primary_child_byte_candidate) + { + *dominant_candidate_pattern_counts + .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) + .or_default() += 1; + } + } + let dominant_name_pair = dominant_name_pair_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|((primary_name, secondary_name), count)| { + (primary_name.clone(), secondary_name.clone(), *count) + }); + let dominant_prefix = dominant_prefix_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map( + |((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), count)| { + ( + *prefix_leading_dword, + *prefix_trailing_word, + *prefix_separator_byte, + *count, + ) + }, + ); + let dominant_candidate_pattern = dominant_candidate_pattern_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map( + |((child_count_candidate, saved_primary_child_byte_candidate), count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), + saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + count: *count, + } + }, + ); + let name_pair_summaries = dominant_name_pair_counts + .iter() + .map(|((primary_name, secondary_name), count)| { + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + count: *count, + } + }) + .take(8) + .collect::>(); + let compact_prefix_pattern_summaries = dominant_prefix_counts + .iter() + .map( + |((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), count)| { + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanPrefixSummary { + prefix_leading_dword: *prefix_leading_dword, + prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), + prefix_trailing_word: *prefix_trailing_word, + prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), + prefix_separator_byte: *prefix_separator_byte, + prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), + count: *count, + } + }, + ) + .take(8) + .collect::>(); + let candidate_pattern_summaries = dominant_candidate_pattern_counts + .iter() + .map( + |((child_count_candidate, saved_primary_child_byte_candidate), count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), + saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: format!( + "0x{saved_primary_child_byte_candidate:02x}" + ), + count: *count, + } + }, + ) + .take(8) + .collect::>(); + Some( + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanClassSummary { + profile_chunk_len_to_next_name_or_end: dominant_profile_span_len, + row_count: dominant_rows.len(), + unique_name_pair_count: dominant_name_pair_counts.len(), + unique_compact_prefix_pattern_count: dominant_prefix_counts.len(), + dominant_candidate_pattern, + dominant_primary_name: dominant_name_pair + .as_ref() + .and_then(|(primary_name, _, _)| primary_name.clone()), + dominant_secondary_name: dominant_name_pair + .as_ref() + .and_then(|(_, secondary_name, _)| secondary_name.clone()), + dominant_name_pair_count: dominant_name_pair + .map(|(_, _, count)| count) + .unwrap_or_default(), + dominant_prefix_leading_dword: dominant_prefix + .map(|(prefix_leading_dword, _, _, _)| prefix_leading_dword), + dominant_prefix_leading_dword_hex: dominant_prefix + .map(|(prefix_leading_dword, _, _, _)| format!("0x{prefix_leading_dword:08x}")), + dominant_prefix_trailing_word: dominant_prefix + .map(|(_, prefix_trailing_word, _, _)| prefix_trailing_word), + dominant_prefix_trailing_word_hex: dominant_prefix + .map(|(_, prefix_trailing_word, _, _)| format!("0x{prefix_trailing_word:04x}")), + dominant_prefix_separator_byte: dominant_prefix + .map(|(_, _, prefix_separator_byte, _)| prefix_separator_byte), + dominant_prefix_separator_byte_hex: dominant_prefix + .map(|(_, _, prefix_separator_byte, _)| format!("0x{prefix_separator_byte:02x}")), + dominant_prefix_count: dominant_prefix + .map(|(_, _, _, count)| count) + .unwrap_or_default(), + sample_rows: dominant_rows + .iter() + .take(8) + .enumerate() + .map( + |( + sample_index, + ( + name_tag_relative_offset, + primary_name, + secondary_name, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + child_count_candidate, + saved_primary_child_byte_candidate, + ), + )| { + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanSample { + sample_index, + name_tag_relative_offset: *name_tag_relative_offset, + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + prefix_leading_dword: *prefix_leading_dword, + prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), + prefix_trailing_word: *prefix_trailing_word, + prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), + prefix_separator_byte: *prefix_separator_byte, + prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), + child_count_candidate: *child_count_candidate, + child_count_candidate_hex: child_count_candidate + .map(|value| format!("0x{value:04x}")), + saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, + saved_primary_child_byte_candidate_hex: + saved_primary_child_byte_candidate + .map(|value| format!("0x{value:02x}")), + } + }, + ) + .collect(), + name_pair_summaries, + compact_prefix_pattern_summaries, + candidate_pattern_summaries, + }, + ) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/fixed_policy.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/fixed_policy.rs new file mode 100644 index 0000000..38e67f9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/fixed_policy.rs @@ -0,0 +1,324 @@ +use super::super::embedded_names::EmbeddedNameRow; +use super::rows::PayloadEnvelopeRow; +use crate::inspect::smp::common::{read_u16_at, read_u32_at}; +use crate::inspect::smp::structures::dynamic_side_buffer::mode_family::classify_side_buffer_mode_family; +use crate::inspect::smp::structures::{ + SmpSavePlacedStructureDynamicSideBufferFixedPolicyCompactPrefixCorrelation, + SmpSavePlacedStructureDynamicSideBufferFixedPolicySample, + SmpSavePlacedStructureDynamicSideBufferFixedPolicySummary, + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount, +}; +use std::collections::BTreeMap; + +pub(crate) fn build_fixed_policy_summary( + records_payload: &[u8], + payload_envelope_rows: &[PayloadEnvelopeRow], + embedded_name_rows: &[EmbeddedNameRow], +) -> Option { + let fixed_policy_rows = payload_envelope_rows + .iter() + .filter(|row| row.policy_chunk_len == Some(0x1a)) + .filter_map(|row| { + let embedded_name_row = embedded_name_rows + .iter() + .find(|entry| entry.name_tag_relative_offset == row.name_tag_relative_offset)?; + let policy_tag_relative_offset = row.policy_tag_relative_offset?; + let policy_payload = records_payload + .get(policy_tag_relative_offset + 4..policy_tag_relative_offset + 4 + 0x1a)?; + let first_triplet_dwords = [ + read_u32_at(policy_payload, 0)?, + read_u32_at(policy_payload, 4)?, + read_u32_at(policy_payload, 8)?, + ]; + let second_triplet_dwords = [ + read_u32_at(policy_payload, 12)?, + read_u32_at(policy_payload, 16)?, + read_u32_at(policy_payload, 20)?, + ]; + let trailing_word = read_u16_at(policy_payload, 24)?; + Some(( + row.name_tag_relative_offset, + row.primary_name.clone(), + row.secondary_name.clone(), + embedded_name_row.prefix_leading_dword, + embedded_name_row.prefix_trailing_word, + embedded_name_row.prefix_separator_byte, + first_triplet_dwords, + second_triplet_dwords, + trailing_word, + )) + }) + .collect::>(); + let mut fixed_policy_trailing_word_counts = BTreeMap::::new(); + for (_, _, _, _, _, _, _, _, trailing_word) in &fixed_policy_rows { + *fixed_policy_trailing_word_counts + .entry(*trailing_word) + .or_default() += 1; + } + let dominant_fixed_policy_trailing_word = fixed_policy_trailing_word_counts + .iter() + .max_by(|(left_word, left_count), (right_word, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_word.cmp(left_word)) + }) + .map(|(word, count)| (*word, *count)); + let mut fixed_policy_compact_prefix_groups = BTreeMap::< + (u32, u16, u8), + Vec<( + usize, + Option, + Option, + [u32; 3], + [u32; 3], + u16, + )>, + >::new(); + for ( + name_tag_relative_offset, + primary_name, + secondary_name, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + first_triplet_dwords, + second_triplet_dwords, + trailing_word, + ) in &fixed_policy_rows + { + fixed_policy_compact_prefix_groups + .entry(( + *prefix_leading_dword, + *prefix_trailing_word, + *prefix_separator_byte, + )) + .or_default() + .push(( + *name_tag_relative_offset, + primary_name.clone(), + secondary_name.clone(), + *first_triplet_dwords, + *second_triplet_dwords, + *trailing_word, + )); + } + let fixed_policy_compact_prefix_correlations = fixed_policy_compact_prefix_groups + .into_iter() + .map( + |((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), rows)| { + let mut name_pair_counts = + BTreeMap::<(Option, Option), usize>::new(); + let mut mode_family_counts = BTreeMap::::new(); + let mut policy_tuple_counts = BTreeMap::<([u32; 3], [u32; 3], u16), usize>::new(); + for ( + _, + primary_name, + secondary_name, + first_triplet_dwords, + second_triplet_dwords, + trailing_word, + ) in &rows + { + *name_pair_counts + .entry((primary_name.clone(), secondary_name.clone())) + .or_default() += 1; + *mode_family_counts + .entry( + classify_side_buffer_mode_family( + primary_name.as_deref(), + secondary_name.as_deref(), + ) + .to_string(), + ) + .or_default() += 1; + *policy_tuple_counts + .entry(( + *first_triplet_dwords, + *second_triplet_dwords, + *trailing_word, + )) + .or_default() += 1; + } + let dominant_name_pair = name_pair_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|((primary_name, secondary_name), count)| { + (primary_name.clone(), secondary_name.clone(), *count) + }); + let dominant_mode_family = mode_family_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map(|(mode_family, count)| (mode_family.clone(), *count)); + let dominant_policy_tuple = policy_tuple_counts + .iter() + .max_by(|(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }) + .map( + |((first_triplet_dwords, second_triplet_dwords, trailing_word), count)| { + ( + *first_triplet_dwords, + *second_triplet_dwords, + *trailing_word, + *count, + ) + }, + ); + SmpSavePlacedStructureDynamicSideBufferFixedPolicyCompactPrefixCorrelation { + prefix_leading_dword, + prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), + prefix_trailing_word, + prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), + prefix_separator_byte, + prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), + row_count: rows.len(), + unique_policy_tuple_count: policy_tuple_counts.len(), + dominant_primary_name: dominant_name_pair + .as_ref() + .and_then(|(primary_name, _, _)| primary_name.clone()), + dominant_secondary_name: dominant_name_pair + .as_ref() + .and_then(|(_, secondary_name, _)| secondary_name.clone()), + dominant_name_pair_count: dominant_name_pair + .map(|(_, _, count)| count) + .unwrap_or_default(), + dominant_mode_family: dominant_mode_family + .as_ref() + .map(|(mode_family, _)| mode_family.clone()), + dominant_mode_family_count: dominant_mode_family + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_first_triplet_dwords_hex: dominant_policy_tuple + .as_ref() + .map(|(first_triplet_dwords, _, _, _)| { + first_triplet_dwords + .iter() + .map(|value| format!("0x{value:08x}")) + .collect() + }) + .unwrap_or_default(), + dominant_second_triplet_dwords_hex: dominant_policy_tuple + .as_ref() + .map(|(_, second_triplet_dwords, _, _)| { + second_triplet_dwords + .iter() + .map(|value| format!("0x{value:08x}")) + .collect() + }) + .unwrap_or_default(), + dominant_trailing_word: dominant_policy_tuple + .map(|(_, _, trailing_word, _)| trailing_word), + dominant_trailing_word_hex: dominant_policy_tuple + .map(|(_, _, trailing_word, _)| format!("0x{trailing_word:04x}")), + dominant_policy_tuple_count: dominant_policy_tuple + .map(|(_, _, _, count)| count) + .unwrap_or_default(), + mode_family_counts: mode_family_counts + .into_iter() + .map(|(mode_family, count)| { + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family, + count, + } + }) + .collect(), + sample_rows: rows + .iter() + .take(8) + .enumerate() + .map( + |( + sample_index, + ( + name_tag_relative_offset, + primary_name, + secondary_name, + first_triplet_dwords, + second_triplet_dwords, + trailing_word, + ), + )| { + SmpSavePlacedStructureDynamicSideBufferFixedPolicySample { + sample_index, + name_tag_relative_offset: *name_tag_relative_offset, + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + first_triplet_dwords_hex: first_triplet_dwords + .iter() + .map(|value| format!("0x{value:08x}")) + .collect(), + second_triplet_dwords_hex: second_triplet_dwords + .iter() + .map(|value| format!("0x{value:08x}")) + .collect(), + trailing_word: *trailing_word, + trailing_word_hex: format!("0x{trailing_word:04x}"), + } + }, + ) + .collect(), + } + }, + ) + .take(8) + .collect::>(); + Some(SmpSavePlacedStructureDynamicSideBufferFixedPolicySummary { + row_count_with_0x1a_policy_chunk: fixed_policy_rows.len(), + unique_trailing_word_count: fixed_policy_trailing_word_counts.len(), + dominant_trailing_word: dominant_fixed_policy_trailing_word.map(|(word, _)| word), + dominant_trailing_word_hex: dominant_fixed_policy_trailing_word + .map(|(word, _)| format!("0x{word:04x}")), + dominant_trailing_word_count: dominant_fixed_policy_trailing_word + .map(|(_, count)| count) + .unwrap_or_default(), + compact_prefix_correlations: fixed_policy_compact_prefix_correlations, + sample_rows: fixed_policy_rows + .iter() + .take(8) + .enumerate() + .map( + |( + sample_index, + ( + name_tag_relative_offset, + primary_name, + secondary_name, + _, + _, + _, + first_triplet_dwords, + second_triplet_dwords, + trailing_word, + ), + )| { + SmpSavePlacedStructureDynamicSideBufferFixedPolicySample { + sample_index, + name_tag_relative_offset: *name_tag_relative_offset, + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + first_triplet_dwords_hex: first_triplet_dwords + .iter() + .map(|value| format!("0x{value:08x}")) + .collect(), + second_triplet_dwords_hex: second_triplet_dwords + .iter() + .map(|value| format!("0x{value:08x}")) + .collect(), + trailing_word: *trailing_word, + trailing_word_hex: format!("0x{trailing_word:04x}"), + } + }, + ) + .collect(), + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/mod.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/mod.rs new file mode 100644 index 0000000..6e81666 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/mod.rs @@ -0,0 +1,9 @@ +mod fixed_policy; +mod rows; +mod short_profile; +mod span_stats; + +pub(super) use fixed_policy::build_fixed_policy_summary; +pub(super) use rows::{PayloadEnvelopeRow, build_payload_envelope_rows}; +pub(super) use short_profile::build_short_profile_flag_pair_summary; +pub(super) use span_stats::{PayloadEnvelopeSpanStats, summarize_payload_envelope_spans}; diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/rows.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/rows.rs new file mode 100644 index 0000000..5d586ca --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/rows.rs @@ -0,0 +1,97 @@ +use super::super::embedded_names::EmbeddedNameRow; + +#[derive(Clone)] +pub(crate) struct PayloadEnvelopeRow { + pub(in crate::inspect::smp) name_tag_relative_offset: usize, + pub(in crate::inspect::smp) primary_name: Option, + pub(in crate::inspect::smp) secondary_name: Option, + pub(in crate::inspect::smp) name_payload_end_relative_offset: Option, + pub(in crate::inspect::smp) policy_tag_relative_offset: Option, + pub(in crate::inspect::smp) profile_tag_relative_offset: Option, + pub(in crate::inspect::smp) next_name_tag_relative_offset: Option, + pub(in crate::inspect::smp) name_to_policy_gap_len: Option, + pub(in crate::inspect::smp) policy_chunk_len: Option, + pub(in crate::inspect::smp) profile_chunk_len_to_next_name_or_end: Option, + pub(in crate::inspect::smp) short_profile_first_flag_byte: Option, + pub(in crate::inspect::smp) short_profile_second_flag_byte: Option, +} + +pub(crate) fn build_payload_envelope_rows( + records_payload: &[u8], + embedded_name_tag_offsets: &[usize], + embedded_name_rows: &[EmbeddedNameRow], + policy_tag_offsets: &[usize], + profile_tag_offsets: &[usize], +) -> Vec { + embedded_name_rows + .iter() + .enumerate() + .map(|(row_index, row)| { + let next_name_tag_relative_offset = embedded_name_tag_offsets + .get(row_index + 1) + .copied() + .or(Some(records_payload.len())); + let name_payload_end_relative_offset = Some( + row.name_tag_relative_offset + + row.name_payload_relative_offset + + row.name_payload_len, + ); + let policy_tag_relative_offset = policy_tag_offsets.iter().copied().find(|offset| { + *offset > row.name_tag_relative_offset + && next_name_tag_relative_offset.is_none_or(|next_name| *offset < next_name) + }); + let profile_tag_relative_offset = policy_tag_relative_offset.and_then(|policy| { + profile_tag_offsets.iter().copied().find(|offset| { + *offset > policy + && next_name_tag_relative_offset.is_none_or(|next_name| *offset < next_name) + }) + }); + let name_to_policy_gap_len = name_payload_end_relative_offset + .zip(policy_tag_relative_offset) + .and_then( + |(name_payload_end_relative_offset, policy_tag_relative_offset)| { + policy_tag_relative_offset.checked_sub(name_payload_end_relative_offset) + }, + ); + let policy_chunk_len = policy_tag_relative_offset + .zip(profile_tag_relative_offset) + .and_then( + |(policy_tag_relative_offset, profile_tag_relative_offset)| { + profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4) + }, + ); + let profile_chunk_len_to_next_name_or_end = + profile_tag_relative_offset.and_then(|profile_tag_relative_offset| { + next_name_tag_relative_offset.and_then(|next_name_tag_relative_offset| { + next_name_tag_relative_offset.checked_sub(profile_tag_relative_offset + 4) + }) + }); + let short_profile_first_flag_byte = + profile_tag_relative_offset.and_then(|profile_tag_relative_offset| { + records_payload + .get(profile_tag_relative_offset + 4) + .copied() + }); + let short_profile_second_flag_byte = + profile_tag_relative_offset.and_then(|profile_tag_relative_offset| { + records_payload + .get(profile_tag_relative_offset + 5) + .copied() + }); + PayloadEnvelopeRow { + name_tag_relative_offset: row.name_tag_relative_offset, + primary_name: row.primary_name.clone(), + secondary_name: row.secondary_name.clone(), + name_payload_end_relative_offset, + policy_tag_relative_offset, + profile_tag_relative_offset, + next_name_tag_relative_offset, + name_to_policy_gap_len, + policy_chunk_len, + profile_chunk_len_to_next_name_or_end, + short_profile_first_flag_byte, + short_profile_second_flag_byte, + } + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/short_profile.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/short_profile.rs new file mode 100644 index 0000000..4d8182f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/short_profile.rs @@ -0,0 +1,114 @@ +use super::super::super::*; +use super::rows::PayloadEnvelopeRow; +use std::collections::BTreeMap; + +pub(crate) fn build_short_profile_flag_pair_summary( + payload_envelope_rows: &[PayloadEnvelopeRow], +) -> Option { + let short_profile_flag_rows = payload_envelope_rows + .iter() + .filter(|row| row.profile_chunk_len_to_next_name_or_end == Some(6)) + .filter_map(|row| { + Some(( + row.name_tag_relative_offset, + row.primary_name.clone(), + row.secondary_name.clone(), + row.short_profile_first_flag_byte?, + row.short_profile_second_flag_byte?, + )) + }) + .collect::>(); + let mut short_profile_flag_pair_counts = BTreeMap::<(u8, u8), usize>::new(); + let mut short_profile_first_flag_counts = BTreeMap::::new(); + let mut short_profile_second_flag_counts = BTreeMap::::new(); + for (_, _, _, first_flag_byte, second_flag_byte) in &short_profile_flag_rows { + *short_profile_flag_pair_counts + .entry((*first_flag_byte, *second_flag_byte)) + .or_default() += 1; + *short_profile_first_flag_counts + .entry(*first_flag_byte) + .or_default() += 1; + *short_profile_second_flag_counts + .entry(*second_flag_byte) + .or_default() += 1; + } + let dominant_short_profile_flag_pair = short_profile_flag_pair_counts + .iter() + .max_by(|(left_pair, left_count), (right_pair, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_pair.cmp(left_pair)) + }) + .map(|((first_flag_byte, second_flag_byte), count)| { + SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPair { + first_flag_byte: *first_flag_byte, + first_flag_byte_hex: format!("0x{first_flag_byte:02x}"), + second_flag_byte: *second_flag_byte, + second_flag_byte_hex: format!("0x{second_flag_byte:02x}"), + count: *count, + } + }); + let dominant_short_profile_first_flag = short_profile_first_flag_counts + .iter() + .max_by(|(left_byte, left_count), (right_byte, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_byte.cmp(left_byte)) + }) + .map(|(byte, count)| (*byte, *count)); + let dominant_short_profile_second_flag = short_profile_second_flag_counts + .iter() + .max_by(|(left_byte, left_count), (right_byte, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_byte.cmp(left_byte)) + }) + .map(|(byte, count)| (*byte, *count)); + Some( + SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSummary { + row_count_with_0x06_profile_span: short_profile_flag_rows.len(), + unique_flag_pair_count: short_profile_flag_pair_counts.len(), + dominant_first_flag_byte: dominant_short_profile_first_flag.map(|(byte, _)| byte), + dominant_first_flag_byte_hex: dominant_short_profile_first_flag + .map(|(byte, _)| format!("0x{byte:02x}")), + dominant_first_flag_byte_count: dominant_short_profile_first_flag + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_second_flag_byte: dominant_short_profile_second_flag.map(|(byte, _)| byte), + dominant_second_flag_byte_hex: dominant_short_profile_second_flag + .map(|(byte, _)| format!("0x{byte:02x}")), + dominant_second_flag_byte_count: dominant_short_profile_second_flag + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_flag_pair: dominant_short_profile_flag_pair, + sample_rows: short_profile_flag_rows + .iter() + .take(8) + .enumerate() + .map( + |( + sample_index, + ( + name_tag_relative_offset, + primary_name, + secondary_name, + first_flag_byte, + second_flag_byte, + ), + )| { + SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSample { + sample_index, + name_tag_relative_offset: *name_tag_relative_offset, + primary_name: primary_name.clone(), + secondary_name: secondary_name.clone(), + first_flag_byte: *first_flag_byte, + first_flag_byte_hex: format!("0x{first_flag_byte:02x}"), + second_flag_byte: *second_flag_byte, + second_flag_byte_hex: format!("0x{second_flag_byte:02x}"), + } + }, + ) + .collect(), + }, + ) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/span_stats.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/span_stats.rs new file mode 100644 index 0000000..d055aff --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/payload_envelope/span_stats.rs @@ -0,0 +1,80 @@ +use super::rows::PayloadEnvelopeRow; +use std::collections::BTreeMap; + +pub(crate) struct PayloadEnvelopeSpanStats { + pub(in crate::inspect::smp) row_count_with_policy_tag_before_next_name: usize, + pub(in crate::inspect::smp) row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope: usize, + pub(in crate::inspect::smp) row_count_missing_policy_tag_before_next_name: usize, + pub(in crate::inspect::smp) row_count_missing_profile_tag_after_policy: usize, + pub(in crate::inspect::smp) unique_policy_chunk_lens: Vec, + pub(in crate::inspect::smp) unique_profile_chunk_lens: Vec, + pub(in crate::inspect::smp) dominant_policy_chunk_len: Option<(usize, usize)>, + pub(in crate::inspect::smp) dominant_profile_chunk_len: Option<(usize, usize)>, +} + +pub(crate) fn summarize_payload_envelope_spans( + payload_envelope_rows: &[PayloadEnvelopeRow], +) -> PayloadEnvelopeSpanStats { + let row_count_with_policy_tag_before_next_name = payload_envelope_rows + .iter() + .filter(|row| row.policy_tag_relative_offset.is_some()) + .count(); + let row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope = payload_envelope_rows + .iter() + .filter(|row| { + row.policy_tag_relative_offset.is_some() && row.profile_tag_relative_offset.is_some() + }) + .count(); + let row_count_missing_policy_tag_before_next_name = payload_envelope_rows + .iter() + .filter(|row| row.policy_tag_relative_offset.is_none()) + .count(); + let row_count_missing_profile_tag_after_policy = payload_envelope_rows + .iter() + .filter(|row| { + row.policy_tag_relative_offset.is_some() && row.profile_tag_relative_offset.is_none() + }) + .count(); + let mut policy_chunk_len_counts = BTreeMap::::new(); + let mut profile_chunk_len_counts = BTreeMap::::new(); + for row in payload_envelope_rows { + if let Some(policy_chunk_len) = row.policy_chunk_len { + *policy_chunk_len_counts.entry(policy_chunk_len).or_default() += 1; + } + if let Some(profile_chunk_len_to_next_name_or_end) = + row.profile_chunk_len_to_next_name_or_end + { + *profile_chunk_len_counts + .entry(profile_chunk_len_to_next_name_or_end) + .or_default() += 1; + } + } + let unique_policy_chunk_lens = policy_chunk_len_counts.keys().copied().collect::>(); + let unique_profile_chunk_lens = profile_chunk_len_counts.keys().copied().collect::>(); + let dominant_policy_chunk_len = policy_chunk_len_counts + .iter() + .max_by(|(left_len, left_count), (right_len, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_len.cmp(left_len)) + }) + .map(|(len, count)| (*len, *count)); + let dominant_profile_chunk_len = profile_chunk_len_counts + .iter() + .max_by(|(left_len, left_count), (right_len, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_len.cmp(left_len)) + }) + .map(|(len, count)| (*len, *count)); + PayloadEnvelopeSpanStats { + row_count_with_policy_tag_before_next_name, + row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, + row_count_missing_policy_tag_before_next_name, + row_count_missing_profile_tag_after_policy, + unique_policy_chunk_lens, + unique_profile_chunk_lens, + dominant_policy_chunk_len, + dominant_profile_chunk_len, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/prefix_patterns.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/prefix_patterns.rs new file mode 100644 index 0000000..b01c260 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/prefix_patterns.rs @@ -0,0 +1,176 @@ +use std::collections::BTreeMap; + +use super::embedded_names::EmbeddedNameRow; +use crate::inspect::smp::structures::*; + +#[derive(Default)] +struct PrefixPatternAccumulator { + count: usize, + first_name_tag_relative_offset: usize, + first_primary_name: Option, + first_secondary_name: Option, + section_like_primary_name_count: usize, + cap_like_primary_name_count: usize, + other_primary_name_count: usize, +} + +#[derive(Default)] +struct NamePairAccumulator { + count: usize, + first_name_tag_relative_offset: usize, + prefix_counts: BTreeMap<(u32, u16, u8), usize>, +} + +pub(super) struct PrefixAndNamePairSummary { + pub(super) compact_prefix_pattern_summaries: + Vec, + pub(super) name_pair_summaries: Vec, + pub(super) prefix_leading_dword_matching_embedded_profile_tag_count: usize, +} + +pub(super) fn summarize_prefix_and_name_pairs( + embedded_name_rows: &[EmbeddedNameRow], +) -> PrefixAndNamePairSummary { + let mut compact_prefix_pattern_map = + BTreeMap::<(u32, u16, u8), PrefixPatternAccumulator>::new(); + let mut name_pair_map = BTreeMap::<(String, String), NamePairAccumulator>::new(); + for row in embedded_name_rows { + let entry = compact_prefix_pattern_map + .entry(( + row.prefix_leading_dword, + row.prefix_trailing_word, + row.prefix_separator_byte, + )) + .or_insert_with(|| PrefixPatternAccumulator { + first_name_tag_relative_offset: row.name_tag_relative_offset, + first_primary_name: row.primary_name.clone(), + first_secondary_name: row.secondary_name.clone(), + ..Default::default() + }); + entry.count += 1; + match row.primary_name.as_deref() { + Some(name) if name.ends_with("_Section.3dp") => { + entry.section_like_primary_name_count += 1; + } + Some(name) if name.ends_with("_Cap.3dp") => { + entry.cap_like_primary_name_count += 1; + } + _ => { + entry.other_primary_name_count += 1; + } + } + if let (Some(primary_name), Some(secondary_name)) = + (row.primary_name.as_ref(), row.secondary_name.as_ref()) + { + let entry = name_pair_map + .entry((primary_name.clone(), secondary_name.clone())) + .or_insert_with(|| NamePairAccumulator { + first_name_tag_relative_offset: row.name_tag_relative_offset, + ..Default::default() + }); + entry.count += 1; + *entry + .prefix_counts + .entry(( + row.prefix_leading_dword, + row.prefix_trailing_word, + row.prefix_separator_byte, + )) + .or_default() += 1; + } + } + + let prefix_leading_dword_matching_embedded_profile_tag_count = embedded_name_rows + .iter() + .filter(|row| row.prefix_leading_dword == u32::from(SAVE_REGION_RECORD_PROFILE_TAG)) + .count(); + + let mut compact_prefix_pattern_summaries = compact_prefix_pattern_map + .into_iter() + .map( + |((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), accumulator)| { + SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary { + prefix_leading_dword, + prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), + prefix_trailing_word, + prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), + prefix_separator_byte, + prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), + count: accumulator.count, + first_name_tag_relative_offset: accumulator.first_name_tag_relative_offset, + prefix_leading_dword_matches_embedded_profile_tag: prefix_leading_dword + == u32::from(SAVE_REGION_RECORD_PROFILE_TAG), + section_like_primary_name_count: accumulator.section_like_primary_name_count, + cap_like_primary_name_count: accumulator.cap_like_primary_name_count, + other_primary_name_count: accumulator.other_primary_name_count, + first_primary_name: accumulator.first_primary_name, + first_secondary_name: accumulator.first_secondary_name, + } + }, + ) + .collect::>(); + compact_prefix_pattern_summaries.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| { + left.first_name_tag_relative_offset + .cmp(&right.first_name_tag_relative_offset) + }) + .then_with(|| left.prefix_leading_dword.cmp(&right.prefix_leading_dword)) + .then_with(|| left.prefix_trailing_word.cmp(&right.prefix_trailing_word)) + .then_with(|| left.prefix_separator_byte.cmp(&right.prefix_separator_byte)) + }); + + let mut name_pair_summaries = name_pair_map + .into_iter() + .filter_map(|((primary_name, secondary_name), accumulator)| { + let dominant_prefix = accumulator.prefix_counts.iter().max_by( + |(left_key, left_count), (right_key, right_count)| { + left_count + .cmp(right_count) + .then_with(|| right_key.cmp(left_key)) + }, + )?; + let ( + dominant_prefix_leading_dword, + dominant_prefix_trailing_word, + dominant_prefix_separator_byte, + ) = *dominant_prefix.0; + let dominant_prefix_count = *dominant_prefix.1; + Some(SmpSavePlacedStructureDynamicSideBufferNamePairSummary { + primary_name, + secondary_name, + count: accumulator.count, + first_name_tag_relative_offset: accumulator.first_name_tag_relative_offset, + unique_compact_prefix_pattern_count: accumulator.prefix_counts.len(), + dominant_prefix_leading_dword, + dominant_prefix_leading_dword_hex: format!("0x{dominant_prefix_leading_dword:08x}"), + dominant_prefix_trailing_word, + dominant_prefix_trailing_word_hex: format!("0x{dominant_prefix_trailing_word:04x}"), + dominant_prefix_separator_byte, + dominant_prefix_separator_byte_hex: format!( + "0x{dominant_prefix_separator_byte:02x}" + ), + dominant_prefix_count, + }) + }) + .collect::>(); + name_pair_summaries.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| { + left.first_name_tag_relative_offset + .cmp(&right.first_name_tag_relative_offset) + }) + .then_with(|| left.primary_name.cmp(&right.primary_name)) + .then_with(|| left.secondary_name.cmp(&right.secondary_name)) + }); + + PrefixAndNamePairSummary { + compact_prefix_pattern_summaries, + name_pair_summaries, + prefix_leading_dword_matching_embedded_profile_tag_count, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/scan.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/scan.rs new file mode 100644 index 0000000..5759f66 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/scan.rs @@ -0,0 +1,330 @@ +use super::embedded_names::*; +use super::evidence::*; +use super::live_entry_prelude::*; +use super::name_prelude::*; +use super::payload_envelope::*; +use super::prefix_patterns::*; +use crate::inspect::smp::structures::*; + +pub(in crate::inspect::smp) fn parse_save_placed_structure_dynamic_side_buffer_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + if file_extension_hint != Some("gms") { + return None; + } + let profile = container_profile?; + if !matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ) { + return None; + } + + let metadata_offsets = find_u32_le_offsets(bytes, 0x000038a5); + let records_offsets = find_u32_le_offsets(bytes, 0x000038a6); + let close_offsets = find_u32_le_offsets(bytes, 0x000038a7); + for metadata_tag_offset in metadata_offsets { + let Some(records_tag_offset) = records_offsets + .iter() + .copied() + .find(|offset| *offset > metadata_tag_offset) + else { + continue; + }; + let Some(close_tag_offset) = close_offsets + .iter() + .copied() + .find(|offset| *offset > records_tag_offset) + else { + continue; + }; + let Some(payload) = bytes.get(metadata_tag_offset + 4..records_tag_offset) else { + continue; + }; + if payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { + continue; + } + let Some(header_words) = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT) + .map(|index| read_u32_at(payload, index * 4)) + .collect::>>() + else { + continue; + }; + let Some(header_words): Option<[u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT]> = + header_words.try_into().ok() + else { + continue; + }; + let summary = IndexedCollectionHeaderSummary { + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + direct_collection_flag: header_words[0], + direct_record_stride: header_words[1], + live_id_bound: header_words[4], + live_record_count: header_words[5], + header_words, + }; + if !(summary.direct_collection_flag == 0 + && summary.direct_record_stride == 0x06 + && summary.header_words.get(2) == Some(&1000) + && summary.header_words.get(3) == Some(&500) + && summary.header_words.get(6) == Some(&0) + && summary.header_words.get(7) == Some(&1) + && summary.live_id_bound >= 0x100 + && summary.live_id_bound <= 0x1000 + && summary.live_record_count >= 0x100 + && summary.live_record_count <= summary.live_id_bound) + { + continue; + } + let Some(records_payload) = bytes.get(records_tag_offset + 4..close_tag_offset) else { + continue; + }; + let embedded_name_tag_offsets = + find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG); + let Some(&first_embedded_name_tag_relative_offset) = embedded_name_tag_offsets.first() + else { + continue; + }; + let Some(prefix_payload) = records_payload.get(..first_embedded_name_tag_relative_offset) + else { + continue; + }; + if prefix_payload.len() < 7 { + continue; + } + let Some(FirstRecordNamePrelude { + owner_shared_dword, + owner_shared_dword_relative_offset, + first_record_child_count_after_owner_shared, + first_record_saved_primary_child_byte_after_owner_shared, + first_record_first_name_tag_relative_offset_after_owner_shared, + prefix_leading_dword, + prefix_trailing_word, + prefix_separator_byte, + first_embedded_primary_name, + first_embedded_secondary_name, + first_embedded_tertiary_name, + }) = parse_first_record_name_prelude( + records_payload, + first_embedded_name_tag_relative_offset, + ) + else { + continue; + }; + let embedded_name_rows = + parse_embedded_name_rows(records_payload, &embedded_name_tag_offsets); + let policy_tag_offsets = + find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG); + let profile_tag_offsets = + find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG); + let payload_envelope_rows = build_payload_envelope_rows( + records_payload, + &embedded_name_tag_offsets, + &embedded_name_rows, + &policy_tag_offsets, + &profile_tag_offsets, + ); + let embedded_name_row_samples = embedded_name_rows + .iter() + .take(8) + .enumerate() + .map( + |(sample_index, row)| SmpSavePlacedStructureDynamicSideBufferSampleEntry { + sample_index, + name_tag_relative_offset: row.name_tag_relative_offset, + prefix_leading_dword: row.prefix_leading_dword, + prefix_leading_dword_hex: format!("0x{:08x}", row.prefix_leading_dword), + prefix_trailing_word: row.prefix_trailing_word, + prefix_trailing_word_hex: format!("0x{:04x}", row.prefix_trailing_word), + prefix_separator_byte: row.prefix_separator_byte, + prefix_separator_byte_hex: format!("0x{:02x}", row.prefix_separator_byte), + primary_name: row.primary_name.clone(), + secondary_name: row.secondary_name.clone(), + tertiary_name: row.tertiary_name.clone(), + }, + ) + .collect::>(); + let PrefixAndNamePairSummary { + compact_prefix_pattern_summaries, + name_pair_summaries, + prefix_leading_dword_matching_embedded_profile_tag_count, + } = summarize_prefix_and_name_pairs(&embedded_name_rows); + let PayloadEnvelopeSpanStats { + row_count_with_policy_tag_before_next_name, + row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, + row_count_missing_policy_tag_before_next_name, + row_count_missing_profile_tag_after_policy, + unique_policy_chunk_lens, + unique_profile_chunk_lens, + dominant_policy_chunk_len, + dominant_profile_chunk_len, + } = summarize_payload_envelope_spans(&payload_envelope_rows); + let short_profile_flag_pair_summary = + build_short_profile_flag_pair_summary(&payload_envelope_rows); + let fixed_policy_summary = build_fixed_policy_summary( + records_payload, + &payload_envelope_rows, + &embedded_name_rows, + ); + let name_prelude_candidate_summary = build_name_prelude_candidate_summary( + records_payload, + &payload_envelope_rows, + &embedded_name_rows, + ); + let dominant_profile_span_class_summary = build_dominant_profile_span_class_summary( + records_payload, + &payload_envelope_rows, + &embedded_name_rows, + dominant_profile_chunk_len, + ); + let payload_envelope_summary = Some( + SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSummary { + row_count_with_policy_tag_before_next_name, + row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, + row_count_missing_policy_tag_before_next_name, + row_count_missing_profile_tag_after_policy, + unique_policy_chunk_lens, + unique_profile_chunk_lens, + dominant_policy_chunk_len: dominant_policy_chunk_len.map(|(len, _)| len), + dominant_policy_chunk_len_count: dominant_policy_chunk_len + .map(|(_, count)| count) + .unwrap_or_default(), + dominant_profile_chunk_len: dominant_profile_chunk_len.map(|(len, _)| len), + dominant_profile_chunk_len_count: dominant_profile_chunk_len + .map(|(_, count)| count) + .unwrap_or_default(), + short_profile_flag_pair_summary: short_profile_flag_pair_summary.clone(), + fixed_policy_summary: fixed_policy_summary.clone(), + name_prelude_candidate_summary: name_prelude_candidate_summary.clone(), + dominant_profile_span_class_summary: dominant_profile_span_class_summary.clone(), + sample_rows: payload_envelope_rows + .iter() + .take(8) + .enumerate() + .map(|(sample_index, row)| { + SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSample { + sample_index, + name_tag_relative_offset: row.name_tag_relative_offset, + primary_name: row.primary_name.clone(), + secondary_name: row.secondary_name.clone(), + name_payload_end_relative_offset: row.name_payload_end_relative_offset, + policy_tag_relative_offset: row.policy_tag_relative_offset, + profile_tag_relative_offset: row.profile_tag_relative_offset, + next_name_tag_relative_offset: row.next_name_tag_relative_offset, + name_to_policy_gap_len: row.name_to_policy_gap_len, + policy_chunk_len: row.policy_chunk_len, + profile_chunk_len_to_next_name_or_end: row + .profile_chunk_len_to_next_name_or_end, + } + }) + .collect(), + }, + ); + let live_entry_prelude_summary = build_live_entry_prelude_summary( + payload, + records_payload, + summary.live_id_bound, + summary.live_record_count, + ); + let unique_embedded_name_pair_count = name_pair_summaries.len(); + let dominant_compact_prefix_pattern = compact_prefix_pattern_summaries.first().cloned(); + let decoded_embedded_name_row_count = embedded_name_rows + .iter() + .filter(|row| row.primary_name.is_some() && row.secondary_name.is_some()) + .count(); + let decoded_embedded_name_row_with_tertiary_name_count = embedded_name_rows + .iter() + .filter(|row| { + row.primary_name.is_some() + && row.secondary_name.is_some() + && row.tertiary_name.is_some() + }) + .count(); + let first_record_saved_primary_child_byte_after_owner_shared_hex = + first_record_saved_primary_child_byte_after_owner_shared + .map(|value| format!("0x{value:02x}")); + return Some(SmpSavePlacedStructureDynamicSideBufferProbe { + profile_family: profile.profile_family.clone(), + source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(), + semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records" + .to_string(), + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + records_span_len: close_tag_offset.saturating_sub(records_tag_offset + 4), + direct_record_stride: summary.direct_record_stride, + direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride), + live_id_bound: summary.live_id_bound, + live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound), + live_record_count: summary.live_record_count, + live_record_count_hex: format!("0x{:08x}", summary.live_record_count), + owner_shared_dword, + owner_shared_dword_hex: format!("0x{owner_shared_dword:08x}"), + owner_shared_dword_relative_offset, + owner_shared_dword_matches_first_compact_prefix_leading_dword: owner_shared_dword + == prefix_leading_dword, + first_record_child_count_after_owner_shared, + first_record_child_count_after_owner_shared_hex: + first_record_child_count_after_owner_shared.map(|value| format!("0x{value:04x}")), + first_record_saved_primary_child_byte_after_owner_shared, + first_record_saved_primary_child_byte_after_owner_shared_hex: + first_record_saved_primary_child_byte_after_owner_shared + .map(|value| format!("0x{value:02x}")), + first_record_first_name_tag_relative_offset_after_owner_shared, + prefix_leading_dword, + prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), + prefix_trailing_word, + prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), + prefix_separator_byte, + prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), + first_embedded_name_tag_relative_offset, + embedded_name_tag_count: embedded_name_tag_offsets.len(), + decoded_embedded_name_row_count, + decoded_embedded_name_row_with_tertiary_name_count, + unique_compact_prefix_pattern_count: compact_prefix_pattern_summaries.len(), + prefix_leading_dword_matching_embedded_profile_tag_count, + unique_embedded_name_pair_count, + first_embedded_primary_name: Some(first_embedded_primary_name.clone()), + first_embedded_secondary_name: Some(first_embedded_secondary_name.clone()), + first_embedded_tertiary_name: first_embedded_tertiary_name.clone(), + embedded_name_row_samples, + compact_prefix_pattern_summaries, + name_pair_summaries, + payload_envelope_summary, + live_entry_prelude_summary: live_entry_prelude_summary.clone(), + evidence: build_dynamic_side_buffer_evidence(DynamicSideBufferEvidenceInputs { + owner_shared_dword, + first_record_child_count_after_owner_shared, + first_record_saved_primary_child_byte_after_owner_shared_hex: + first_record_saved_primary_child_byte_after_owner_shared_hex.as_deref(), + first_record_first_name_tag_relative_offset_after_owner_shared, + first_embedded_primary_name: &first_embedded_primary_name, + first_embedded_secondary_name: &first_embedded_secondary_name, + first_embedded_tertiary_name: first_embedded_tertiary_name.as_ref(), + embedded_name_tag_count: embedded_name_tag_offsets.len(), + embedded_name_row_count: embedded_name_rows.len(), + prefix_leading_dword_matching_embedded_profile_tag_count, + decoded_embedded_name_row_count, + unique_embedded_name_pair_count, + decoded_embedded_name_row_with_tertiary_name_count, + row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, + row_count_missing_policy_tag_before_next_name, + row_count_missing_profile_tag_after_policy, + dominant_policy_chunk_len, + dominant_profile_chunk_len, + short_profile_flag_pair_summary: short_profile_flag_pair_summary.as_ref(), + fixed_policy_summary: fixed_policy_summary.as_ref(), + live_entry_prelude_summary: live_entry_prelude_summary.as_ref(), + dominant_compact_prefix_pattern: dominant_compact_prefix_pattern.as_ref(), + }), + }); + } + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/summary.rs b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/summary.rs new file mode 100644 index 0000000..59e881b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/dynamic_side_buffer/summary.rs @@ -0,0 +1,30 @@ +use crate::inspect::smp::structures::*; + +pub(in crate::inspect::smp) fn derive_loaded_placed_structure_dynamic_side_buffer_summary( + probe: &SmpSavePlacedStructureDynamicSideBufferProbe, + alignment: Option<&SmpSavePlacedStructureDynamicSideBufferAlignmentProbe>, +) -> SmpLoadedPlacedStructureDynamicSideBufferSummary { + SmpLoadedPlacedStructureDynamicSideBufferSummary { + source_kind: probe.source_kind.clone(), + semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-summary".to_string(), + observed_entry_count: probe.live_record_count, + owner_shared_dword_hex: probe.owner_shared_dword_hex.clone(), + unique_embedded_name_pair_count: probe.unique_embedded_name_pair_count, + decoded_embedded_name_row_count: probe.decoded_embedded_name_row_count, + first_prefix_leading_dword_hex: probe.prefix_leading_dword_hex.clone(), + first_prefix_trailing_word_hex: probe.prefix_trailing_word_hex.clone(), + first_prefix_separator_byte_hex: probe.prefix_separator_byte_hex.clone(), + triplet_alignment_overlap_count: alignment + .map(|alignment| alignment.overlapping_name_pair_count) + .unwrap_or_default(), + triplet_alignment_side_buffer_only_name_pair_count: alignment + .map(|alignment| { + alignment + .unique_side_buffer_name_pair_count + .saturating_sub(alignment.overlapping_name_pair_count) + }) + .unwrap_or_default(), + compact_prefix_pattern_summaries: probe.compact_prefix_pattern_summaries.clone(), + name_pair_summaries: probe.name_pair_summaries.clone(), + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/entrypoints.rs b/crates/rrt-runtime/src/inspect/smp/structures/entrypoints.rs new file mode 100644 index 0000000..f6bdb22 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/entrypoints.rs @@ -0,0 +1,37 @@ +use crate::inspect::smp::bundle::{ + classify_container_profile, classify_header_variant_probe, classify_secondary_variant_probe, + parse_shared_header, probe_early_content_layout, +}; +use crate::inspect::smp::common::find_first_ascii_run; +use crate::inspect::smp::structures::*; +use std::fs; +use std::path::Path; + +pub fn inspect_save_placed_structure_dynamic_side_buffer_file( + path: &Path, +) -> Result, Box> { + let bytes = fs::read(path)?; + let file_extension_hint = path + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.to_ascii_lowercase()); + let shared_header = parse_shared_header(&bytes); + let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); + let first_ascii_run = find_first_ascii_run(&bytes); + let early_content_probe = first_ascii_run + .as_ref() + .and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run)); + let secondary_variant_probe = early_content_probe + .as_ref() + .and_then(classify_secondary_variant_probe); + let container_profile = classify_container_profile( + file_extension_hint.as_deref(), + header_variant_probe.as_ref(), + secondary_variant_probe.as_ref(), + ); + Ok(parse_save_placed_structure_dynamic_side_buffer_probe( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + )) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/mod.rs b/crates/rrt-runtime/src/inspect/smp/structures/mod.rs new file mode 100644 index 0000000..b52299c --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/mod.rs @@ -0,0 +1,19 @@ +use super::bundle::*; +use super::events::decode_live_entry_ids_from_tombstone_bitset; +use super::regions::*; +use crate::inspect::smp::*; + +mod alignment; +mod dynamic_side_buffer; +mod entrypoints; +mod model; +mod queued_notice; +mod triplets; + +pub(in crate::inspect::smp) use alignment::*; +pub(in crate::inspect::smp) use dynamic_side_buffer::*; +pub(in crate::inspect::smp) use triplets::*; + +pub use entrypoints::*; +pub use model::*; +pub use queued_notice::*; diff --git a/crates/rrt-runtime/src/inspect/smp/structures/model/alignment.rs b/crates/rrt-runtime/src/inspect/smp/structures/model/alignment.rs new file mode 100644 index 0000000..3fa4975 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/model/alignment.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::structures::SmpSavePlacedStructureDynamicSideBufferNamePairSummary; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferAlignmentProbe { + pub unique_side_buffer_name_pair_count: usize, + pub unique_triplet_name_pair_count: usize, + pub overlapping_name_pair_count: usize, + pub side_buffer_row_count: usize, + pub side_buffer_rows_with_matching_triplet_name_pair_count: usize, + pub side_buffer_rows_without_matching_triplet_name_pair_count: usize, + pub triplet_name_pairs_without_side_buffer_match_count: usize, + #[serde(default)] + pub matched_name_pair_samples: Vec, + #[serde(default)] + pub unmatched_side_buffer_name_pair_samples: + Vec, + #[serde(default)] + pub evidence: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/core.rs b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/core.rs new file mode 100644 index 0000000..8b194c5 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/core.rs @@ -0,0 +1,93 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::structures::{ + SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSummary, + SmpSavePlacedStructureDynamicSideBufferNamePairSummary, + SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSummary, + SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary, + SmpSavePlacedStructureDynamicSideBufferSampleEntry, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub metadata_tag_offset: usize, + pub records_tag_offset: usize, + pub close_tag_offset: usize, + pub records_span_len: usize, + pub direct_record_stride: u32, + pub direct_record_stride_hex: String, + pub live_id_bound: u32, + pub live_id_bound_hex: String, + pub live_record_count: u32, + pub live_record_count_hex: String, + pub owner_shared_dword: u32, + pub owner_shared_dword_hex: String, + pub owner_shared_dword_relative_offset: usize, + pub owner_shared_dword_matches_first_compact_prefix_leading_dword: bool, + #[serde(default)] + pub first_record_child_count_after_owner_shared: Option, + #[serde(default)] + pub first_record_child_count_after_owner_shared_hex: Option, + #[serde(default)] + pub first_record_saved_primary_child_byte_after_owner_shared: Option, + #[serde(default)] + pub first_record_saved_primary_child_byte_after_owner_shared_hex: Option, + #[serde(default)] + pub first_record_first_name_tag_relative_offset_after_owner_shared: Option, + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + pub first_embedded_name_tag_relative_offset: usize, + pub embedded_name_tag_count: usize, + pub decoded_embedded_name_row_count: usize, + pub decoded_embedded_name_row_with_tertiary_name_count: usize, + pub unique_compact_prefix_pattern_count: usize, + pub prefix_leading_dword_matching_embedded_profile_tag_count: usize, + pub unique_embedded_name_pair_count: usize, + #[serde(default)] + pub first_embedded_primary_name: Option, + #[serde(default)] + pub first_embedded_secondary_name: Option, + #[serde(default)] + pub first_embedded_tertiary_name: Option, + #[serde(default)] + pub embedded_name_row_samples: Vec, + #[serde(default)] + pub compact_prefix_pattern_summaries: + Vec, + #[serde(default)] + pub name_pair_summaries: Vec, + #[serde(default)] + pub payload_envelope_summary: + Option, + #[serde(default)] + pub live_entry_prelude_summary: + Option, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPlacedStructureDynamicSideBufferSummary { + pub source_kind: String, + pub semantic_family: String, + pub observed_entry_count: u32, + pub owner_shared_dword_hex: String, + pub unique_embedded_name_pair_count: usize, + pub decoded_embedded_name_row_count: usize, + pub first_prefix_leading_dword_hex: String, + pub first_prefix_trailing_word_hex: String, + pub first_prefix_separator_byte_hex: String, + pub triplet_alignment_overlap_count: usize, + pub triplet_alignment_side_buffer_only_name_pair_count: usize, + #[serde(default)] + pub compact_prefix_pattern_summaries: + Vec, + #[serde(default)] + pub name_pair_summaries: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/mod.rs b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/mod.rs new file mode 100644 index 0000000..58bb3f9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/mod.rs @@ -0,0 +1,9 @@ +mod core; +mod patterns; +mod payload; +mod prelude; + +pub use core::*; +pub use patterns::*; +pub use payload::*; +pub use prelude::*; diff --git a/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/patterns.rs b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/patterns.rs new file mode 100644 index 0000000..196357f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/patterns.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferSampleEntry { + pub sample_index: usize, + pub name_tag_relative_offset: usize, + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + #[serde(default)] + pub tertiary_name: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary { + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + pub count: usize, + pub first_name_tag_relative_offset: usize, + pub prefix_leading_dword_matches_embedded_profile_tag: bool, + pub section_like_primary_name_count: usize, + pub cap_like_primary_name_count: usize, + pub other_primary_name_count: usize, + #[serde(default)] + pub first_primary_name: Option, + #[serde(default)] + pub first_secondary_name: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePairSummary { + pub primary_name: String, + pub secondary_name: String, + pub count: usize, + pub first_name_tag_relative_offset: usize, + pub unique_compact_prefix_pattern_count: usize, + pub dominant_prefix_leading_dword: u32, + pub dominant_prefix_leading_dword_hex: String, + pub dominant_prefix_trailing_word: u16, + pub dominant_prefix_trailing_word_hex: String, + pub dominant_prefix_separator_byte: u8, + pub dominant_prefix_separator_byte_hex: String, + pub dominant_prefix_count: usize, +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/payload.rs b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/payload.rs new file mode 100644 index 0000000..25ef9b2 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/payload.rs @@ -0,0 +1,254 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::structures::{ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern, + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSummary, + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSummary { + pub row_count_with_policy_tag_before_next_name: usize, + pub row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope: usize, + pub row_count_missing_policy_tag_before_next_name: usize, + pub row_count_missing_profile_tag_after_policy: usize, + #[serde(default)] + pub unique_policy_chunk_lens: Vec, + #[serde(default)] + pub unique_profile_chunk_lens: Vec, + #[serde(default)] + pub dominant_policy_chunk_len: Option, + pub dominant_policy_chunk_len_count: usize, + #[serde(default)] + pub dominant_profile_chunk_len: Option, + pub dominant_profile_chunk_len_count: usize, + #[serde(default)] + pub short_profile_flag_pair_summary: + Option, + #[serde(default)] + pub fixed_policy_summary: Option, + #[serde(default)] + pub name_prelude_candidate_summary: + Option, + #[serde(default)] + pub dominant_profile_span_class_summary: + Option, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanClassSummary { + pub profile_chunk_len_to_next_name_or_end: usize, + pub row_count: usize, + pub unique_name_pair_count: usize, + pub unique_compact_prefix_pattern_count: usize, + #[serde(default)] + pub dominant_candidate_pattern: + Option, + #[serde(default)] + pub dominant_primary_name: Option, + #[serde(default)] + pub dominant_secondary_name: Option, + pub dominant_name_pair_count: usize, + #[serde(default)] + pub dominant_prefix_leading_dword: Option, + #[serde(default)] + pub dominant_prefix_leading_dword_hex: Option, + #[serde(default)] + pub dominant_prefix_trailing_word: Option, + #[serde(default)] + pub dominant_prefix_trailing_word_hex: Option, + #[serde(default)] + pub dominant_prefix_separator_byte: Option, + #[serde(default)] + pub dominant_prefix_separator_byte_hex: Option, + pub dominant_prefix_count: usize, + #[serde(default)] + pub sample_rows: Vec, + #[serde(default)] + pub name_pair_summaries: + Vec, + #[serde(default)] + pub compact_prefix_pattern_summaries: + Vec, + #[serde(default)] + pub candidate_pattern_summaries: + Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanSample { + pub sample_index: usize, + pub name_tag_relative_offset: usize, + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + #[serde(default)] + pub child_count_candidate: Option, + #[serde(default)] + pub child_count_candidate_hex: Option, + #[serde(default)] + pub saved_primary_child_byte_candidate: Option, + #[serde(default)] + pub saved_primary_child_byte_candidate_hex: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanPrefixSummary { + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferFixedPolicySummary { + pub row_count_with_0x1a_policy_chunk: usize, + pub unique_trailing_word_count: usize, + #[serde(default)] + pub dominant_trailing_word: Option, + #[serde(default)] + pub dominant_trailing_word_hex: Option, + pub dominant_trailing_word_count: usize, + #[serde(default)] + pub compact_prefix_correlations: + Vec, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferFixedPolicySample { + pub sample_index: usize, + pub name_tag_relative_offset: usize, + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + #[serde(default)] + pub first_triplet_dwords_hex: Vec, + #[serde(default)] + pub second_triplet_dwords_hex: Vec, + pub trailing_word: u16, + pub trailing_word_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferFixedPolicyCompactPrefixCorrelation { + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + pub row_count: usize, + pub unique_policy_tuple_count: usize, + #[serde(default)] + pub dominant_primary_name: Option, + #[serde(default)] + pub dominant_secondary_name: Option, + pub dominant_name_pair_count: usize, + #[serde(default)] + pub dominant_mode_family: Option, + pub dominant_mode_family_count: usize, + #[serde(default)] + pub dominant_first_triplet_dwords_hex: Vec, + #[serde(default)] + pub dominant_second_triplet_dwords_hex: Vec, + #[serde(default)] + pub dominant_trailing_word: Option, + #[serde(default)] + pub dominant_trailing_word_hex: Option, + pub dominant_policy_tuple_count: usize, + #[serde(default)] + pub mode_family_counts: Vec, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSummary { + pub row_count_with_0x06_profile_span: usize, + pub unique_flag_pair_count: usize, + #[serde(default)] + pub dominant_first_flag_byte: Option, + #[serde(default)] + pub dominant_first_flag_byte_hex: Option, + pub dominant_first_flag_byte_count: usize, + #[serde(default)] + pub dominant_second_flag_byte: Option, + #[serde(default)] + pub dominant_second_flag_byte_hex: Option, + pub dominant_second_flag_byte_count: usize, + #[serde(default)] + pub dominant_flag_pair: Option, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPair { + pub first_flag_byte: u8, + pub first_flag_byte_hex: String, + pub second_flag_byte: u8, + pub second_flag_byte_hex: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSample { + pub sample_index: usize, + pub name_tag_relative_offset: usize, + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + pub first_flag_byte: u8, + pub first_flag_byte_hex: String, + pub second_flag_byte: u8, + pub second_flag_byte_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSample { + pub sample_index: usize, + pub name_tag_relative_offset: usize, + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + #[serde(default)] + pub name_payload_end_relative_offset: Option, + #[serde(default)] + pub policy_tag_relative_offset: Option, + #[serde(default)] + pub profile_tag_relative_offset: Option, + #[serde(default)] + pub next_name_tag_relative_offset: Option, + #[serde(default)] + pub name_to_policy_gap_len: Option, + #[serde(default)] + pub policy_chunk_len: Option, + #[serde(default)] + pub profile_chunk_len_to_next_name_or_end: Option, +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/prelude.rs b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/prelude.rs new file mode 100644 index 0000000..d6199d5 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/model/dynamic_side_buffer/prelude.rs @@ -0,0 +1,269 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::structures::SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSummary { + pub row_count_with_candidate_window: usize, + pub unique_candidate_pattern_count: usize, + #[serde(default)] + pub dominant_child_count_candidate: Option, + pub dominant_child_count_candidate_count: usize, + #[serde(default)] + pub dominant_saved_primary_child_byte_candidate: Option, + #[serde(default)] + pub dominant_saved_primary_child_byte_candidate_hex: Option, + pub dominant_saved_primary_child_byte_candidate_count: usize, + #[serde(default)] + pub dominant_candidate_pattern: + Option, + #[serde(default)] + pub candidate_pattern_correlations: + Vec, + #[serde(default)] + pub profile_span_correlations: + Vec, + #[serde(default)] + pub compact_prefix_correlations: + Vec, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + pub child_count_candidate: u16, + pub child_count_candidate_hex: String, + pub saved_primary_child_byte_candidate: u8, + pub saved_primary_child_byte_candidate_hex: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSample { + pub sample_index: usize, + pub name_tag_relative_offset: usize, + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + pub child_count_candidate: u16, + pub child_count_candidate_hex: String, + pub saved_primary_child_byte_candidate: u8, + pub saved_primary_child_byte_candidate_hex: String, + #[serde(default)] + pub previous_profile_chunk_len_to_next_name_or_end: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + pub row_count: usize, + pub unique_name_pair_count: usize, + pub unique_profile_span_count: usize, + #[serde(default)] + pub dominant_primary_name: Option, + #[serde(default)] + pub dominant_secondary_name: Option, + pub dominant_name_pair_count: usize, + #[serde(default)] + pub dominant_profile_span: Option, + pub dominant_profile_span_count: usize, + #[serde(default)] + pub dominant_candidate_pattern: + Option, + #[serde(default)] + pub dominant_mode_family: Option, + pub dominant_mode_family_count: usize, + #[serde(default)] + pub mode_family_counts: Vec, + #[serde(default)] + pub name_pair_summaries: + Vec, + #[serde(default)] + pub profile_span_counts: + Vec, + pub rows_with_previous_short_profile_flag_pair: usize, + #[serde(default)] + pub previous_short_profile_flag_pair_counts: + Vec, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { + pub previous_profile_chunk_len_to_next_name_or_end: usize, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { + pub first_flag_byte: u8, + pub first_flag_byte_hex: String, + pub second_flag_byte: u8, + pub second_flag_byte_hex: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { + pub sample_index: usize, + pub name_tag_relative_offset: usize, + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + pub child_count_candidate: u16, + pub child_count_candidate_hex: String, + pub saved_primary_child_byte_candidate: u8, + pub saved_primary_child_byte_candidate_hex: String, + #[serde(default)] + pub previous_profile_chunk_len_to_next_name_or_end: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePatternCorrelation { + pub child_count_candidate: u16, + pub child_count_candidate_hex: String, + pub saved_primary_child_byte_candidate: u8, + pub saved_primary_child_byte_candidate_hex: String, + pub row_count: usize, + pub unique_name_pair_count: usize, + pub unique_profile_span_count: usize, + #[serde(default)] + pub dominant_primary_name: Option, + #[serde(default)] + pub dominant_secondary_name: Option, + pub dominant_name_pair_count: usize, + #[serde(default)] + pub dominant_profile_span: Option, + pub dominant_profile_span_count: usize, + #[serde(default)] + pub dominant_mode_family: Option, + pub dominant_mode_family_count: usize, + #[serde(default)] + pub mode_family_counts: Vec, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + pub mode_family: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { + pub previous_profile_chunk_len_to_next_name_or_end: usize, + pub row_count: usize, + #[serde(default)] + pub dominant_child_count_candidate: Option, + pub dominant_child_count_candidate_count: usize, + #[serde(default)] + pub dominant_saved_primary_child_byte_candidate: Option, + #[serde(default)] + pub dominant_saved_primary_child_byte_candidate_hex: Option, + pub dominant_saved_primary_child_byte_candidate_count: usize, + #[serde(default)] + pub dominant_candidate_pattern: + Option, + #[serde(default)] + pub dominant_mode_family: Option, + pub dominant_mode_family_count: usize, + #[serde(default)] + pub mode_family_counts: Vec, + #[serde(default)] + pub compact_prefix_pattern_summaries: + Vec, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { + pub sample_index: usize, + pub name_tag_relative_offset: usize, + #[serde(default)] + pub primary_name: Option, + #[serde(default)] + pub secondary_name: Option, + pub prefix_leading_dword: u32, + pub prefix_leading_dword_hex: String, + pub prefix_trailing_word: u16, + pub prefix_trailing_word_hex: String, + pub prefix_separator_byte: u8, + pub prefix_separator_byte_hex: String, + pub child_count_candidate: u16, + pub child_count_candidate_hex: String, + pub saved_primary_child_byte_candidate: u8, + pub saved_primary_child_byte_candidate_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSummary { + pub live_entry_directory_row_count: usize, + pub decoded_live_entry_id_count: usize, + pub payload_relative_offset_monotonic: bool, + pub rows_with_payload_pointer_inside_records_span: usize, + pub rows_with_zero_child_count: usize, + pub rows_with_nonzero_child_count: usize, + pub rows_with_first_name_tag_after_prelude: usize, + pub rows_with_first_name_tag_at_offset_3: usize, + #[serde(default)] + pub unique_child_count_values: Vec, + #[serde(default)] + pub unique_first_name_tag_relative_offsets: Vec, + #[serde(default)] + pub dominant_child_count: Option, + pub dominant_child_count_count: usize, + #[serde(default)] + pub dominant_saved_primary_child_byte: Option, + #[serde(default)] + pub dominant_saved_primary_child_byte_hex: Option, + pub dominant_saved_primary_child_byte_count: usize, + #[serde(default)] + pub dominant_first_name_tag_relative_offset: Option, + pub dominant_first_name_tag_relative_offset_count: usize, + #[serde(default)] + pub sample_rows: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSample { + pub sample_index: usize, + pub live_entry_id: u32, + pub payload_relative_offset: u32, + pub payload_relative_offset_hex: String, + pub payload_relative_to_records: usize, + pub child_count: u16, + pub child_count_hex: String, + pub saved_primary_child_byte: u8, + pub saved_primary_child_byte_hex: String, + pub first_payload_dword_hex: String, + #[serde(default)] + pub first_name_tag_relative_offset: Option, + #[serde(default)] + pub first_primary_name: Option, + #[serde(default)] + pub first_secondary_name: Option, + #[serde(default)] + pub first_tertiary_name: Option, +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/model/mod.rs b/crates/rrt-runtime/src/inspect/smp/structures/model/mod.rs new file mode 100644 index 0000000..d497be9 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/model/mod.rs @@ -0,0 +1,7 @@ +mod alignment; +mod dynamic_side_buffer; +mod triplets; + +pub use alignment::*; +pub use dynamic_side_buffer::*; +pub use triplets::*; diff --git a/crates/rrt-runtime/src/inspect/smp/structures/model/triplets.rs b/crates/rrt-runtime/src/inspect/smp/structures/model/triplets.rs new file mode 100644 index 0000000..95ecfec --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/model/triplets.rs @@ -0,0 +1,81 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureRecordTripletEntryProbe { + pub record_index: usize, + pub primary_name: String, + pub secondary_name: String, + pub name_tag_relative_offset: usize, + pub policy_tag_relative_offset: usize, + pub profile_tag_relative_offset: usize, + pub policy_chunk_len: usize, + pub profile_chunk_len: usize, + pub policy_f32_lane_0: f32, + pub policy_f32_lane_1: f32, + pub policy_f32_lane_2: f32, + pub policy_f32_lane_3: f32, + pub policy_f32_lane_4: f32, + pub policy_reserved_dword: u32, + pub policy_trailing_word: u16, + pub policy_trailing_word_hex: String, + pub profile_open_marker: u32, + pub profile_open_marker_hex: String, + pub profile_repeated_primary_name: String, + pub profile_repeated_secondary_name: String, + pub profile_footer_relative_offset: usize, + pub profile_footer_relative_offset_hex: String, + pub profile_pre_footer_padding_len: usize, + #[serde(default)] + pub profile_pre_footer_padding_hex_bytes: Vec, + #[serde(default)] + pub profile_companion_byte_u8: Option, + #[serde(default)] + pub profile_companion_byte_hex: Option, + pub profile_payload_dword: u32, + pub profile_payload_dword_hex: String, + pub profile_sentinel_i32: i32, + pub profile_status_kind: String, + pub farm_growth_stage_index: Option, + pub profile_close_marker: u32, + pub profile_close_marker_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSavePlacedStructureRecordTripletProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub records_tag_offset: usize, + pub close_tag_offset: usize, + pub record_count: usize, + #[serde(default)] + pub entries: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPlacedStructureEntry { + pub record_index: usize, + pub primary_name: String, + pub secondary_name: String, + pub policy_trailing_word: u16, + pub policy_trailing_word_hex: String, + pub profile_payload_dword: u32, + pub profile_payload_dword_hex: String, + pub profile_status_kind: String, + #[serde(default)] + pub farm_growth_stage_index: Option, + #[serde(default)] + pub profile_companion_byte_u8: Option, + #[serde(default)] + pub profile_companion_byte_hex: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPlacedStructureCollection { + pub source_kind: String, + pub semantic_family: String, + pub observed_entry_count: usize, + #[serde(default)] + pub entries: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/queued_notice.rs b/crates/rrt-runtime/src/inspect/smp/structures/queued_notice.rs new file mode 100644 index 0000000..dd6c7a5 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/queued_notice.rs @@ -0,0 +1,44 @@ +use crate::inspect::smp::bundle::{ + classify_container_profile, classify_header_variant_probe, classify_secondary_variant_probe, + parse_shared_header, probe_early_content_layout, +}; +use crate::inspect::smp::common::find_first_ascii_run; +use crate::inspect::smp::regions::parse_save_region_collection_header_probe; +use crate::inspect::smp::structures::*; +use std::fs; +use std::path::Path; + +pub fn inspect_save_region_queued_notice_records_file( + path: &Path, +) -> Result, Box> { + let bytes = fs::read(path)?; + let file_extension_hint = path + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.to_ascii_lowercase()); + let shared_header = parse_shared_header(&bytes); + let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); + let first_ascii_run = find_first_ascii_run(&bytes); + let early_content_probe = first_ascii_run + .as_ref() + .and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run)); + let secondary_variant_probe = early_content_probe + .as_ref() + .and_then(classify_secondary_variant_probe); + let container_profile = classify_container_profile( + file_extension_hint.as_deref(), + header_variant_probe.as_ref(), + secondary_variant_probe.as_ref(), + ); + let save_region_collection_header_probe = parse_save_region_collection_header_probe( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + ); + Ok(parse_save_region_queued_notice_record_probe( + &bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), + save_region_collection_header_probe.as_ref(), + )) +} diff --git a/crates/rrt-runtime/src/inspect/smp/structures/triplets.rs b/crates/rrt-runtime/src/inspect/smp/structures/triplets.rs new file mode 100644 index 0000000..2b5ac84 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/structures/triplets.rs @@ -0,0 +1,246 @@ +use crate::inspect::smp::structures::*; + +pub(in crate::inspect::smp) fn derive_loaded_placed_structure_collection_from_probe( + probe: &SmpSavePlacedStructureRecordTripletProbe, +) -> SmpLoadedPlacedStructureCollection { + SmpLoadedPlacedStructureCollection { + source_kind: probe.source_kind.clone(), + semantic_family: "scenario-save-placed-structure-triplet-collection".to_string(), + observed_entry_count: probe.record_count, + entries: probe + .entries + .iter() + .map(|entry| SmpLoadedPlacedStructureEntry { + record_index: entry.record_index, + primary_name: entry.primary_name.clone(), + secondary_name: entry.secondary_name.clone(), + policy_trailing_word: entry.policy_trailing_word, + policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(), + profile_payload_dword: entry.profile_payload_dword, + profile_payload_dword_hex: entry.profile_payload_dword_hex.clone(), + profile_status_kind: entry.profile_status_kind.clone(), + farm_growth_stage_index: entry.farm_growth_stage_index, + profile_companion_byte_u8: entry.profile_companion_byte_u8, + profile_companion_byte_hex: entry.profile_companion_byte_hex.clone(), + }) + .collect(), + } +} + +pub(in crate::inspect::smp) fn parse_save_placed_structure_record_triplet_probe( + bytes: &[u8], + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + let header_probe = header_probe?; + if header_probe.source_kind != "save-placed-structure-tagged-header-counts" { + return None; + } + let records_payload = + bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?; + let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG); + let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG); + let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG); + let record_count = header_probe.live_record_count as usize; + if name_offsets.len() != record_count + || policy_offsets.len() != record_count + || profile_offsets.len() != record_count + { + return None; + } + let mut entries = Vec::with_capacity(record_count); + for index in 0..record_count { + let name_tag_relative_offset = name_offsets[index]; + let policy_tag_relative_offset = policy_offsets[index]; + let profile_tag_relative_offset = profile_offsets[index]; + let next_record_relative_offset = name_offsets + .get(index + 1) + .copied() + .unwrap_or(records_payload.len()); + if !(name_tag_relative_offset < policy_tag_relative_offset + && policy_tag_relative_offset < profile_tag_relative_offset + && profile_tag_relative_offset < next_record_relative_offset) + { + return None; + } + let name_payload = + records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?; + let (primary_name, secondary_name) = parse_save_len_prefixed_ascii_name_pair(name_payload)?; + let policy_chunk_len = + profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?; + if policy_chunk_len != 0x1a { + return None; + } + let policy_payload = + records_payload.get(policy_tag_relative_offset + 4..profile_tag_relative_offset)?; + let policy_f32_lane_0 = f32::from_bits(read_u32_at(policy_payload, 0)?); + let policy_f32_lane_1 = f32::from_bits(read_u32_at(policy_payload, 4)?); + let policy_f32_lane_2 = f32::from_bits(read_u32_at(policy_payload, 8)?); + let policy_f32_lane_3 = f32::from_bits(read_u32_at(policy_payload, 12)?); + let policy_f32_lane_4 = f32::from_bits(read_u32_at(policy_payload, 16)?); + let policy_reserved_dword = read_u32_at(policy_payload, 20)?; + let policy_trailing_word = read_u16_at(policy_payload, 24)?; + let profile_chunk_len = + next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?; + let profile_payload = + records_payload.get(profile_tag_relative_offset + 4..next_record_relative_offset)?; + let profile_open_marker = read_u32_at(profile_payload, 0)?; + if profile_open_marker != 0x00005dc1 { + return None; + } + let (profile_repeated_primary_name, profile_repeated_secondary_name) = + parse_save_len_prefixed_ascii_name_pair(profile_payload.get(4..)?)?; + let mut trailer_offset = 4usize; + let repeated_primary_len = *profile_payload.get(trailer_offset)? as usize; + trailer_offset += 1 + repeated_primary_len; + while matches!(profile_payload.get(trailer_offset), Some(0)) { + trailer_offset += 1; + } + let repeated_secondary_len = *profile_payload.get(trailer_offset)? as usize; + trailer_offset += 1 + repeated_secondary_len; + let mut matched_footer = None; + for candidate_offset in [trailer_offset, trailer_offset + 1] { + if let ( + Some(profile_payload_dword), + Some(profile_sentinel_i32), + Some(profile_close_marker), + ) = ( + read_u32_at(profile_payload, candidate_offset), + read_i32_at(profile_payload, candidate_offset + 4), + read_u32_at(profile_payload, candidate_offset + 8), + ) { + if profile_close_marker == 0x00005dc2 { + matched_footer = Some(( + candidate_offset, + profile_payload_dword, + profile_sentinel_i32, + profile_close_marker, + )); + break; + } + } + } + let ( + profile_footer_relative_offset, + profile_payload_dword, + profile_sentinel_i32, + profile_close_marker, + ) = matched_footer?; + let profile_pre_footer_padding = profile_payload + .get(trailer_offset..profile_footer_relative_offset)? + .iter() + .map(|byte| format!("0x{byte:02x}")) + .collect::>(); + let profile_companion_byte_u8 = if profile_pre_footer_padding.len() == 1 { + profile_payload.get(trailer_offset).copied() + } else { + None + }; + let (profile_status_kind, farm_growth_stage_index) = + derive_save_placed_structure_profile_status( + &primary_name, + &secondary_name, + profile_sentinel_i32, + ); + entries.push(SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: index, + primary_name, + secondary_name, + name_tag_relative_offset, + policy_tag_relative_offset, + profile_tag_relative_offset, + policy_chunk_len, + profile_chunk_len, + policy_f32_lane_0, + policy_f32_lane_1, + policy_f32_lane_2, + policy_f32_lane_3, + policy_f32_lane_4, + policy_reserved_dword, + policy_trailing_word, + policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"), + profile_open_marker, + profile_open_marker_hex: format!("0x{profile_open_marker:08x}"), + profile_repeated_primary_name, + profile_repeated_secondary_name, + profile_footer_relative_offset, + profile_footer_relative_offset_hex: format!("0x{profile_footer_relative_offset:x}"), + profile_pre_footer_padding_len: profile_pre_footer_padding.len(), + profile_pre_footer_padding_hex_bytes: profile_pre_footer_padding, + profile_companion_byte_hex: profile_companion_byte_u8 + .map(|byte| format!("0x{byte:02x}")), + profile_companion_byte_u8, + profile_payload_dword, + profile_payload_dword_hex: format!("0x{profile_payload_dword:08x}"), + profile_sentinel_i32, + profile_status_kind: profile_status_kind.to_string(), + farm_growth_stage_index, + profile_close_marker, + profile_close_marker_hex: format!("0x{profile_close_marker:08x}"), + }); + } + let farm_growth_stage_entry_count = entries + .iter() + .filter(|entry| entry.farm_growth_stage_index.is_some()) + .count(); + Some(SmpSavePlacedStructureRecordTripletProbe { + profile_family: header_probe.profile_family.clone(), + source_kind: "save-placed-structure-record-triplets".to_string(), + semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), + records_tag_offset: header_probe.records_tag_offset, + close_tag_offset: header_probe.close_tag_offset, + record_count, + entries, + evidence: vec![ + "save-side placed-structure records are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the tagged records span".to_string(), + "the 0x55f1 chunk currently exposes two len-prefixed structure-name stems before the fixed 0x55f2 policy row".to_string(), + "each fixed placed-structure 0x55f2 policy chunk currently decodes as five f32-like lanes, one reserved dword, and one trailing u16 word".to_string(), + format!( + "the compact 0x55f3 footer status lane behaves like a farm growth-stage bucket on grounded saves: {farm_growth_stage_entry_count} entries expose nonnegative 0..11 values and all observed non-farm families stay at -1" + ), + ], + }) +} + +pub(in crate::inspect::smp) fn derive_save_placed_structure_profile_status( + primary_name: &str, + secondary_name: &str, + raw_status: i32, +) -> (&'static str, Option) { + let looks_like_farm = primary_name.starts_with("Farm") || secondary_name.contains("Farm"); + if raw_status == -1 { + return ("unset", None); + } + if looks_like_farm && (0..=11).contains(&raw_status) { + return ("farm_growth_stage_bucket", Some(raw_status as u8)); + } + ("opaque_nondefault", None) +} + +pub(in crate::inspect::smp) fn parse_save_placed_structure_collection_header_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + parse_save_tagged_collection_header_probe( + bytes, + file_extension_hint, + container_profile, + 0x000036b1, + 0x000036b2, + 0x000036b3, + "save-placed-structure-tagged-header-counts", + "scenario-save-placed-structure-header-counts", + |header| { + header.direct_collection_flag == 0 + && header.direct_record_stride >= 1 + && header.direct_record_stride <= 0x20 + && header.live_id_bound >= 0x100 + && header.live_record_count >= 0x100 + && header.live_record_count <= header.live_id_bound + }, + vec![ + "save-side placed-structure collection uses tagged family 0x36b1/0x36b2/0x36b3 beneath the wider local-runtime and route-entry rebuild owners".to_string(), + "current evidence only grounds header-level placed-structure collection counts here; direct record-body reconstruction still needs the later per-entry load/save slot study.".to_string(), + ], + ) +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/events/collection.rs b/crates/rrt-runtime/src/inspect/smp/tests/events/collection.rs new file mode 100644 index 0000000..e079e38 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/events/collection.rs @@ -0,0 +1,615 @@ +use super::*; + +#[test] +fn parses_event_runtime_collection_summary_from_synthetic_chunks() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + + let header_words = [1u32, 4, 0, 0, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + + bytes.extend_from_slice(&[0x14, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]); + bytes.extend_from_slice(&[0x55, 0x66, 0x77, 0x88]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!(summary.packed_state_version, 0x3e9); + assert_eq!(summary.live_id_bound, 5); + assert_eq!(summary.live_record_count, 3); + assert_eq!(summary.live_entry_ids, vec![1, 3, 5]); + assert_eq!(summary.records_tag_offset, 96); + assert_eq!(summary.decoded_record_count, 0); + assert_eq!(summary.records_with_trigger_kind, 0); + assert_eq!(summary.records_missing_trigger_kind, 3); + assert_eq!(summary.nondirect_compact_record_count, 0); + assert!( + summary + .add_building_dispatch_strip_record_indexes + .is_empty() + ); + assert!( + summary + .add_building_dispatch_strip_descriptor_labels + .is_empty() + ); + assert_eq!(summary.records.len(), 3); + assert_eq!(summary.records[0].decode_status, "unsupported_framing"); +} + +#[test] +fn parses_event_runtime_collection_summary_from_u32_tag_chunks() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + + let header_words = [1u32, 4, 0, 0, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + + bytes.extend_from_slice(&[0x14, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]); + bytes.extend_from_slice(&[0x55, 0x66, 0x77, 0x88]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse from u32 tags"); + + assert_eq!(summary.packed_state_version, 0x3e9); + assert_eq!(summary.live_id_bound, 5); + assert_eq!(summary.live_record_count, 3); + assert_eq!(summary.live_entry_ids, vec![1, 3, 5]); + assert_eq!(summary.records_tag_offset, 98); + assert_eq!(summary.decoded_record_count, 0); + assert_eq!(summary.records_with_trigger_kind, 0); + assert_eq!(summary.records_missing_trigger_kind, 3); + assert_eq!(summary.nondirect_compact_record_count, 0); + assert!( + summary + .add_building_dispatch_strip_record_indexes + .is_empty() + ); + assert!( + summary + .add_building_dispatch_strip_descriptor_labels + .is_empty() + ); + assert_eq!(summary.records.len(), 3); + assert_eq!(summary.records[0].decode_status, "unsupported_framing"); +} + +#[test] +fn parses_nondirect_event_runtime_collection_summary_from_u32_tag_chunks() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + + let header_words = [ + 0u32, 6, 10, 20, 30, 3, 0, 1, 0, 0, 0, 0, 0, 1, 1, 23, 0, 0, 0, + ]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + + bytes.extend_from_slice(&[0u8; 18]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("non-direct event runtime collection summary should parse"); + + assert_eq!( + summary.source_kind, + "packed-event-runtime-collection-nondirect" + ); + assert_eq!(summary.packed_state_version, 0x3e9); + assert_eq!(summary.live_id_bound, 30); + assert_eq!(summary.live_record_count, 3); + assert_eq!(summary.live_entry_ids, vec![1, 2, 3]); + assert_eq!(summary.records_tag_offset, 102); + assert_eq!(summary.decoded_record_count, 0); + assert_eq!(summary.records_with_trigger_kind, 0); + assert_eq!(summary.records_missing_trigger_kind, 3); + assert_eq!(summary.nondirect_compact_record_count, 0); + assert_eq!(summary.nondirect_compact_records_missing_trigger_kind, 0); + assert_eq!(summary.records.len(), 3); + assert_eq!(summary.records[0].decode_status, "unsupported_framing"); +} + +#[test] +fn parses_nondirect_compact_event_runtime_record_rows() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + + let header_words = [ + 0u32, 6, 10, 20, 30, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 23, 0, 0, 0, + ]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + + bytes.extend_from_slice(&[0u8; 18]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); + + bytes.extend_from_slice(&(PACKED_EVENT_REAL_CONDITION_MARKER as u32).to_le_bytes()); + bytes.extend_from_slice(&1u32.to_le_bytes()); + bytes.extend_from_slice(&u32::MAX.to_le_bytes()); + bytes.push(4); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.push(2); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&(PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER as u32).to_le_bytes()); + bytes.extend_from_slice(&1u32.to_le_bytes()); + bytes.extend_from_slice(&43u32.to_le_bytes()); + bytes.extend_from_slice(&1u32.to_le_bytes()); + bytes.push(4); + bytes.extend_from_slice(&u32::MAX.to_le_bytes()); + bytes.extend_from_slice(&u32::MAX.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&[0u8; 12]); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&(PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER as u32).to_le_bytes()); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("non-direct event runtime collection summary should parse"); + assert_eq!(summary.records_with_trigger_kind, 0); + assert_eq!(summary.records_missing_trigger_kind, 1); + assert_eq!(summary.nondirect_compact_record_count, 1); + assert_eq!(summary.nondirect_compact_records_missing_trigger_kind, 1); + assert!( + summary + .add_building_dispatch_strip_record_indexes + .is_empty() + ); + assert!( + summary + .add_building_dispatch_strip_descriptor_labels + .is_empty() + ); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("all compact non-direct rows currently decode row bodies only") + })); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("records with grouped opcodes already in the 0x00431b20 dispatch strip = [0]") + })); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("decoded grouped rows already reach the 0x00431b20 dispatch strip") + })); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("every currently decoded dispatch-strip row") + && line.contains("0x4e99/0x4e9a/0x4e9b") + && line.contains("0x4e21/0x4e22") + })); + assert!( + summary + .control_lane_notes + .iter() + .any(|line| { line.contains("0x0042db20 allocates linked 0x1e/0x28 row nodes") }) + ); + assert!( + summary.control_lane_notes.iter().any(|line| { + line.contains("0x004dba23 sits under the event-editor duplication path") + }) + ); + assert!( + summary + .control_lane_notes + .iter() + .any(|line| { line.contains("0x00430b50 allocates a fresh live runtime-effect row") }) + ); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("0x00443200..0x004436e3") + && line.contains("0x005a57cf") + && line.contains("New Beginnings") + && line.contains("The American") + })); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("0x004323a0") + && line.contains("[event+0x81f]") + && line.contains("[event+0x7ef]") + && line.contains("0x00432ca1..0x00432cb0") + && line.contains("0x00438710") + })); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("0x00442c30") + && line.contains("0x00444b50") + && line.contains("0x0062be18/0x0062bae0") + && line.contains("Open Aus") + && line.contains("Win - Gold") + && line.contains("Win - Silver") + })); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("SP - GOLD") + && line.contains("0x00443526") + && line.contains("1 to 5") + && line.contains("Labor") + && line.contains("0x00443601") + && line.contains("0 to 2") + })); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains("0x00431b20 dispatch-strip opcodes present in decoded grouped rows = [4]") + })); + assert!(summary.control_lane_notes.iter().any(|line| { + line.contains( + "decoded grouped descriptor labels present in the 0x00431b20 dispatch strip = [\"Company Variable 1\"]", + ) + })); + let record = summary + .records + .first() + .expect("first compact non-direct record"); + + assert_eq!(record.decode_status, "parity_only"); + assert_eq!(record.payload_family, "real_packed_nondirect_compact_v1"); + assert_eq!(record.standalone_condition_row_count, 1); + assert_eq!(record.standalone_condition_rows.len(), 1); + assert_eq!(record.standalone_condition_rows[0].raw_condition_id, -1); + assert_eq!(record.standalone_condition_rows[0].subtype, 4); + assert_eq!(record.grouped_effect_row_counts, vec![1, 0, 0, 0]); + assert_eq!(record.grouped_effect_rows.len(), 1); + assert_eq!(record.grouped_effect_rows[0].descriptor_id, 43); + assert_eq!(record.grouped_effect_rows[0].raw_scalar_value, 1); + assert_eq!(record.grouped_effect_rows[0].opcode, 4); + assert!(record.notes.iter().any(|line| { + line.contains("compact non-direct 0x4e99/0x4e9a/0x4e9b map-bundle row framing") + })); + assert!( + record + .notes + .iter() + .any(|line| { line.contains("does not materialize the compact control lane") }) + ); + assert!( + record.notes.iter().any(|line| { + line.contains("0x0042e050 reached from editor duplication at 0x004dba23") + }) + ); + assert!( + record + .notes + .iter() + .any(|line| { line.contains("0x00430b50 allocates a fresh live runtime-effect row") }) + ); + assert!( + record + .notes + .iter() + .any(|line| { line.contains("compact signature family = nondirect-") }) + ); +} + +#[test] + +fn parses_synthetic_event_runtime_record_summaries_and_actions() { + let append_template = encode_template( + 99, + 0x0a, + 0x01, + &[encode_action_set_special_condition("Imported Follow-On", 1)], + ); + let record_body = build_synthetic_event_record( + 7, + 0x03, + 1, + [0, 1, 0, 0], + [b"Alpha", b"", b"", b"", b"", b""], + &[ + encode_action_set_world_flag("from_packed_root", true), + encode_action_append_template(append_template), + ], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC); + bytes.extend_from_slice(&(record_body.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!(summary.decoded_record_count, 1); + assert_eq!(summary.imported_runtime_record_count, 1); + assert_eq!(summary.records_with_trigger_kind, 1); + assert_eq!(summary.records_missing_trigger_kind, 0); + assert_eq!(summary.trigger_kinds_present, vec![7]); + assert_eq!(summary.records.len(), 1); + assert_eq!(summary.records[0].decode_status, "executable"); + assert_eq!(summary.records[0].payload_family, "synthetic_harness"); + assert_eq!(summary.records[0].trigger_kind, Some(7)); + assert_eq!(summary.records[0].text_bands[0].preview, "Alpha"); + assert_eq!(summary.records[0].standalone_condition_row_count, 1); + assert_eq!( + summary.records[0].grouped_effect_row_counts, + vec![0, 1, 0, 0] + ); + assert_eq!(summary.records[0].decoded_actions.len(), 2); + match &summary.records[0].decoded_actions[1] { + RuntimeEffect::AppendEventRecord { record } => { + assert_eq!(record.record_id, 99); + assert_eq!(record.trigger_kind, 0x0a); + } + other => panic!("unexpected decoded action: {other:?}"), + } +} + +#[test] +fn decodes_company_targeted_synthetic_records_as_parity_only() { + let record_body = build_synthetic_event_record( + 8, + 0x01, + 0, + [0, 0, 0, 0], + [b"", b"", b"", b"", b"", b""], + &[encode_action_adjust_company_cash_ids(&[7], 25)], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC); + bytes.extend_from_slice(&(record_body.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!(summary.decoded_record_count, 1); + assert_eq!(summary.imported_runtime_record_count, 1); + assert_eq!(summary.records_with_trigger_kind, 1); + assert_eq!(summary.records_missing_trigger_kind, 0); + assert_eq!(summary.trigger_kinds_present, vec![8]); + assert_eq!(summary.records[0].decode_status, "executable"); + assert_eq!(summary.records[0].payload_family, "synthetic_harness"); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn parses_real_style_event_runtime_record_with_zero_rows() { + let record_body = build_real_event_record( + [b"Alpha", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 1, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 1, 2, 3], + grouped_scope_checkboxes_0x7ff: [1, 0, 1, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, 10, -1, 22], + }), + &[], + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!(summary.decoded_record_count, 1); + assert_eq!(summary.imported_runtime_record_count, 0); + assert_eq!(summary.records[0].decode_status, "parity_only"); + assert_eq!(summary.records[0].payload_family, "real_packed_v1"); + assert_eq!(summary.records[0].trigger_kind, Some(7)); + assert_eq!(summary.records[0].one_shot, Some(true)); + assert_eq!( + summary.records[0] + .compact_control + .as_ref() + .expect("real compact control should parse") + .primary_selector_0x7f0, + 0x63 + ); + assert_eq!(summary.records[0].text_bands[0].preview, "Alpha"); + assert_eq!(summary.records[0].standalone_condition_row_count, 0); + assert_eq!(summary.records[0].standalone_condition_rows.len(), 0); + assert!(summary.records[0].negative_sentinel_scope.is_none()); + assert_eq!( + summary.records[0].grouped_effect_row_counts, + vec![0, 0, 0, 0] + ); + assert_eq!(summary.records[0].grouped_effect_rows.len(), 0); +} + +#[test] +fn parses_real_style_rows_and_side_strings() { + let condition_row = build_real_condition_row(-1, 4, 0x30, Some("AutoPlant")); + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 2, + opcode: 8, + raw_scalar_value: 7, + value_byte_0x09: 1, + value_dword_0x0d: 12, + value_byte_0x11: 2, + value_byte_0x12: 3, + value_word_0x14: 24, + value_word_0x16: 36, + locomotive_name: Some("Mikado"), + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"Gamma", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0x2a, + grouped_mode_0x7f4: 1, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 2, + modifier_flag_0x7fa: 3, + grouped_target_scope_ordinals_0x7fb: [1, 4, 7, 8], + grouped_scope_checkboxes_0x7ff: [0, 1, 0, 1], + summary_toggle_0x800: 0, + grouped_territory_selectors_0x80f: [11, -1, 33, -1], + }), + &[condition_row], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!(summary.records[0].standalone_condition_rows.len(), 1); + assert_eq!( + summary.records[0] + .compact_control + .as_ref() + .expect("real compact control should parse") + .grouped_target_scope_ordinals_0x7fb, + vec![1, 4, 7, 8] + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0].raw_condition_id, + -1 + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .candidate_name + .as_deref(), + Some("AutoPlant") + ); + let negative_sentinel_scope = summary.records[0] + .negative_sentinel_scope + .as_ref() + .expect("negative-sentinel scope summary should decode"); + assert_eq!( + negative_sentinel_scope.company_test_scope, + RuntimeCompanyConditionTestScope::SelectedCompanyOnly + ); + assert_eq!( + negative_sentinel_scope.player_test_scope, + RuntimePlayerConditionTestScope::AiPlayersOnly + ); + assert!(!negative_sentinel_scope.territory_scope_selector_is_0x63); + assert_eq!(negative_sentinel_scope.source_row_indexes, vec![0]); + assert_eq!(summary.records[0].grouped_effect_rows.len(), 1); + assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Company Cash") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0].target_mask_bits, + Some(0x01) + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0].row_shape, + "multivalue_scalar" + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .semantic_family + .as_deref(), + Some("multivalue_scalar") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .semantic_preview + .as_deref(), + Some("Set Company Cash to 7 with aux [2, 3, 24, 36]") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .locomotive_name + .as_deref(), + Some("Mikado") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::SelectedCompany, + value: 7, + }] + ); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/events/conditions_actions.rs b/crates/rrt-runtime/src/inspect/smp/tests/events/conditions_actions.rs new file mode 100644 index 0000000..2f96822 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/events/conditions_actions.rs @@ -0,0 +1,1037 @@ +use super::*; + +#[test] +fn decodes_real_special_condition_descriptor_from_checked_in_metadata() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 108, + opcode: 3, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Use Wartime Cargos") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("special_condition_scalar") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetSpecialCondition { + label: "Use Wartime Cargos".to_string(), + value: 1, + }] + ); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn decodes_real_candidate_availability_descriptor_from_checked_in_metadata() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 109, + opcode: 3, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Turbo Diesel Availability") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("candidate_availability_scalar") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetCandidateAvailability { + name: "Turbo Diesel".to_string(), + value: 1, + }] + ); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn decodes_real_special_condition_threshold_from_checked_in_world_condition_metadata() { + let condition_row = build_real_condition_row_with_threshold(3835, 0, 1, None); + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Special Condition: Use Wartime Cargos") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .semantic_family + .as_deref(), + Some("world_state_threshold") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::SpecialConditionThreshold { + label: "Use Wartime Cargos".to_string(), + comparator: RuntimeConditionComparator::Ge, + value: 1, + }] + ); +} + +#[test] +fn decodes_real_candidate_availability_threshold_from_checked_in_world_condition_metadata() { + let condition_row = build_real_condition_row_with_threshold( + REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, + 0, + 2, + Some("Mogul"), + ); + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Candidate Availability: Mogul") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::CandidateAvailabilityThreshold { + name: "Mogul".to_string(), + comparator: RuntimeConditionComparator::Ge, + value: 2, + }] + ); +} + +#[test] +fn decodes_real_economic_status_threshold_from_checked_in_world_condition_metadata() { + let condition_row = build_real_condition_row_with_threshold(2350, 0, 4, None); + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Economic Status") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::EconomicStatusCodeThreshold { + comparator: RuntimeConditionComparator::Ge, + value: 4, + }] + ); +} + +#[test] +fn decodes_real_named_locomotive_availability_threshold_from_checked_in_metadata() { + let condition_row = build_real_condition_row_with_threshold( + REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID, + 4, + 42, + Some("Big Boy"), + ); + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Named Locomotive Availability: Big Boy") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .semantic_family + .as_deref(), + Some("world_scalar_threshold") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Big Boy".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 42, + }] + ); +} + +#[test] +fn decodes_real_named_locomotive_cost_threshold_from_checked_in_metadata() { + let condition_row = build_real_condition_row_with_threshold( + REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID, + 0, + 250000, + Some("Locomotive 1"), + ); + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Named Locomotive Cost: Locomotive 1") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::NamedLocomotiveCostThreshold { + name: "Locomotive 1".to_string(), + comparator: RuntimeConditionComparator::Ge, + value: 250000, + }] + ); +} + +#[test] +fn decodes_real_named_cargo_production_threshold_from_checked_in_metadata() { + let condition_row = build_real_condition_row_with_threshold( + REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID, + 4, + 125, + Some("Cargo Production Slot 1"), + ); + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Cargo Production: Cargo Production Slot 1") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0].recovered_cargo_slot, + Some(1) + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::CargoProductionSlotThreshold { + slot: 1, + label: "Cargo Production Slot 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 125, + }] + ); +} + +#[test] +fn decodes_real_world_scalar_thresholds_from_checked_in_metadata() { + let condition_rows = vec![ + build_real_condition_row_with_threshold( + REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID, + 0, + 200, + None, + ), + build_real_condition_row_with_threshold( + REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID, + 4, + 125, + None, + ), + build_real_condition_row_with_threshold( + REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID, + 4, + 75, + None, + ), + build_real_condition_row_with_threshold( + REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID, + 4, + 30, + None, + ), + build_real_condition_row_with_threshold( + REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID, + 4, + 18, + None, + ), + build_real_condition_row_with_threshold( + REAL_TERRITORY_ACCESS_COST_CONDITION_ID, + 4, + 750000, + None, + ), + ]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &condition_rows, + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Cargo Production Total") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .semantic_family + .as_deref(), + Some("world_scalar_threshold") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![ + RuntimeCondition::CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Ge, + value: 200, + }, + RuntimeCondition::FactoryProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, + RuntimeCondition::FarmMineProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 75, + }, + RuntimeCondition::OtherCargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 30, + }, + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 18, + }, + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 750000, + }, + ] + ); +} + +#[test] +fn decodes_real_world_flag_condition_from_checked_in_world_condition_metadata() { + let condition_row = build_real_condition_row_with_threshold(2535, 4, 1, None); + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .metric + .as_deref(), + Some("World Flag: Disable Stock Buying and Selling") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .semantic_family + .as_deref(), + Some("world_flag_equals") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::WorldFlagEquals { + key: "world.disable_stock_buying_and_selling".to_string(), + value: true, + }] + ); +} + +#[test] +fn looks_up_checked_in_world_scalar_condition_metadata() { + let named_cargo = real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID) + .expect("named cargo condition metadata should exist"); + assert_eq!(named_cargo.label, "%1 Production"); + + let availability = + real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID) + .expect("availability condition metadata should exist"); + assert_eq!(availability.label, "Unknown Loco Available"); + + let cost = real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID) + .expect("cost condition metadata should exist"); + assert_eq!(cost.label, "Unknown Loco Cost"); + + let cargo = real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID) + .expect("cargo condition metadata should exist"); + assert_eq!(cargo.label, "All Cargo Production"); + + let factory = real_ordinary_condition_metadata(REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID) + .expect("factory production condition metadata should exist"); + assert_eq!(factory.label, "All Factory Production"); + + let farm_mine = real_ordinary_condition_metadata(REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID) + .expect("farm/mine production condition metadata should exist"); + assert_eq!(farm_mine.label, "All Farm/Mine Production"); + + let build_limit = + real_ordinary_condition_metadata(REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID) + .expect("build-limit condition metadata should exist"); + assert_eq!(build_limit.label, "Limited Track Building Amount"); + + let access_cost = real_ordinary_condition_metadata(REAL_TERRITORY_ACCESS_COST_CONDITION_ID) + .expect("territory-access-cost condition metadata should exist"); + assert_eq!(access_cost.label, "Access Rights Cost:"); +} + +#[test] +fn looks_up_checked_in_chairman_and_governance_condition_metadata() { + let world_variable = real_ordinary_condition_metadata(REAL_WORLD_VARIABLE_1_CONDITION_ID) + .expect("world-variable condition metadata should exist"); + assert_eq!(world_variable.label, "Game Variable 1"); + + let player_variable = real_ordinary_condition_metadata(REAL_PLAYER_VARIABLE_3_CONDITION_ID) + .expect("player-variable condition metadata should exist"); + assert_eq!(player_variable.label, "Player Variable 3"); + + let chairman_cash = real_ordinary_condition_metadata(REAL_CHAIRMAN_CASH_CONDITION_ID) + .expect("chairman cash condition metadata should exist"); + assert_eq!(chairman_cash.label, "Player Cash"); + + let holdings = real_ordinary_condition_metadata(REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID) + .expect("chairman holdings condition metadata should exist"); + assert_eq!(holdings.label, "Player Stock Value"); + + let net_worth = real_ordinary_condition_metadata(REAL_CHAIRMAN_NET_WORTH_CONDITION_ID) + .expect("chairman net worth condition metadata should exist"); + assert_eq!(net_worth.label, "Player Net Worth"); + + let purchasing_power = + real_ordinary_condition_metadata(REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID) + .expect("chairman purchasing-power condition metadata should exist"); + assert_eq!(purchasing_power.label, "Purchasing Power"); + + let investor_confidence = + real_ordinary_condition_metadata(REAL_INVESTOR_CONFIDENCE_CONDITION_ID) + .expect("investor-confidence condition metadata should exist"); + assert_eq!(investor_confidence.label, "Investor Confidence"); + + let credit_rating = real_ordinary_condition_metadata(REAL_CREDIT_RATING_CONDITION_ID) + .expect("credit-rating condition metadata should exist"); + assert_eq!(credit_rating.label, "Credit Rating"); + + let prime_rate = real_ordinary_condition_metadata(REAL_PRIME_RATE_CONDITION_ID) + .expect("prime-rate condition metadata should exist"); + assert_eq!(prime_rate.label, "Prime Rate"); + + let management_attitude = + real_ordinary_condition_metadata(REAL_MANAGEMENT_ATTITUDE_CONDITION_ID) + .expect("management-attitude condition metadata should exist"); + assert_eq!(management_attitude.label, "Management Attitude"); + + let book_value = real_ordinary_condition_metadata(REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID) + .expect("book value condition metadata should exist"); + assert_eq!(book_value.label, "Book Value Per Share"); +} + +#[test] +fn decodes_world_variable_condition() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_WORLD_VARIABLE_1_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&111_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Game Variable 1".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Game Variable 1 == 111".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + + assert_eq!( + decode_real_condition_row(&row, None), + Some(RuntimeCondition::WorldVariableThreshold { + index: 1, + comparator: RuntimeConditionComparator::Eq, + value: 111, + }) + ); +} + +#[test] +fn decodes_player_variable_condition_from_selected_player_scope() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_PLAYER_VARIABLE_3_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&333_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Player Variable 3".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Player Variable 3 == 333".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + }; + + assert_eq!( + decode_real_condition_row(&row, Some(&negative_scope)), + Some(RuntimeCondition::PlayerVariableThreshold { + target: RuntimePlayerTarget::SelectedPlayer, + index: 3, + comparator: RuntimeConditionComparator::Eq, + value: 333, + }) + ); +} + +#[test] +fn decodes_territory_variable_condition_with_world_territory_scope() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_TERRITORY_VARIABLE_4_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&444_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Territory Variable 4".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Territory Variable 4 == 444".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::Disabled, + territory_scope_selector_is_0x63: true, + source_row_indexes: vec![0], + }; + + assert_eq!( + decode_real_condition_row(&row, Some(&negative_scope)), + Some(RuntimeCondition::TerritoryVariableThreshold { + target: RuntimeTerritoryTarget::AllTerritories, + index: 4, + comparator: RuntimeConditionComparator::Eq, + value: 444, + }) + ); +} + +#[test] +fn decodes_chairman_cash_condition_from_selected_player_scope() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&500_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Player Cash".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Player Cash == 500".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + }; + + assert_eq!( + decode_real_condition_row(&row, Some(&negative_scope)), + Some(RuntimeCondition::ChairmanNumericThreshold { + target: RuntimeChairmanTarget::SelectedChairman, + metric: RuntimeChairmanMetric::CurrentCash, + comparator: RuntimeConditionComparator::Eq, + value: 500, + }) + ); +} + +#[test] +fn decodes_book_value_per_share_condition_to_company_metric() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Book Value Per Share".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Book Value Per Share == 2620".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + + assert_eq!( + decode_real_condition_row(&row, None), + Some(RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric: RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }) + ); +} + +#[test] +fn decodes_investor_confidence_condition_to_company_metric() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_INVESTOR_CONFIDENCE_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&37_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Investor Confidence".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Investor Confidence == 37".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + + assert_eq!( + decode_real_condition_row(&row, None), + Some(RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric: RuntimeCompanyMetric::InvestorConfidence, + comparator: RuntimeConditionComparator::Eq, + value: 37, + }) + ); +} + +#[test] +fn decodes_management_attitude_condition_to_company_metric() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_MANAGEMENT_ATTITUDE_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&58_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Management Attitude".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Management Attitude == 58".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + + assert_eq!( + decode_real_condition_row(&row, None), + Some(RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric: RuntimeCompanyMetric::ManagementAttitude, + comparator: RuntimeConditionComparator::Eq, + value: 58, + }) + ); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/events/descriptors.rs b/crates/rrt-runtime/src/inspect/smp/tests/events/descriptors.rs new file mode 100644 index 0000000..619498d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/events/descriptors.rs @@ -0,0 +1,1159 @@ +use super::*; + +#[test] +fn looks_up_checked_in_world_flag_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(110).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Disable Stock Buying and Selling"); + assert_eq!(metadata.parameter_family, "world_flag_toggle"); + assert_eq!( + metadata.runtime_key, + Some("world.disable_stock_buying_and_selling") + ); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_checked_in_credit_rating_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(56).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Credit Rating"); + assert_eq!(metadata.target_mask_bits, 0x0b); + assert_eq!(metadata.parameter_family, "company_governance_scalar"); + assert_eq!( + real_grouped_effect_runtime_status_name(metadata.runtime_status), + "executable" + ); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn checked_in_event_effect_table_covers_the_full_exported_descriptor_set() { + let rows = checked_in_event_effect_descriptor_rows(); + assert_eq!(rows.len(), 614); + for descriptor_id in 0..614_u32 { + assert!( + real_grouped_effect_descriptor_metadata(descriptor_id).is_some(), + "descriptor {descriptor_id} should be recoverable from the checked-in effect table" + ); + } +} + +#[test] +fn looks_up_checked_in_prime_rate_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(57).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Prime Rate"); + assert_eq!(metadata.target_mask_bits, 0x0b); + assert_eq!(metadata.parameter_family, "company_governance_scalar"); + assert_eq!( + real_grouped_effect_runtime_status_name(metadata.runtime_status), + "executable" + ); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn classifies_shell_owned_finance_descriptors_from_checked_in_effect_table() { + let stock_prices = + real_grouped_effect_descriptor_metadata(55).expect("descriptor metadata should exist"); + assert_eq!(stock_prices.label, "Stock Prices"); + assert_eq!( + stock_prices.parameter_family, + "company_finance_shell_scalar" + ); + assert_eq!( + real_grouped_effect_runtime_status_name(stock_prices.runtime_status), + "shell_owned" + ); + assert!(!stock_prices.executable_in_runtime); + + let merger_premium = + real_grouped_effect_descriptor_metadata(58).expect("descriptor metadata should exist"); + assert_eq!(merger_premium.label, "Merger Premium"); + assert_eq!( + merger_premium.parameter_family, + "company_finance_shell_scalar" + ); + assert_eq!( + real_grouped_effect_runtime_status_name(merger_premium.runtime_status), + "shell_owned" + ); + assert!(!merger_premium.executable_in_runtime); +} + +#[test] +fn decodes_credit_rating_descriptor_into_company_governance_scalar_effect() { + let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 56, + raw_scalar_value: 640, + opcode: 3, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let record_body = build_real_event_record( + [b"Gov", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [1, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&[row_bytes], &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Credit Rating") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .grouped_target_subject + .as_deref(), + Some("company") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::SelectedCompany, + metric: RuntimeCompanyMetric::CreditRating, + value: 640, + }] + ); +} + +#[test] +fn looks_up_recovered_world_toggle_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(111).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Disable Margin Buying/Short Selling Stock"); + assert_eq!(metadata.parameter_family, "world_flag_toggle"); + assert_eq!( + runtime_world_flag_key(metadata), + Some("world.disable_margin_buying_short_selling_stock".to_string()) + ); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_limited_track_building_amount_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(122).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Limited Track Building Amount"); + assert_eq!(metadata.parameter_family, "world_track_build_limit_scalar"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_recovered_late_world_toggle_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(143).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Disable Train Crashes AND Breakdowns"); + assert_eq!(metadata.parameter_family, "world_flag_toggle"); + assert_eq!( + runtime_world_flag_key(metadata), + Some("world.disable_train_crashes_and_breakdowns".to_string()) + ); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_recovered_locomotive_availability_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(250).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Big Boy 4-8-8-4 Availability"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "locomotive_availability_scalar"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_upper_band_recovered_locomotive_availability_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(457).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Upper-Band Locomotive Availability Slot 1"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "locomotive_availability_scalar"); + assert_eq!(recovered_locomotive_availability_loco_id(457), None); +} + +#[test] +fn looks_up_extended_lower_band_locomotive_availability_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(301).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Zephyr Availability"); + assert_eq!(metadata.parameter_family, "locomotive_availability_scalar"); + assert_eq!(recovered_locomotive_availability_loco_id(301), Some(61)); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn parses_recovered_locomotive_availability_row_with_structured_locomotive_id() { + let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 250, + raw_scalar_value: 1, + opcode: 3, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + + let row = + parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None).expect("row should parse"); + + assert_eq!(row.descriptor_id, 250); + assert_eq!( + row.descriptor_label.as_deref(), + Some("Big Boy 4-8-8-4 Availability") + ); + assert_eq!(row.recovered_locomotive_id, Some(10)); + assert_eq!( + row.parameter_family.as_deref(), + Some("locomotive_availability_scalar") + ); +} + +#[test] +fn looks_up_recovered_cargo_production_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(230).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Cargo Production Slot 1"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "cargo_production_scalar"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_recovered_all_cargo_price_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(105).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "All Cargo Prices"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "cargo_price_scalar"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_named_cargo_price_slot_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(106).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Alcohol Price"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "cargo_price_scalar"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_runtime_variable_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(43).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Company Variable 1"); + assert_eq!(metadata.target_mask_bits, 0x01); + assert_eq!(metadata.parameter_family, "runtime_variable_scalar"); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_recovered_aggregate_cargo_production_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(177).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "All Cargo Production"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "cargo_production_scalar"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_grounded_named_cargo_production_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(180).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Alcohol Production"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "cargo_production_scalar"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_recovered_lower_band_locomotive_cost_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(352).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "2-D-2 Cost"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "locomotive_cost_scalar"); + assert_eq!(metadata.runtime_key, None); + assert_eq!(recovered_locomotive_cost_loco_id(352), Some(1)); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_recovered_upper_band_locomotive_cost_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(475).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Upper-Band Locomotive Cost Slot 1"); + assert_eq!(metadata.parameter_family, "locomotive_cost_scalar"); + assert_eq!(recovered_locomotive_cost_loco_id(475), None); + assert!(!metadata.executable_in_runtime); +} + +#[test] +fn looks_up_extended_lower_band_locomotive_cost_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(412).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Zephyr Cost"); + assert_eq!(metadata.parameter_family, "locomotive_cost_scalar"); + assert_eq!(recovered_locomotive_cost_loco_id(412), Some(61)); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn looks_up_recovered_territory_access_cost_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(453).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Territory Access Cost"); + assert_eq!(metadata.target_mask_bits, 0x08); + assert_eq!(metadata.parameter_family, "territory_access_cost_scalar"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn parses_recovered_locomotive_cost_row_with_structured_locomotive_id() { + let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 352, + raw_scalar_value: 250000, + opcode: 3, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + + let row = + parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None).expect("row should parse"); + + assert_eq!(row.descriptor_id, 352); + assert_eq!(row.descriptor_label.as_deref(), Some("2-D-2 Cost")); + assert_eq!(row.recovered_locomotive_id, Some(1)); + assert_eq!( + row.parameter_family.as_deref(), + Some("locomotive_cost_scalar") + ); +} + +#[test] +fn parses_grounded_named_cargo_production_row_with_label() { + let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 180, + raw_scalar_value: 160, + opcode: 3, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + + let row = + parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None).expect("row should parse"); + + assert_eq!(row.descriptor_id, 180); + assert_eq!(row.descriptor_label.as_deref(), Some("Alcohol Production")); + assert_eq!(row.recovered_cargo_label.as_deref(), Some("Alcohol")); + assert_eq!( + row.parameter_family.as_deref(), + Some("cargo_production_scalar") + ); +} + +#[test] +fn parses_grounded_named_cargo_price_row_with_label() { + let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 106, + raw_scalar_value: 140, + opcode: 3, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + + let row = + parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None).expect("row should parse"); + + assert_eq!(row.descriptor_id, 106); + assert_eq!(row.descriptor_label.as_deref(), Some("Alcohol Price")); + assert_eq!(row.recovered_cargo_label.as_deref(), Some("Alcohol")); + assert_eq!(row.parameter_family.as_deref(), Some("cargo_price_scalar")); +} + +#[test] +fn looks_up_recovered_locomotive_policy_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(454).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "All Steam Locos Avail."); + assert_eq!(metadata.parameter_family, "world_flag_toggle"); + assert_eq!( + metadata.runtime_key, + Some("world.all_steam_locos_available") + ); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn decodes_recovered_locomotive_policy_descriptor_from_checked_in_metadata() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 454, + opcode: 0, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"Locos", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("All Steam Locos Avail.") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("world_flag_toggle") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetWorldFlag { + key: "world.all_steam_locos_available".to_string(), + value: true, + }] + ); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn looks_up_checked_in_deactivate_player_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(14).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Deactivate Player"); + assert_eq!(metadata.parameter_family, "player_lifecycle_toggle"); + assert_eq!(metadata.runtime_key, None); + assert!(metadata.executable_in_runtime); +} + +#[test] +fn decodes_real_deactivate_player_descriptor_from_checked_in_metadata() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 14, + opcode: 1, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"Players", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [1, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Deactivate Player") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("player_lifecycle_toggle") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::DeactivatePlayer { + target: RuntimePlayerTarget::SelectedPlayer, + }] + ); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn decodes_real_world_flag_descriptor_with_checked_in_runtime_key() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 110, + opcode: 0, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Disable Stock Buying and Selling") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("world_flag_toggle") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetWorldFlag { + key: "world.disable_stock_buying_and_selling".to_string(), + value: true, + }] + ); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn decodes_recovered_world_toggle_descriptor_family() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 131, + opcode: 0, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Disable Starting Any Companies") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("world_flag_toggle") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetWorldFlag { + key: "world.disable_starting_any_companies".to_string(), + value: true, + }] + ); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn decodes_limited_track_building_amount_descriptor_family() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 122, + opcode: 3, + raw_scalar_value: 18, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Limited Track Building Amount") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("world_track_build_limit_scalar") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }] + ); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn decodes_recovered_late_world_toggle_descriptor_family() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 144, + opcode: 0, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("AI Ignore Territories At Startup") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("world_flag_toggle") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetWorldFlag { + key: "world.ai_ignore_territories_at_startup".to_string(), + value: true, + }] + ); + assert!(summary.records[0].executable_import_ready); +} + +#[test] +fn decodes_negative_sentinel_scope_modifiers_and_territory_marker() { + for (value, expected) in [ + (0, RuntimeCompanyConditionTestScope::Disabled), + (1, RuntimeCompanyConditionTestScope::AllCompanies), + (2, RuntimeCompanyConditionTestScope::SelectedCompanyOnly), + (3, RuntimeCompanyConditionTestScope::AiCompaniesOnly), + (4, RuntimeCompanyConditionTestScope::HumanCompaniesOnly), + ] { + assert_eq!(decode_company_condition_test_scope(value), Some(expected)); + } + for (value, expected) in [ + (0, RuntimePlayerConditionTestScope::Disabled), + (1, RuntimePlayerConditionTestScope::AllPlayers), + (2, RuntimePlayerConditionTestScope::SelectedPlayerOnly), + (3, RuntimePlayerConditionTestScope::AiPlayersOnly), + (4, RuntimePlayerConditionTestScope::HumanPlayersOnly), + ] { + assert_eq!(decode_player_condition_test_scope(value), Some(expected)); + } + + let rows = vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: -1, + subtype: 4, + flag_bytes: vec![0x30; 25], + candidate_name: Some("AutoPlant".to_string()), + comparator: None, + metric: None, + semantic_family: None, + semantic_preview: None, + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }]; + let summary = derive_negative_sentinel_scope_summary( + &rows, + &SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 1, + modifier_flag_0x7f9: 4, + modifier_flag_0x7fa: 2, + grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }, + ) + .expect("negative sentinel summary should derive"); + assert_eq!( + summary.company_test_scope, + RuntimeCompanyConditionTestScope::HumanCompaniesOnly + ); + assert_eq!( + summary.player_test_scope, + RuntimePlayerConditionTestScope::SelectedPlayerOnly + ); + assert!(summary.territory_scope_selector_is_0x63); + assert_eq!(summary.source_row_indexes, vec![0]); +} + +#[test] +fn classifies_real_grouped_row_semantic_families() { + let grouped_rows = vec![ + build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 2, + opcode: 1, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }), + build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 2, + opcode: 4, + raw_scalar_value: 25, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 2, + value_word_0x16: 6, + locomotive_name: None, + }), + build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 2, + opcode: 3, + raw_scalar_value: 250, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }), + build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 2, + opcode: 8, + raw_scalar_value: 7, + value_byte_0x09: 1, + value_dword_0x0d: 12, + value_byte_0x11: 2, + value_byte_0x12: 3, + value_word_0x14: 24, + value_word_0x16: 36, + locomotive_name: Some("Mikado"), + }), + ]; + let record_body = build_real_event_record( + [b"Semantic", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 1, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 0, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&grouped_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + let families = summary.records[0] + .grouped_effect_rows + .iter() + .map(|row| row.semantic_family.as_deref().unwrap_or("")) + .collect::>(); + assert_eq!( + families, + vec![ + "bool_toggle", + "timed_duration", + "scalar_assignment", + "multivalue_scalar", + ] + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .semantic_preview + .as_deref(), + Some("Set Company Cash to TRUE") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[1] + .semantic_preview + .as_deref(), + Some("Set Company Cash to 25 for 2 years 6 months") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[2] + .semantic_preview + .as_deref(), + Some("Set Company Cash to 250") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[3] + .semantic_preview + .as_deref(), + Some("Set Company Cash to 7 with aux [2, 3, 24, 36]") + ); +} + +#[test] +fn rejects_truncated_real_style_event_runtime_record() { + let mut record_body = build_real_event_record( + [b"Oops", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 5, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 0, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 0, + grouped_territory_selectors_0x80f: [0, 0, 0, 0], + }), + &[], + [&[], &[], &[], &[]], + ); + record_body.pop(); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!(summary.records[0].decode_status, "unsupported_framing"); + assert_eq!(summary.records[0].payload_family, "unsupported_framing"); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/events/mod.rs b/crates/rrt-runtime/src/inspect/smp/tests/events/mod.rs new file mode 100644 index 0000000..cf84961 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/events/mod.rs @@ -0,0 +1,5 @@ +use super::*; + +mod collection; +mod conditions_actions; +mod descriptors; diff --git a/crates/rrt-runtime/src/inspect/smp/tests/mod.rs b/crates/rrt-runtime/src/inspect/smp/tests/mod.rs new file mode 100644 index 0000000..60520a1 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/mod.rs @@ -0,0 +1,22 @@ +use super::bundle::*; +use super::events::*; +use super::map_title::*; +use super::profiles::*; +use super::regions::*; +use super::save_load::*; +use super::special_conditions::*; +use super::structures::*; +use super::world::*; +use super::*; + +mod events; +mod profiles; +mod regions; +mod save_load; +mod services; +mod special_conditions; +mod structures; +mod support; +mod world; + +use support::*; diff --git a/crates/rrt-runtime/src/inspect/smp/tests/profiles.rs b/crates/rrt-runtime/src/inspect/smp/tests/profiles.rs new file mode 100644 index 0000000..b7a717e --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/profiles.rs @@ -0,0 +1,722 @@ +use super::*; + +#[test] +fn classifies_rt3_105_post_span_bridge_variants() { + let base_trailer = SmpRuntimeTrailerBlock { + profile_family: "rt3-105-save-container-v1".to_string(), + trailer_family: "rt3-105-save-trailer-v1".to_string(), + trailer_evidence: vec![], + trailer_offset: 944, + prefix_words_0_to_5: vec![], + prefix_hex_words_0_to_5: vec![], + tag_word_6: 0, + tag_word_6_hex: String::new(), + tag_chunk_id_u16: 0x2ee1, + tag_chunk_id_hex: "0x2ee1".to_string(), + tag_chunk_id_grounded_alignment: None, + length_word_7: 0x32c8_0000, + length_word_7_hex: "0x32c80000".to_string(), + length_high_u16: 0x32c8, + length_high_hex: "0x32c8".to_string(), + selector_word_8: 0x7110_0000, + selector_word_8_hex: "0x71100000".to_string(), + selector_high_u16: 0x7110, + selector_high_hex: "0x7110".to_string(), + layout_word_9: 0, + layout_word_9_hex: String::new(), + descriptor_word_10: 0x7801_0000, + descriptor_word_10_hex: "0x78010000".to_string(), + descriptor_high_u16: 0x7801, + descriptor_high_hex: "0x7801".to_string(), + descriptor_word_11: 0, + descriptor_word_11_hex: String::new(), + counter_word_12: 0, + counter_word_12_hex: String::new(), + offset_word_13: 0, + offset_word_13_hex: String::new(), + span_word_14: 0, + span_word_14_hex: String::new(), + mode_word_15: 0, + mode_word_15_hex: String::new(), + words: vec![], + hex_words: vec![], + }; + let base_post_span = SmpRuntimePostSpanProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + span_target_offset: 13944, + next_nonzero_offset: Some(14795), + next_aligned_candidate_offset: Some(20244), + next_aligned_candidate_words: vec![], + next_aligned_candidate_hex_words: vec![], + header_candidates: vec![SmpRuntimePostSpanHeaderCandidate { + offset: 20244, + words: vec![], + hex_words: vec![], + dense_word_count: 3, + high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515], + high_hex_words: vec![], + grounded_alignments: vec![], + }], + grounded_progress_hits: vec![], + }; + let base_profile = SmpRt3105PackedProfileProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + packed_profile_offset: 29632, + packed_profile_len: 0x108, + packed_profile_len_hex: "0x108".to_string(), + 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: 0x0100_0000, + 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: 0, + profile_byte_0x97_hex: "0x00".to_string(), + profile_byte_0xc5: 0, + profile_byte_0xc5_hex: "0x00".to_string(), + stable_nonzero_words: vec![], + }, + ascii_runs: vec![], + }; + let base_bridge = parse_rt3_105_post_span_bridge_probe( + Some(&base_trailer), + Some(&base_post_span), + Some(&base_profile), + ) + .expect("base bridge should parse"); + assert_eq!( + base_bridge.bridge_family, + "rt3-105-save-post-span-bridge-v1" + ); + assert_eq!(base_bridge.packed_profile_delta_from_span_target, 15688); + assert_eq!( + base_bridge.next_candidate_delta_from_packed_profile, + Some(-9388) + ); + let base_variant_trailer = SmpRuntimeTrailerBlock { + descriptor_word_10: 0x7401_0000, + descriptor_word_10_hex: "0x74010000".to_string(), + descriptor_high_u16: 0x7401, + descriptor_high_hex: "0x7401".to_string(), + ..base_trailer.clone() + }; + let base_variant_bridge = parse_rt3_105_post_span_bridge_probe( + Some(&base_variant_trailer), + Some(&base_post_span), + Some(&base_profile), + ) + .expect("base bridge variant should parse"); + assert_eq!( + base_variant_bridge.bridge_family, + "rt3-105-save-post-span-bridge-v1" + ); + + let alt_trailer = SmpRuntimeTrailerBlock { + profile_family: "rt3-105-alt-save-container-v1".to_string(), + selector_word_8: 0x54cd_0000, + selector_word_8_hex: "0x54cd0000".to_string(), + selector_high_u16: 0x54cd, + selector_high_hex: "0x54cd".to_string(), + descriptor_word_10: 0x5901_0000, + descriptor_word_10_hex: "0x59010000".to_string(), + descriptor_high_u16: 0x5901, + descriptor_high_hex: "0x5901".to_string(), + ..base_trailer.clone() + }; + let alt_post_span = SmpRuntimePostSpanProbe { + profile_family: "rt3-105-alt-save-container-v1".to_string(), + next_aligned_candidate_offset: Some(29892), + header_candidates: vec![SmpRuntimePostSpanHeaderCandidate { + offset: 29892, + words: vec![], + hex_words: vec![], + dense_word_count: 3, + high_u16_words: vec![0x1500, 0x0100, 0x4100, 0x0200], + high_hex_words: vec![], + grounded_alignments: vec![], + }], + ..base_post_span.clone() + }; + let alt_profile = SmpRt3105PackedProfileProbe { + profile_family: "rt3-105-alt-save-container-v1".to_string(), + packed_profile_block: SmpRt3105PackedProfileBlock { + map_path: Some("Spanish Mainline.gmp".to_string()), + display_name: Some("Spanish Mainline".to_string()), + profile_byte_0x82: 0xa3, + profile_byte_0x82_hex: "0xa3".to_string(), + ..base_profile.packed_profile_block.clone() + }, + ..base_profile.clone() + }; + let alt_bridge = parse_rt3_105_post_span_bridge_probe( + Some(&alt_trailer), + Some(&alt_post_span), + Some(&alt_profile), + ) + .expect("alt bridge should parse"); + assert_eq!( + alt_bridge.bridge_family, + "rt3-105-alt-save-post-span-bridge-v1" + ); + assert_eq!( + alt_bridge.next_candidate_delta_from_packed_profile, + Some(260) + ); + + let scenario_trailer = SmpRuntimeTrailerBlock { + profile_family: "rt3-105-scenario-save-container-v1".to_string(), + trailer_family: "unknown".to_string(), + trailer_offset: 864, + length_word_7: 0, + length_word_7_hex: "0x00000000".to_string(), + length_high_u16: 0, + length_high_hex: "0x0000".to_string(), + selector_word_8: 0x0001_86a0, + selector_word_8_hex: "0x000186a0".to_string(), + selector_high_u16: 0x0001, + selector_high_hex: "0x0001".to_string(), + descriptor_word_10: 0x0186_a000, + descriptor_word_10_hex: "0x0186a000".to_string(), + descriptor_high_u16: 0x0186, + descriptor_high_hex: "0x0186".to_string(), + ..base_trailer.clone() + }; + let scenario_post_span = SmpRuntimePostSpanProbe { + profile_family: "rt3-105-scenario-save-container-v1".to_string(), + span_target_offset: 864, + next_aligned_candidate_offset: Some(940), + header_candidates: vec![SmpRuntimePostSpanHeaderCandidate { + offset: 940, + words: vec![], + hex_words: vec![], + dense_word_count: 3, + high_u16_words: vec![0x0186, 0x0006, 0x0006, 0x0001], + high_hex_words: vec![], + grounded_alignments: vec![], + }], + ..base_post_span.clone() + }; + let scenario_profile = SmpRt3105PackedProfileProbe { + profile_family: "rt3-105-scenario-save-container-v1".to_string(), + packed_profile_block: SmpRt3105PackedProfileBlock { + map_path: Some("Southern Pacific.gmp".to_string()), + display_name: Some("Southern Pacific".to_string()), + profile_byte_0x82: 0x90, + profile_byte_0x82_hex: "0x90".to_string(), + ..base_profile.packed_profile_block.clone() + }, + ..base_profile.clone() + }; + let scenario_bridge = parse_rt3_105_post_span_bridge_probe( + Some(&scenario_trailer), + Some(&scenario_post_span), + Some(&scenario_profile), + ) + .expect("scenario bridge should parse"); + assert_eq!( + scenario_bridge.bridge_family, + "rt3-105-scenario-post-span-bridge-v1" + ); + assert_eq!( + scenario_bridge.next_candidate_delta_from_packed_profile, + Some(-28692) + ); +} + +#[test] +fn parses_rt3_105_save_bridge_payload_probe() { + let mut bytes = vec![0u8; 0x7000]; + let primary = 0x4f14usize; + let secondary = 0x671cusize; + let primary_words: [u32; 8] = [ + 0x62000000, 0x00000000, 0xfff70000, 0x55150000, 0x55550000, 0x00000000, 0xfff70000, + 0x54550000, + ]; + for (index, word) in primary_words.iter().enumerate() { + bytes[primary + index * 4..primary + (index + 1) * 4] + .copy_from_slice(&(*word).to_le_bytes()); + } + + let secondary_words: [u32; 8] = [ + 0x00050000, 0x00050005, 0xfff70000, 0x54540000, 0x545400f9, 0x00f900f9, 0x00f94008, + 0x00001555, + ]; + for (index, word) in secondary_words.iter().enumerate() { + bytes[secondary + index * 4..secondary + (index + 1) * 4] + .copy_from_slice(&(*word).to_le_bytes()); + } + + let bridge_probe = SmpRt3105PostSpanBridgeProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(), + bridge_evidence: vec![], + span_target_offset: 0x3678, + next_candidate_offset: Some(primary), + next_candidate_delta_from_span_target: Some(primary - 0x3678), + packed_profile_offset: 0x73c0, + packed_profile_delta_from_span_target: 0x3d48, + next_candidate_delta_from_packed_profile: Some(primary as i64 - 0x73c0), + selector_high_u16: 0x7110, + selector_high_hex: "0x7110".to_string(), + descriptor_high_u16: 0x7801, + descriptor_high_hex: "0x7801".to_string(), + next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515], + next_candidate_high_hex_words: vec![], + }; + + let probe = parse_rt3_105_save_bridge_payload_probe(&bytes, Some(&bridge_probe)) + .expect("save bridge payload probe should parse"); + + assert_eq!(probe.primary_block_offset, primary); + assert_eq!(probe.primary_block_len, 0x20); + assert_eq!(probe.secondary_block_offset, secondary); + assert_eq!(probe.secondary_block_delta_from_primary, 0x1808); + assert_eq!(probe.secondary_block_end_offset, 0x73c0); + assert_eq!(probe.secondary_block_len, 0xca4); + assert_eq!(probe.primary_words[..4], primary_words[..4]); + assert_eq!(probe.secondary_words[..8], secondary_words[..8]); +} + +#[test] +fn parses_rt3_105_save_name_table_probe() { + let mut bytes = vec![0u8; 0x7400]; + let secondary = 0x671cusize; + let header = secondary + 0x354; + let entries = secondary + 0x3b5; + let stride = 0x22usize; + let names = ["AluminumMill", "Nuclear Power Plant", "Bakery"]; + + bytes[header..header + 4].copy_from_slice(&0x10000000u32.to_le_bytes()); + bytes[header + 4..header + 8].copy_from_slice(&0x00009000u32.to_le_bytes()); + bytes[header + 8..header + 12].copy_from_slice(&0x0000332eu32.to_le_bytes()); + bytes[header + 0x1c..header + 0x20].copy_from_slice(&4u32.to_le_bytes()); + bytes[header + 0x20..header + 0x24].copy_from_slice(&(names.len() as u32).to_le_bytes()); + bytes[header + 12..header + 16].copy_from_slice(&1u32.to_le_bytes()); + bytes[header + 16..header + 20].copy_from_slice(&0x22u32.to_le_bytes()); + bytes[header + 20..header + 24].copy_from_slice(&2u32.to_le_bytes()); + bytes[header + 24..header + 28].copy_from_slice(&2u32.to_le_bytes()); + bytes[header + 0x28..header + 0x2c].copy_from_slice(&1u32.to_le_bytes()); + + for (index, name) in names.iter().enumerate() { + let off = entries + index * stride; + let raw = &mut bytes[off..off + stride]; + raw[..name.len()].copy_from_slice(name.as_bytes()); + let trailer = if *name == "Nuclear Power Plant" { + 0u32 + } else { + 1u32 + }; + raw[stride - 4..stride].copy_from_slice(&trailer.to_le_bytes()); + } + let footer = entries + names.len() * stride; + bytes[footer..footer + 4].copy_from_slice(&0x32dcu32.to_le_bytes()); + bytes[footer + 4..footer + 8].copy_from_slice(&0x3714u32.to_le_bytes()); + bytes[footer + 8] = 0x00; + + let payload = SmpRt3105SaveBridgePayloadProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(), + primary_block_offset: 0x4f14, + primary_block_len: 0x20, + primary_block_len_hex: "0x20".to_string(), + primary_words: vec![], + primary_hex_words: vec![], + secondary_block_offset: secondary, + secondary_block_delta_from_primary: 0x1808, + secondary_block_delta_from_primary_hex: "0x1808".to_string(), + secondary_block_end_offset: footer + 9, + secondary_block_len: footer + 9 - secondary, + secondary_block_len_hex: format!("0x{:x}", footer + 9 - secondary), + secondary_preview_word_count: 32, + secondary_words: vec![], + secondary_hex_words: vec![], + evidence: vec![], + }; + + let probe = parse_rt3_105_save_name_table_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&payload), + ) + .expect("save name table probe should parse"); + + assert_eq!(probe.source_kind, "save-bridge-secondary-block"); + assert_eq!( + probe.semantic_family, + "scenario-named-candidate-availability-table" + ); + assert_eq!(probe.header_offset, header); + assert_eq!(probe.entry_stride, stride); + assert_eq!(probe.observed_entry_capacity, 4); + assert_eq!(probe.observed_entry_count, names.len()); + assert_eq!(probe.entries[0].text, "AluminumMill"); + assert_eq!(probe.entries[0].availability_dword, 1); + assert_eq!(probe.entries[2].text, "Bakery"); + assert_eq!(probe.zero_trailer_entry_count, 1); + assert_eq!( + probe.zero_trailer_entry_names, + vec!["Nuclear Power Plant".to_string()] + ); + assert_eq!(probe.trailing_footer_hex, "dc3200001437000000"); + assert_eq!(probe.footer_progress_word_0, 0x32dc); + assert_eq!(probe.footer_progress_word_1, 0x3714); + assert_eq!(probe.footer_trailing_byte, 0x00); +} + +#[test] +fn parses_rt3_105_map_name_table_probe_from_fixed_offsets() { + let mut bytes = vec![0u8; 0x7400]; + let header = 0x6a70usize; + let entries = 0x6ad1usize; + let stride = 0x22usize; + let observed_entry_count = 67usize; + + bytes[header..header + 4].copy_from_slice(&0x00000000u32.to_le_bytes()); + bytes[header + 4..header + 8].copy_from_slice(&0x00000000u32.to_le_bytes()); + bytes[header + 8..header + 12].copy_from_slice(&0x0000332eu32.to_le_bytes()); + bytes[header + 12..header + 16].copy_from_slice(&1u32.to_le_bytes()); + bytes[header + 16..header + 20].copy_from_slice(&0x22u32.to_le_bytes()); + bytes[header + 20..header + 24].copy_from_slice(&2u32.to_le_bytes()); + bytes[header + 24..header + 28].copy_from_slice(&2u32.to_le_bytes()); + bytes[header + 0x1c..header + 0x20].copy_from_slice(&0x44u32.to_le_bytes()); + bytes[header + 0x20..header + 0x24] + .copy_from_slice(&(observed_entry_count as u32).to_le_bytes()); + bytes[header + 0x28..header + 0x2c].copy_from_slice(&1u32.to_le_bytes()); + + for index in 0..observed_entry_count { + let name = match index { + 0 => "AutoPlant".to_string(), + 1 => "Nuclear Power Plant".to_string(), + 66 => "Warehouse11".to_string(), + _ => format!("Entry{index:02}"), + }; + let off = entries + index * stride; + let raw = &mut bytes[off..off + stride]; + raw[..name.len()].copy_from_slice(name.as_bytes()); + let trailer = if name == "Nuclear Power Plant" { + 0u32 + } else { + 1u32 + }; + raw[stride - 4..stride].copy_from_slice(&trailer.to_le_bytes()); + } + + let footer = entries + observed_entry_count * stride; + bytes[footer..footer + 4].copy_from_slice(&0x32dcu32.to_le_bytes()); + bytes[footer + 4..footer + 8].copy_from_slice(&0x3714u32.to_le_bytes()); + bytes[footer + 8] = 0x00; + + let probe = parse_rt3_105_save_name_table_probe( + &bytes, + Some("gmp"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-map-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + None, + ) + .expect("map name table probe should parse"); + + assert_eq!(probe.profile_family, "rt3-105-map-container-v1"); + assert_eq!(probe.source_kind, "map-fixed-catalog-range"); + assert_eq!(probe.header_offset, header); + assert_eq!(probe.entries_offset, entries); + assert_eq!(probe.observed_entry_count, observed_entry_count); + assert_eq!(probe.entries[0].text, "AutoPlant"); + assert_eq!(probe.entries[66].text, "Warehouse11"); + assert_eq!( + probe.zero_trailer_entry_names, + vec!["Nuclear Power Plant".to_string()] + ); + assert_eq!(probe.footer_progress_word_0, 0x32dc); + assert_eq!(probe.footer_progress_word_1, 0x3714); +} + +#[test] +fn parses_map_title_hint_probe_from_grounded_titles_and_embedded_map_reference() { + let mut bytes = vec![0u8; 0x9000]; + let embedded_reference = b"Dutchlantis.gmp"; + let title = b"Dutchlantis"; + let later_title = b"Germany"; + + bytes[0x73d0..0x73d0 + embedded_reference.len()].copy_from_slice(embedded_reference); + bytes[0x73e0..0x73e0 + title.len()].copy_from_slice(title); + bytes[0x8400..0x8400 + later_title.len()].copy_from_slice(later_title); + + let probe = parse_map_title_hint_probe( + &bytes, + Some("gmp"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-map-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("map title hint probe should parse"); + + assert_eq!(probe.source_kind, "grounded-title-string-scan"); + assert_eq!( + probe.profile_family, + Some("rt3-105-map-container-v1".to_string()) + ); + assert_eq!(probe.grounded_title_hits.len(), 2); + assert_eq!(probe.grounded_title_hits[0].title, "Germany"); + assert_eq!(probe.grounded_title_hits[0].earliest_offset, 0x8400); + assert_eq!(probe.grounded_title_hits[1].title, "Dutchlantis"); + assert_eq!(probe.grounded_title_hits[1].earliest_offset, 0x73d0); + assert_eq!(probe.embedded_map_references.len(), 1); + assert_eq!(probe.embedded_map_references[0].offset, 0x73d0); + assert_eq!(probe.embedded_map_references[0].text, "Dutchlantis.gmp"); + assert_eq!(probe.adjacent_reference_title_pairs.len(), 1); + assert_eq!( + probe + .strongest_same_stem_pair + .as_ref() + .map(|pair| pair.title.as_str()), + Some("Dutchlantis") + ); + let pair = probe.strongest_same_stem_pair.expect("same-stem pair"); + assert_eq!(pair.map_reference_offset, 0x73d0); + assert_eq!(pair.title_offset, 0x73d0); + assert!(pair.normalized_stem_match); + assert_eq!(pair.byte_distance, 0); +} + +#[test] +fn parses_rt3_105_save_named_locomotive_availability_probe() { + let mut bytes = vec![0u8; 0x9000]; + let packed_profile_offset = 0x73c0usize; + let packed_profile_len = 0x108usize; + let entries_offset = 0x7c78usize; + let names = [ + ("Eight Wheeler 4-4-0", 1u32), + ("EP-2 Bipolar", 1u32), + ("ET22", 1u32), + ("F3", 0u32), + ("Fairlie 0-6-6-0", 1u32), + ("Firefly 2-2-2", 0u32), + ("FP45", 0u32), + ("Ge 6/6 Crocodile", 1u32), + ("GG1", 0u32), + ("GP7", 1u32), + ]; + + for (index, (name, value)) in names.iter().enumerate() { + let offset = entries_offset + index * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE; + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + bytes[offset + 4..offset + 4 + name.len()].copy_from_slice(name.as_bytes()); + } + + let probe = parse_rt3_105_save_named_locomotive_availability_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&SmpRt3105PackedProfileProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + packed_profile_offset, + packed_profile_len, + packed_profile_len_hex: "0x108".to_string(), + packed_profile_block: SmpRt3105PackedProfileBlock { + relative_len: packed_profile_len, + 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: 1, + header_flag_word_3_hex: "0x00000001".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: 0, + profile_byte_0x97_hex: "0x00".to_string(), + profile_byte_0xc5: 0, + profile_byte_0xc5_hex: "0x00".to_string(), + stable_nonzero_words: vec![], + }, + ascii_runs: vec![], + }), + ) + .expect("save-side locomotive table probe should parse"); + + assert_eq!(probe.source_kind, "save-direct-locomotive-row-run"); + assert_eq!( + probe.semantic_family, + "scenario-named-locomotive-availability-table" + ); + assert_eq!(probe.entries_offset, entries_offset); + assert_eq!( + probe.entry_stride, + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE + ); + assert_eq!(probe.observed_entry_count, names.len()); + assert_eq!(probe.zero_availability_count, 4); + assert_eq!(probe.entries[0].text, "Eight Wheeler 4-4-0"); + assert_eq!(probe.entries[9].text, "GP7"); +} + +#[test] +fn classifies_rt3_105_alt_save_container_profile() { + let shared_header = SmpSharedHeader { + byte_len: 64, + root_kind_word: 0x000025e5, + root_kind_word_hex: "0x000025e5".to_string(), + primary_family_tag: 0x00002ee0, + primary_family_tag_hex: "0x00002ee0".to_string(), + shared_signature_words_1_to_7: vec![ + 0x00002ee0, 0x0001c001, 0x00018000, 0x00010000, 0x00000754, 0x00000754, 0x00000754, + ], + shared_signature_hex_words_1_to_7: vec![ + "0x00002ee0".to_string(), + "0x0001c001".to_string(), + "0x00018000".to_string(), + "0x00010000".to_string(), + "0x00000754".to_string(), + "0x00000754".to_string(), + "0x00000754".to_string(), + ], + matches_grounded_common_signature: false, + payload_window_words_8_to_9: vec![0x007a5978, 0x007a9022], + payload_window_hex_words_8_to_9: vec!["0x007a5978".to_string(), "0x007a9022".to_string()], + reserved_words_10_to_14: vec![0; 5], + reserved_words_10_to_14_all_zero: true, + final_flag_word: 0, + final_flag_word_hex: "0x00000000".to_string(), + }; + let early_content_probe = SmpEarlyContentProbe { + first_post_text_nonzero_offset: 722, + zero_pad_after_text_len: 431, + first_post_text_block_len: 35, + first_post_text_block_hex: + "0101010000010000000000000100000000000000010000000000000000010100000001".to_string(), + trailing_zero_pad_after_first_block_len: 45, + secondary_nonzero_offset: Some(802), + secondary_aligned_word_window_offset: Some(800), + secondary_aligned_word_window_words: vec![ + 0x00010000, 0x49f00100, 0x00000002, 0xa0000000, 0x00000186, 0x00000000, 0x000186a0, + 0x00000000, + ], + secondary_aligned_word_window_hex_words: vec![ + "0x00010000".to_string(), + "0x49f00100".to_string(), + "0x00000002".to_string(), + "0xa0000000".to_string(), + "0x00000186".to_string(), + "0x00000000".to_string(), + "0x000186a0".to_string(), + "0x00000000".to_string(), + ], + secondary_preview_hex: "01000001f04902000000000000a08601000000000000a08601000000000000a0" + .to_string(), + }; + + let header_variant = classify_header_variant_probe(&shared_header); + let secondary_variant = + classify_secondary_variant_probe(&early_content_probe).expect("secondary probe"); + let container_profile = + classify_container_profile(Some("gms"), Some(&header_variant), Some(&secondary_variant)) + .expect("container profile"); + + assert_eq!(header_variant.variant_family, "rt3-105-alt-save-header-v1"); + assert_eq!( + secondary_variant.variant_family, + "rt3-105-gms-alt-family-v1" + ); + assert_eq!( + container_profile.profile_family, + "rt3-105-alt-save-container-v1" + ); + assert!(container_profile.is_known_profile); +} + +#[test] +fn classifies_rt3_105_map_container_profiles_from_header_families() { + let scenario_profile = classify_container_profile( + Some("gmp"), + Some(&SmpHeaderVariantProbe { + variant_family: "rt3-105-scenario-save-header-v1".to_string(), + variant_evidence: vec![], + is_known_family: true, + }), + Some(&SmpSecondaryVariantProbe { + aligned_window_offset: 0, + words: vec![1, 0, 0, 0], + hex_words: vec![], + variant_family: "unknown".to_string(), + variant_evidence: vec![], + }), + ) + .expect("scenario map profile"); + + let alt_profile = classify_container_profile( + Some("gmp"), + Some(&SmpHeaderVariantProbe { + variant_family: "rt3-105-alt-save-header-v1".to_string(), + variant_evidence: vec![], + is_known_family: true, + }), + Some(&SmpSecondaryVariantProbe { + aligned_window_offset: 0, + words: vec![0x49f00100, 2, 0xa0000000, 0x186], + hex_words: vec![], + variant_family: "unknown".to_string(), + variant_evidence: vec![], + }), + ) + .expect("alt map profile"); + + assert_eq!( + scenario_profile.profile_family, + "rt3-105-scenario-map-container-v1" + ); + assert!(scenario_profile.is_known_profile); + assert_eq!(alt_profile.profile_family, "rt3-105-alt-map-container-v1"); + assert!(alt_profile.is_known_profile); + + let generic_map_profile = classify_container_profile( + Some("gmp"), + Some(&SmpHeaderVariantProbe { + variant_family: "rt3-map-header-family".to_string(), + variant_evidence: vec![], + is_known_family: true, + }), + Some(&SmpSecondaryVariantProbe { + aligned_window_offset: 0, + words: vec![0x00140000, 0x93e00100, 0x00000004, 0xa0000000], + hex_words: vec![], + variant_family: "rt3-map-secondary-family-v1".to_string(), + variant_evidence: vec![], + }), + ) + .expect("generic map profile"); + + assert_eq!( + generic_map_profile.profile_family, + "rt3-map-container-family" + ); + assert!(generic_map_profile.is_known_profile); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/regions.rs b/crates/rrt-runtime/src/inspect/smp/tests/regions.rs new file mode 100644 index 0000000..de91379 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/regions.rs @@ -0,0 +1,739 @@ +use super::*; + +#[test] +fn parses_company_tagged_collection_header_probe_from_exact_u32_tags() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x180usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x000061a9u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x000061aau32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000061abu32.to_le_bytes()); + let header_words = [ + 1u32, 0x7684, 5, 5, 5, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + + let probe = parse_save_company_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("company header probe should parse"); + + assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); + assert_eq!(probe.records_tag_offset, records_tag_offset); + assert_eq!(probe.close_tag_offset, close_tag_offset); + assert_eq!(probe.direct_record_stride, 0x7684); + assert_eq!(probe.live_id_bound, 5); + assert_eq!(probe.live_record_count, 1); +} + +#[test] +fn parses_chairman_profile_tagged_collection_header_probe_from_exact_u32_tags() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x180usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00005209u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x0000520au32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); + let header_words = [ + 1u32, 0xcab, 8, 6, 8, 2, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + + let probe = parse_save_chairman_profile_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("chairman profile header probe should parse"); + + assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); + assert_eq!(probe.records_tag_offset, records_tag_offset); + assert_eq!(probe.close_tag_offset, close_tag_offset); + assert_eq!(probe.direct_record_stride, 0xcab); + assert_eq!(probe.live_id_bound, 8); + assert_eq!(probe.live_record_count, 2); +} + +#[test] +fn parses_train_tagged_collection_header_probe_from_exact_u32_tags() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x180usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00005209u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x0000520au32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); + let header_words = [ + 1u32, 0x1d5, 0x32, 0x14, 0x32, 0x14, 0x14, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + + let probe = parse_save_train_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("train header probe should parse"); + + assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); + assert_eq!(probe.records_tag_offset, records_tag_offset); + assert_eq!(probe.close_tag_offset, close_tag_offset); + assert_eq!(probe.direct_collection_flag, 1); + assert_eq!(probe.direct_record_stride, 0x1d5); + assert_eq!(probe.live_id_bound, 0x32); + assert_eq!(probe.live_record_count, 0x14); +} + +#[test] +fn parses_region_tagged_collection_header_probe_from_marker09_family() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x180usize; + let close_tag_offset = 0x1c0usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00005209u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x0000520au32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); + let header_words = [ + 0u32, 0x06, 0x0a, 0x14, 0x96, 0x91, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + let marker_offset = records_tag_offset + 4 + 0x20; + bytes[marker_offset..marker_offset + 8].copy_from_slice(b"Marker09"); + + let probe = parse_save_region_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("region header probe should parse"); + + assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); + assert_eq!(probe.records_tag_offset, records_tag_offset); + assert_eq!(probe.close_tag_offset, close_tag_offset); + assert_eq!(probe.direct_collection_flag, 0); + assert_eq!(probe.direct_record_stride, 0x06); + assert_eq!(probe.live_id_bound, 0x96); + assert_eq!(probe.live_record_count, 0x91); +} + +#[test] +fn parses_train_collection_directory_probe_from_tagged_metadata_triplets() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x180usize; + let close_tag_offset = 0x1c0usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00005209u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x0000520au32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); + let header_words = [ + 1u32, 0x1d5, 0x32, 0x03, 0x32, 0x03, 0x14, 1, 0, 0, 1, 1, 0x14, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + let triplets = [(0x2af8u32, 0u32, 2u32), (0x2ee0, 1, 3), (0x32c8, 2, 0)]; + for (index, (offset_word, prev, next)) in triplets.into_iter().enumerate() { + let base = metadata_tag_offset + + 4 + + (SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX + index * 3) * 4; + bytes[base..base + 4].copy_from_slice(&offset_word.to_le_bytes()); + bytes[base + 4..base + 8].copy_from_slice(&prev.to_le_bytes()); + bytes[base + 8..base + 12].copy_from_slice(&next.to_le_bytes()); + } + + let header_probe = parse_save_train_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("train header probe should parse"); + let directory_probe = parse_save_train_collection_directory_probe(&bytes, Some(&header_probe)) + .expect("train directory probe should parse"); + + assert_eq!(directory_probe.directory_root_dword_index, 16); + assert_eq!(directory_probe.live_record_count, 3); + assert_eq!(directory_probe.chain_head_live_entry_id, Some(1)); + assert_eq!(directory_probe.chain_tail_live_entry_id, Some(3)); + assert_eq!(directory_probe.entries.len(), 3); + assert_eq!(directory_probe.entries[0].live_entry_id, 1); + assert_eq!(directory_probe.entries[0].payload_relative_offset, 0x2af8); + assert_eq!(directory_probe.entries[0].previous_live_entry_id, 0); + assert_eq!(directory_probe.entries[0].next_live_entry_id, 2); + assert_eq!(directory_probe.entries[2].live_entry_id, 3); + assert_eq!(directory_probe.entries[2].previous_live_entry_id, 2); + assert_eq!(directory_probe.entries[2].next_live_entry_id, 0); +} + +#[test] +fn parses_region_record_triplet_probe_from_marker09_records() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x260usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00005209u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x0000520au32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); + let mut cursor = records_tag_offset + 4; + for (name, x, density, y) in [ + ("Marker09", 368.0f32, 0.0f32, 92.0f32), + ("Marker10", 552.0f32, 1.5f32, 276.0f32), + ] { + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); + bytes[cursor + 4] = name.len() as u8; + bytes[cursor + 5..cursor + 5 + name.len()].copy_from_slice(name.as_bytes()); + cursor += 0x10; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); + bytes[cursor + 4..cursor + 8].copy_from_slice(&x.to_bits().to_le_bytes()); + bytes[cursor + 8..cursor + 12].copy_from_slice(&density.to_bits().to_le_bytes()); + bytes[cursor + 12..cursor + 16].copy_from_slice(&y.to_bits().to_le_bytes()); + bytes[cursor + 28..cursor + 30].copy_from_slice(&1u16.to_le_bytes()); + cursor += 0x1e; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); + cursor += 0x40; + } + + let header_probe = SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-region-tagged-header-counts".to_string(), + semantic_family: "scenario-save-region-header-counts".to_string(), + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + direct_collection_flag: 0, + direct_collection_flag_hex: "0x00000000".to_string(), + direct_record_stride: 0x06, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 0x96, + live_id_bound_hex: "0x00000096".to_string(), + live_record_count: 2, + live_record_count_hex: "0x00000002".to_string(), + header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x02], + header_hex_words: vec![], + evidence: vec![], + }; + let triplet_probe = parse_save_region_record_triplet_probe(&bytes, Some(&header_probe)) + .expect("region triplet probe should parse"); + + assert_eq!(triplet_probe.record_count, 2); + assert_eq!(triplet_probe.entries[0].name, "Marker09"); + assert_eq!(triplet_probe.entries[0].policy_tag_relative_offset, 0x10); + assert_eq!(triplet_probe.entries[0].profile_tag_relative_offset, 0x2e); + assert_eq!(triplet_probe.entries[0].policy_leading_f32_0, 368.0); + assert_eq!(triplet_probe.entries[0].policy_leading_f32_1, 0.0); + assert_eq!(triplet_probe.entries[0].policy_leading_f32_2, 92.0); + assert!( + triplet_probe.entries[0] + .pre_name_prefix_dword_candidates + .is_empty() + ); + assert_eq!( + triplet_probe.entries[0].policy_reserved_dwords, + vec![0, 0, 0] + ); + assert_eq!( + triplet_probe.entries[0] + .policy_reserved_dword_candidates + .len(), + 3 + ); + assert_eq!( + triplet_probe.entries[0].policy_reserved_dword_candidates[0].relative_offset_hex, + "0x20" + ); + assert_eq!(triplet_probe.entries[0].policy_trailing_word, 1); + assert_eq!(triplet_probe.entries[1].name, "Marker10"); + assert_eq!(triplet_probe.entries[1].policy_leading_f32_0, 552.0); + assert_eq!(triplet_probe.entries[1].policy_leading_f32_1, 1.5); + assert_eq!(triplet_probe.entries[1].policy_leading_f32_2, 276.0); + assert!( + triplet_probe.entries[1] + .pre_name_prefix_dword_candidates + .is_empty() + ); + assert_eq!( + triplet_probe.entries[1] + .policy_reserved_dword_candidates + .len(), + 3 + ); +} + +#[test] +fn parses_region_record_triplet_prefix_dword_candidates() { + let mut bytes = vec![0u8; 0x320]; + let metadata_tag_offset = 0x0usize; + let records_tag_offset = 0x100usize; + let close_tag_offset = 0x200usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00005209u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x0000520au32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); + let mut cursor = records_tag_offset + 4; + let first_record_relative_offset = 0usize; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); + bytes[cursor + 4] = 8; + bytes[cursor + 5..cursor + 13].copy_from_slice(b"Marker11"); + cursor += 0x10; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); + bytes[cursor + 4..cursor + 8].copy_from_slice(&100.0f32.to_bits().to_le_bytes()); + bytes[cursor + 8..cursor + 12].copy_from_slice(&2.0f32.to_bits().to_le_bytes()); + bytes[cursor + 12..cursor + 16].copy_from_slice(&50.0f32.to_bits().to_le_bytes()); + bytes[cursor + 28..cursor + 30].copy_from_slice(&1u16.to_le_bytes()); + cursor += 0x1e; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); + cursor += 0x20; + let second_record_relative_offset = cursor - (records_tag_offset + 4); + bytes[cursor..cursor + 4].copy_from_slice(&0x11223344u32.to_le_bytes()); + bytes[cursor + 4..cursor + 8].copy_from_slice(&0x55667788u32.to_le_bytes()); + cursor += 8; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); + bytes[cursor + 4] = 8; + bytes[cursor + 5..cursor + 13].copy_from_slice(b"Marker12"); + cursor += 0x10; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); + bytes[cursor + 4..cursor + 8].copy_from_slice(&120.0f32.to_bits().to_le_bytes()); + bytes[cursor + 8..cursor + 12].copy_from_slice(&3.0f32.to_bits().to_le_bytes()); + bytes[cursor + 12..cursor + 16].copy_from_slice(&60.0f32.to_bits().to_le_bytes()); + bytes[cursor + 16..cursor + 20].copy_from_slice(&0x01020304u32.to_le_bytes()); + bytes[cursor + 20..cursor + 24].copy_from_slice(&0u32.to_le_bytes()); + bytes[cursor + 24..cursor + 28].copy_from_slice(&0x05060708u32.to_le_bytes()); + bytes[cursor + 28..cursor + 30].copy_from_slice(&1u16.to_le_bytes()); + cursor += 0x1e; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); + let directory_root_byte_offset = SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX * 4; + let first_payload_relative_offset = records_tag_offset - metadata_tag_offset; + let second_payload_relative_offset = + first_payload_relative_offset + second_record_relative_offset; + bytes[metadata_tag_offset + 4 + directory_root_byte_offset + ..metadata_tag_offset + 8 + directory_root_byte_offset] + .copy_from_slice(&(first_payload_relative_offset as u32).to_le_bytes()); + bytes[metadata_tag_offset + 16 + directory_root_byte_offset + ..metadata_tag_offset + 20 + directory_root_byte_offset] + .copy_from_slice(&(second_payload_relative_offset as u32).to_le_bytes()); + + let header_probe = SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-region-tagged-header-counts".to_string(), + semantic_family: "scenario-save-region-header-counts".to_string(), + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + direct_collection_flag: 0, + direct_collection_flag_hex: "0x00000000".to_string(), + direct_record_stride: 0x06, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 0x96, + live_id_bound_hex: "0x00000096".to_string(), + live_record_count: 2, + live_record_count_hex: "0x00000002".to_string(), + header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x02], + header_hex_words: vec![], + evidence: vec![], + }; + let triplet_probe = parse_save_region_record_triplet_probe(&bytes, Some(&header_probe)) + .expect("region triplet probe should parse"); + + assert_eq!(triplet_probe.entries.len(), 2); + assert_eq!( + triplet_probe.entries[0].record_payload_relative_offset, + first_record_relative_offset + ); + assert_eq!(triplet_probe.entries[0].pre_name_prefix_len, 0); + assert_eq!( + triplet_probe.entries[1].record_payload_relative_offset, + second_record_relative_offset + ); + assert_eq!(triplet_probe.entries[1].pre_name_prefix_len, 8); + assert_eq!( + triplet_probe.entries[1] + .pre_name_prefix_dword_candidates + .len(), + 2 + ); + assert_eq!( + triplet_probe.entries[1].pre_name_prefix_dword_candidates[0].raw_u32_hex, + "0x11223344" + ); + assert_eq!( + triplet_probe.entries[1].pre_name_prefix_dword_candidates[1].relative_offset_hex, + "0x52" + ); + assert_eq!( + triplet_probe.entries[1].policy_reserved_dword_candidates[2].relative_offset_hex, + "0xcc" + ); + assert_eq!( + triplet_probe.entries[1].policy_reserved_dword_candidates[0].raw_u32_hex, + "0x01020304" + ); + assert_eq!( + triplet_probe.entries[1].policy_reserved_dword_candidates[2].raw_u32_hex, + "0x05060708" + ); + assert!(triplet_probe.evidence.iter().any(|line| line.contains( + "fixed 0x55f2 policy reserved dwords are nonzero on 1 of 2 decoded region records" + ))); +} + +#[test] +fn parses_region_profile_collection_probe_from_fixed_name_rows() { + let mut payload = vec![0u8; 0x80]; + let header_words = [1u32, 0x22, 2, 2, 3, 2, 0, 1]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = index * 4; + payload[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + let first_row_offset = 0x20usize; + let first_name = b"House"; + payload[first_row_offset..first_row_offset + first_name.len()].copy_from_slice(first_name); + payload[first_row_offset + 0x1e..first_row_offset + 0x22] + .copy_from_slice(&0.2f32.to_bits().to_le_bytes()); + let second_row_offset = first_row_offset + 0x22; + let second_name = b"Farm Corn"; + payload[second_row_offset..second_row_offset + second_name.len()].copy_from_slice(second_name); + payload[second_row_offset + 0x1e..second_row_offset + 0x22] + .copy_from_slice(&0.45f32.to_bits().to_le_bytes()); + + let profile_probe = parse_save_region_profile_collection_probe(&payload) + .expect("profile collection probe should parse"); + + assert_eq!(profile_probe.direct_collection_flag, 1); + assert_eq!(profile_probe.entry_stride, 0x22); + assert_eq!(profile_probe.live_id_bound, 3); + assert_eq!(profile_probe.live_record_count, 2); + assert_eq!(profile_probe.entry_start_relative_offset, 0x20); + assert_eq!(profile_probe.entries.len(), 2); + assert_eq!(profile_probe.entries[0].name, "House"); + assert_eq!(profile_probe.entries[0].trailing_weight_f32, 0.2); + assert_eq!(profile_probe.entries[1].name, "Farm Corn"); + assert_eq!(profile_probe.entries[1].trailing_weight_f32, 0.45); +} + +#[test] +fn parses_region_queued_notice_record_probe_from_seeded_node() { + let mut bytes = vec![0u8; 0x200]; + let node_base_offset = 0x80usize; + bytes[node_base_offset + 4..node_base_offset + 8] + .copy_from_slice(&SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED.to_le_bytes()); + bytes[node_base_offset + 8..node_base_offset + 12] + .copy_from_slice(&SAVE_REGION_QUEUED_NOTICE_NODE_KIND.to_le_bytes()); + bytes[node_base_offset + 12..node_base_offset + 16].copy_from_slice(&0u32.to_le_bytes()); + bytes[node_base_offset + 16..node_base_offset + 20].copy_from_slice(&5u32.to_le_bytes()); + bytes[node_base_offset + 20..node_base_offset + 24].copy_from_slice(&1200u32.to_le_bytes()); + bytes[node_base_offset + 24..node_base_offset + 28].copy_from_slice(&(-1i32).to_le_bytes()); + bytes[node_base_offset + 28..node_base_offset + 32].copy_from_slice(&(-1i32).to_le_bytes()); + + let probe = parse_save_region_queued_notice_record_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-region-tagged-header-counts".to_string(), + semantic_family: "scenario-save-region-header-counts".to_string(), + metadata_tag_offset: 0, + records_tag_offset: 0, + close_tag_offset: 0, + direct_collection_flag: 0, + direct_collection_flag_hex: "0x00000000".to_string(), + direct_record_stride: 0x06, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 0x96, + live_id_bound_hex: "0x00000096".to_string(), + live_record_count: 0x91, + live_record_count_hex: "0x00000091".to_string(), + header_words: vec![], + header_hex_words: vec![], + evidence: vec![], + }), + ) + .expect("region queued notice record probe should parse"); + + assert_eq!(probe.entries.len(), 1); + assert_eq!(probe.entries[0].node_base_offset, node_base_offset); + assert_eq!(probe.entries[0].payload_seed_dword_hex, "0x005c87a8"); + assert_eq!(probe.entries[0].kind, SAVE_REGION_QUEUED_NOTICE_NODE_KIND); + assert_eq!(probe.entries[0].region_id, 5); + assert_eq!(probe.entries[0].amount, 1200); + assert_eq!(probe.entries[0].trailing_sentinel_i32_0, -1); + assert_eq!(probe.entries[0].trailing_sentinel_i32_1, -1); +} + +#[test] +fn parses_region_fixed_row_run_candidate_probe_from_seeded_rows() { + let mut bytes = vec![0u8; 0x200]; + let count_offset = 0x20usize; + let rows_offset = count_offset + 4; + let metadata_tag_offset = 0x120usize; + bytes[count_offset..count_offset + 4].copy_from_slice(&2u32.to_le_bytes()); + + let first_row = rows_offset; + bytes[first_row..first_row + 4].copy_from_slice(&11u32.to_le_bytes()); + bytes[first_row + 4..first_row + 8].copy_from_slice(&0.25f32.to_bits().to_le_bytes()); + bytes[first_row + 8..first_row + 12].copy_from_slice(&0x11223344u32.to_le_bytes()); + bytes[first_row + 0x28] = 0x07; + + let second_row = rows_offset + SAVE_REGION_FIXED_ROW_STRIDE; + bytes[second_row..second_row + 4].copy_from_slice(&12u32.to_le_bytes()); + bytes[second_row + 4..second_row + 8].copy_from_slice(&0.5f32.to_bits().to_le_bytes()); + bytes[second_row + 8..second_row + 12].copy_from_slice(&0x55667788u32.to_le_bytes()); + bytes[second_row + 0x28] = 0x08; + + let probe = parse_save_region_fixed_row_run_candidate_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-region-tagged-header-counts".to_string(), + semantic_family: "scenario-save-region-header-counts".to_string(), + metadata_tag_offset, + records_tag_offset: 0, + close_tag_offset: 0, + direct_collection_flag: 0, + direct_collection_flag_hex: "0x00000000".to_string(), + direct_record_stride: 0x06, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 0x96, + live_id_bound_hex: "0x00000096".to_string(), + live_record_count: 2, + live_record_count_hex: "0x00000002".to_string(), + header_words: vec![], + header_hex_words: vec![], + evidence: vec![], + }), + ) + .expect("region fixed-row run candidate probe should parse"); + + assert_eq!(probe.target_row_count, 2); + assert_eq!(probe.target_row_stride, SAVE_REGION_FIXED_ROW_STRIDE); + assert_eq!(probe.candidates.len(), 1); + assert_eq!(probe.candidates[0].count_offset, count_offset); + assert_eq!(probe.candidates[0].rows_offset, rows_offset); + assert_eq!( + probe.candidates[0].best_probable_density_lane_relative_offset_hex, + Some("0x4".to_string()) + ); + assert_eq!( + probe.candidates[0].dword_lane_summaries[0].small_unsigned_count, + 2 + ); + assert_eq!( + probe.candidates[0].dword_lane_summaries[1].probable_normal_f32_count, + 2 + ); + assert_eq!(probe.candidates[0].trailing_byte_nonzero_count, 2); + assert_eq!( + probe.candidates[0].trailing_byte_sample_values_hex, + vec!["0x07".to_string(), "0x08".to_string()] + ); + assert_eq!( + probe.candidates[0].shape_signature, + "pf32=[0x4:2,0x8:2]|small=[0x0:2]|zero=[]|trail=0/2" + ); + assert_eq!( + probe.candidates[0].shape_family_signature, + "dense_pf32=[0x4,0x8]|small_nonzero=[0x0,0x4,0x8]|partial_zero=[]|trail_bucket=0/0" + ); +} + +#[test] +fn compares_region_fixed_row_run_candidates_by_shape_signature() { + let mut left = empty_analysis_report(); + left.region_fixed_row_run_candidates = Some(SmpSaveRegionFixedRowRunCandidateProbe { + profile_family: left.profile_family.clone(), + source_kind: "save-region-fixed-row-run-candidates".to_string(), + semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), + target_row_count: 145, + target_row_stride: 0x29, + target_row_stride_hex: "0x29".to_string(), + scan_start_offset: 0, + scan_start_offset_hex: "0x0".to_string(), + scan_end_offset: 0x100, + scan_end_offset_hex: "0x100".to_string(), + candidates: vec![ + SmpSaveRegionFixedRowRunCandidate { + count_offset: 0x20, + count_offset_hex: "0x20".to_string(), + row_count: 145, + row_stride: 0x29, + row_stride_hex: "0x29".to_string(), + rows_offset: 0x24, + rows_offset_hex: "0x24".to_string(), + rows_end_offset: 0x39, + rows_end_offset_hex: "0x39".to_string(), + distance_to_region_metadata_tag: 0xc7, + distance_to_region_metadata_tag_hex: "0xc7".to_string(), + dword_lane_summaries: Vec::new(), + shape_signature: "pf32=[0x14:120]|small=[0x20:17]|zero=[0x20:11]|trail=28/63" + .to_string(), + shape_family_signature: + "dense_pf32=[0x14]|small_nonzero=[0x20]|partial_zero=[0x20]|trail_bucket=3/7" + .to_string(), + trailing_byte_zero_count: 28, + trailing_byte_nonzero_count: 117, + trailing_byte_distinct_value_count: 63, + trailing_byte_sample_values_hex: Vec::new(), + best_probable_density_lane_relative_offset_hex: Some("0x14".to_string()), + }, + SmpSaveRegionFixedRowRunCandidate { + count_offset: 0x40, + count_offset_hex: "0x40".to_string(), + row_count: 145, + row_stride: 0x29, + row_stride_hex: "0x29".to_string(), + rows_offset: 0x44, + rows_offset_hex: "0x44".to_string(), + rows_end_offset: 0x59, + rows_end_offset_hex: "0x59".to_string(), + distance_to_region_metadata_tag: 0xa7, + distance_to_region_metadata_tag_hex: "0xa7".to_string(), + dword_lane_summaries: Vec::new(), + shape_signature: "pf32=[0x18:107]|small=[0x10:17]|zero=[0x14:12]|trail=32/58" + .to_string(), + shape_family_signature: + "dense_pf32=[]|small_nonzero=[0x10]|partial_zero=[0x14]|trail_bucket=4/7" + .to_string(), + trailing_byte_zero_count: 32, + trailing_byte_nonzero_count: 113, + trailing_byte_distinct_value_count: 58, + trailing_byte_sample_values_hex: Vec::new(), + best_probable_density_lane_relative_offset_hex: Some("0x18".to_string()), + }, + ], + evidence: Vec::new(), + }); + + let mut right = empty_analysis_report(); + right.region_fixed_row_run_candidates = Some(SmpSaveRegionFixedRowRunCandidateProbe { + profile_family: right.profile_family.clone(), + source_kind: "save-region-fixed-row-run-candidates".to_string(), + semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), + target_row_count: 145, + target_row_stride: 0x29, + target_row_stride_hex: "0x29".to_string(), + scan_start_offset: 0, + scan_start_offset_hex: "0x0".to_string(), + scan_end_offset: 0x100, + scan_end_offset_hex: "0x100".to_string(), + candidates: vec![ + SmpSaveRegionFixedRowRunCandidate { + count_offset: 0x80, + count_offset_hex: "0x80".to_string(), + row_count: 145, + row_stride: 0x29, + row_stride_hex: "0x29".to_string(), + rows_offset: 0x84, + rows_offset_hex: "0x84".to_string(), + rows_end_offset: 0x99, + rows_end_offset_hex: "0x99".to_string(), + distance_to_region_metadata_tag: 0x67, + distance_to_region_metadata_tag_hex: "0x67".to_string(), + dword_lane_summaries: Vec::new(), + shape_signature: "pf32=[0x18:107]|small=[0x10:17]|zero=[0x14:12]|trail=32/58" + .to_string(), + shape_family_signature: + "dense_pf32=[]|small_nonzero=[0x10]|partial_zero=[0x14]|trail_bucket=4/7" + .to_string(), + trailing_byte_zero_count: 32, + trailing_byte_nonzero_count: 113, + trailing_byte_distinct_value_count: 58, + trailing_byte_sample_values_hex: Vec::new(), + best_probable_density_lane_relative_offset_hex: Some("0x18".to_string()), + }, + SmpSaveRegionFixedRowRunCandidate { + count_offset: 0xa0, + count_offset_hex: "0xa0".to_string(), + row_count: 145, + row_stride: 0x29, + row_stride_hex: "0x29".to_string(), + rows_offset: 0xa4, + rows_offset_hex: "0xa4".to_string(), + rows_end_offset: 0xb9, + rows_end_offset_hex: "0xb9".to_string(), + distance_to_region_metadata_tag: 0x47, + distance_to_region_metadata_tag_hex: "0x47".to_string(), + dword_lane_summaries: Vec::new(), + shape_signature: "pf32=[0x24:100]|small=[0xc:16]|zero=[0xc:11]|trail=34/60" + .to_string(), + shape_family_signature: + "dense_pf32=[]|small_nonzero=[0xc]|partial_zero=[0xc]|trail_bucket=4/7" + .to_string(), + trailing_byte_zero_count: 34, + trailing_byte_nonzero_count: 111, + trailing_byte_distinct_value_count: 60, + trailing_byte_sample_values_hex: Vec::new(), + best_probable_density_lane_relative_offset_hex: Some("0x24".to_string()), + }, + ], + evidence: Vec::new(), + }); + + let comparison = compare_save_region_fixed_row_run_candidates(&left, &right) + .expect("comparison should build"); + assert_eq!(comparison.shared_shape_matches.len(), 1); + assert_eq!( + comparison.shared_shape_matches[0].shape_signature, + "pf32=[0x18:107]|small=[0x10:17]|zero=[0x14:12]|trail=32/58" + ); + assert_eq!(comparison.shared_shape_matches[0].left_rank, 2); + assert_eq!(comparison.shared_shape_matches[0].right_rank, 1); + assert_eq!(comparison.shared_shape_family_matches.len(), 1); + assert_eq!( + comparison.shared_shape_family_matches[0].shape_signature, + "dense_pf32=[]|small_nonzero=[0x10]|partial_zero=[0x14]|trail_bucket=4/7" + ); + assert_eq!(comparison.left_only_shape_signatures.len(), 1); + assert_eq!(comparison.right_only_shape_signatures.len(), 1); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/save_load.rs b/crates/rrt-runtime/src/inspect/smp/tests/save_load.rs new file mode 100644 index 0000000..a3ea373 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/save_load.rs @@ -0,0 +1,1288 @@ +use super::*; + +#[test] +fn reports_grounded_tag_hits_and_offsets() { + let bytes = [ + 0x34, 0x12, 0x00, 0x00, 0xe0, 0x2e, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x80, 0x02, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x71, 0x07, 0x00, 0x00, 0x71, 0x07, 0x00, 0x00, 0x71, 0x07, + 0x00, 0x00, 0xaa, 0xbb, 0x00, 0x00, 0xcc, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, b'H', b'e', b'l', b'l', b'o', b' ', b'R', b'R', + b'T', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, + 0x33, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x00, 0x00, 0x00, 0x00, + 0xee, 0x2c, 0x11, 0x51, 0x2d, 0x22, 0x71, 0x94, 0x33, 0x72, 0x94, + ]; + let report = inspect_smp_bytes(&bytes); + + assert!(report.contains_grounded_runtime_tags); + assert_eq!(report.known_tag_hits.len(), 4); + assert_eq!(report.preamble.word_count, 16); + assert_eq!(report.preamble.words[0].value_le, 0x00001234); + let shared_header = report + .shared_header + .as_ref() + .expect("shared header should parse"); + assert!(shared_header.matches_grounded_common_signature); + let header_variant = report + .header_variant_probe + .as_ref() + .expect("header variant probe should exist"); + assert_eq!(header_variant.variant_family, "unknown"); + assert!(!header_variant.is_known_family); + assert_eq!(shared_header.primary_family_tag, 0x00002ee0); + assert_eq!( + shared_header.payload_window_words_8_to_9, + vec![0x0000bbaa, 0x0000ddcc] + ); + assert!(shared_header.reserved_words_10_to_14_all_zero); + assert_eq!(shared_header.final_flag_word, 0); + let ascii_run = report + .first_ascii_run + .as_ref() + .expect("ascii run should exist"); + assert_eq!(ascii_run.offset, 67); + assert_eq!(ascii_run.byte_len, 9); + assert_eq!(ascii_run.preview, "Hello RRT"); + let early_probe = report + .early_content_probe + .as_ref() + .expect("early content probe should exist"); + assert_eq!(early_probe.first_post_text_nonzero_offset, 88); + assert_eq!(early_probe.zero_pad_after_text_len, 12); + assert_eq!(early_probe.first_post_text_block_len, 4); + assert_eq!(early_probe.first_post_text_block_hex, "11223344"); + assert_eq!(early_probe.trailing_zero_pad_after_first_block_len, 16); + assert_eq!(early_probe.secondary_nonzero_offset, Some(108)); + assert_eq!(early_probe.secondary_aligned_word_window_offset, Some(108)); + assert_eq!( + &early_probe.secondary_aligned_word_window_words[..2], + &[0x78563412, 0xf0debc9a] + ); + assert!( + early_probe + .secondary_preview_hex + .starts_with("123456789abcdef0") + ); + let secondary_variant = report + .secondary_variant_probe + .as_ref() + .expect("secondary variant probe should exist"); + assert_eq!(secondary_variant.variant_family, "unknown"); + let container_profile = report + .container_profile + .as_ref() + .expect("container profile should exist"); + assert_eq!(container_profile.profile_family, "unknown"); + assert!(!container_profile.is_known_profile); + assert!(report.save_bootstrap_block.is_none()); + assert!(report.save_anchor_run_block.is_none()); + assert!(report.runtime_anchor_cycle_block.is_none()); + assert!(report.runtime_trailer_block.is_none()); + assert!(report.runtime_post_span_probe.is_none()); + assert!(report.classic_rehydrate_profile_probe.is_none()); + assert_eq!(report.known_tag_hits[0].tag_id, 0x2cee); + assert_eq!(report.known_tag_hits[0].hit_count, 1); + assert_eq!(report.known_tag_hits[0].sample_offsets, vec![120]); + assert_eq!(report.known_tag_hits[1].tag_id, 0x2d51); + assert_eq!(report.known_tag_hits[1].sample_offsets, vec![123]); + assert_eq!(report.known_tag_hits[2].tag_id, 0x9471); + assert_eq!(report.known_tag_hits[2].sample_offsets, vec![126]); + assert_eq!(report.known_tag_hits[3].tag_id, 0x9472); + assert_eq!(report.known_tag_hits[3].sample_offsets, vec![129]); +} + +#[test] +fn warns_when_no_grounded_tags_are_present() { + let report = inspect_smp_bytes(&[0xaa, 0xbb, 0xcc]); + + assert!(!report.contains_grounded_runtime_tags); + assert!(report.known_tag_hits.is_empty()); + assert_eq!(report.preamble.word_count, 0); + assert!(report.shared_header.is_none()); + assert!(report.header_variant_probe.is_none()); + assert!(report.first_ascii_run.is_none()); + assert!(report.early_content_probe.is_none()); + assert!(report.secondary_variant_probe.is_none()); + assert!(report.container_profile.is_none()); + assert!(report.save_bootstrap_block.is_none()); + assert!(report.save_anchor_run_block.is_none()); + assert!(report.runtime_anchor_cycle_block.is_none()); + assert!(report.runtime_trailer_block.is_none()); + assert!(report.runtime_post_span_probe.is_none()); + assert!(report.classic_rehydrate_profile_probe.is_none()); + assert!( + report + .warnings + .iter() + .any(|warning| warning.contains("No grounded runtime bundle tags were found")) + ); +} + +#[test] +fn parses_save_anchor_cycle_and_trailer() { + let cycle_words: [u32; 9] = [ + 0x00000000, 0x0186a000, 0x00000000, 0x86a00000, 0x00000001, 0xa0000000, 0x00000186, + 0x00000000, 0x000186a0, + ]; + let trailer_words: [u32; 3] = [0x00020000, 0x00030000, 0x2ee10000]; + let mut bytes = vec![0u8; 0x1c + (cycle_words.len() * 2 + 2 + trailer_words.len()) * 4]; + + let mut cursor = 0x1c; + for _ in 0..2 { + for word in cycle_words { + bytes[cursor..cursor + 4].copy_from_slice(&word.to_le_bytes()); + cursor += 4; + } + } + for word in &cycle_words[..2] { + bytes[cursor..cursor + 4].copy_from_slice(&(*word).to_le_bytes()); + cursor += 4; + } + for word in trailer_words { + bytes[cursor..cursor + 4].copy_from_slice(&word.to_le_bytes()); + cursor += 4; + } + + let container_profile = SmpContainerProfile { + profile_family: "rt3-classic-save-container-v1".to_string(), + profile_evidence: vec!["test".to_string()], + is_known_profile: true, + }; + let bootstrap = SmpSaveBootstrapBlock { + profile_family: "rt3-classic-save-container-v1".to_string(), + aligned_window_offset: 0, + leading_word: 0, + leading_word_hex: "0x00000000".to_string(), + anchor_word: 0, + anchor_word_hex: "0x00000000".to_string(), + descriptor_word_2: 0, + descriptor_word_2_hex: "0x00000000".to_string(), + descriptor_word_3: 0, + descriptor_word_3_hex: "0x00000000".to_string(), + descriptor_word_4: 0, + descriptor_word_4_hex: "0x00000000".to_string(), + descriptor_word_5: 0, + descriptor_word_5_hex: "0x00000000".to_string(), + descriptor_word_6: 0, + descriptor_word_6_hex: "0x00000000".to_string(), + descriptor_word_7: 0, + descriptor_word_7_hex: "0x00000000".to_string(), + }; + + let parsed = parse_save_anchor_run_block(&bytes, Some(&container_profile), Some(&bootstrap)) + .expect("cycle block should parse"); + + assert_eq!(parsed.cycle_start_offset, 0x1c); + assert_eq!(parsed.cycle_words, cycle_words); + assert_eq!(parsed.full_cycle_count, 2); + assert_eq!(parsed.partial_cycle_word_count, 2); + assert_eq!( + parsed.trailer_offset, + 0x1c + (cycle_words.len() * 2 + 2) * 4 + ); + assert_eq!(parsed.trailer_words, trailer_words); +} + +#[test] +fn classifies_runtime_trailer_family() { + let runtime_anchor_cycle_block = SmpRuntimeAnchorCycleBlock { + profile_family: "rt3-classic-sandbox-container-v1".to_string(), + cycle_start_offset: 0x33c, + cycle_words: vec![0; 9], + cycle_hex_words: vec!["0x00000000".to_string(); 9], + full_cycle_count: 3, + partial_cycle_word_count: 2, + trailer_offset: 0x3b0, + trailer_words: vec![ + 0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, 0x2ee10000, + 0x32c80000, 0x0dcd0000, 0x01010107, 0x26010000, 0x01010107, 0x00010000, 0x0334c68c, + 0x03000000, 0x01000000, + ], + trailer_hex_words: Vec::new(), + }; + let container_profile = SmpContainerProfile { + profile_family: "rt3-classic-sandbox-container-v1".to_string(), + profile_evidence: vec!["test".to_string()], + is_known_profile: true, + }; + + let trailer = + parse_runtime_trailer_block(Some(&container_profile), Some(&runtime_anchor_cycle_block)) + .expect("runtime trailer should parse"); + + assert_eq!(trailer.trailer_family, "rt3-classic-sandbox-trailer-v1"); + assert_eq!(trailer.prefix_words_0_to_5[0], 0x00010000); + assert_eq!(trailer.tag_word_6, 0x2ee10000); + assert_eq!(trailer.tag_chunk_id_u16, 0x2ee1); + assert_eq!(trailer.selector_word_8, 0x0dcd0000); + assert_eq!(trailer.selector_high_u16, 0x0dcd); + assert_eq!(trailer.mode_word_15, 0x01000000); +} + +#[test] +fn probes_runtime_post_span_region() { + let mut bytes = vec![0u8; 0x200]; + bytes[0x90..0x94].copy_from_slice(&0x32dc0000u32.to_le_bytes()); + bytes[0x94..0x98].copy_from_slice(&0x37140000u32.to_le_bytes()); + bytes[0x98..0x9c].copy_from_slice(&0x03000000u32.to_le_bytes()); + bytes[0xa0..0xa4].copy_from_slice(&0x37150000u32.to_le_bytes()); + bytes[0xa4..0xa8].copy_from_slice(&0x00010000u32.to_le_bytes()); + bytes[0xa8..0xac].copy_from_slice(&0x00410000u32.to_le_bytes()); + + let trailer = SmpRuntimeTrailerBlock { + profile_family: "rt3-classic-save-container-v1".to_string(), + trailer_family: "test".to_string(), + trailer_evidence: Vec::new(), + trailer_offset: 0x40, + prefix_words_0_to_5: Vec::new(), + prefix_hex_words_0_to_5: Vec::new(), + tag_word_6: 0x2ee10000, + tag_word_6_hex: "0x2ee10000".to_string(), + tag_chunk_id_u16: 0x2ee1, + tag_chunk_id_hex: "0x2ee1".to_string(), + tag_chunk_id_grounded_alignment: None, + length_word_7: 0x00200000, + length_word_7_hex: "0x00200000".to_string(), + length_high_u16: 0x0020, + length_high_hex: "0x0020".to_string(), + selector_word_8: 0, + selector_word_8_hex: "0x00000000".to_string(), + selector_high_u16: 0, + selector_high_hex: "0x0000".to_string(), + layout_word_9: 0, + layout_word_9_hex: "0x00000000".to_string(), + descriptor_word_10: 0, + descriptor_word_10_hex: "0x00000000".to_string(), + descriptor_high_u16: 0, + descriptor_high_hex: "0x0000".to_string(), + descriptor_word_11: 0, + descriptor_word_11_hex: "0x00000000".to_string(), + counter_word_12: 0, + counter_word_12_hex: "0x00000000".to_string(), + offset_word_13: 0, + offset_word_13_hex: "0x00000000".to_string(), + span_word_14: 0, + span_word_14_hex: "0x00000000".to_string(), + mode_word_15: 0, + mode_word_15_hex: "0x00000000".to_string(), + words: Vec::new(), + hex_words: Vec::new(), + }; + + let probe = parse_runtime_post_span_probe(&bytes, Some(&trailer)) + .expect("post-span probe should parse"); + + assert_eq!(probe.span_target_offset, 0x60); + assert_eq!(probe.next_nonzero_offset, Some(0x92)); + assert_eq!(probe.next_aligned_candidate_offset, Some(0x8c)); + assert_eq!(probe.header_candidates.len(), 1); + assert_eq!(probe.header_candidates[0].dense_word_count, 3); + assert_eq!(probe.header_candidates[0].grounded_alignments.len(), 2); + assert_eq!(probe.grounded_progress_hits[0], "0x32dc@0x00000090"); +} + +#[test] +fn parses_classic_rehydrate_profile_probe() { + let mut bytes = vec![0u8; 0x220]; + bytes[0x90..0x94].copy_from_slice(&0x32dc0000u32.to_le_bytes()); + bytes[0x94..0x98].copy_from_slice(&0x37140000u32.to_le_bytes()); + bytes[0x1a0..0x1a4].copy_from_slice(&0x37150000u32.to_le_bytes()); + bytes[0xab..0xb7].copy_from_slice(b"test-map.gmp"); + bytes[0xde..0xe6].copy_from_slice(b"Test Map"); + + let post_span = SmpRuntimePostSpanProbe { + profile_family: "rt3-classic-save-container-v1".to_string(), + span_target_offset: 0, + next_nonzero_offset: Some(0x92), + next_aligned_candidate_offset: Some(0x8c), + next_aligned_candidate_words: vec![0, 0x32dc0000, 0x37140000, 0x03000000], + next_aligned_candidate_hex_words: vec![], + header_candidates: vec![], + grounded_progress_hits: vec![ + "0x32dc@0x00000090".to_string(), + "0x3714@0x00000094".to_string(), + "0x3715@0x000001a0".to_string(), + ], + }; + + let probe = parse_classic_rehydrate_profile_probe(&bytes, Some(&post_span)) + .expect("classic rehydrate probe should parse"); + + assert_eq!(probe.packed_profile_offset, 0x98); + assert_eq!(probe.packed_profile_len, 0x108); + assert_eq!(probe.ascii_runs[0].preview, "test-map.gmp"); + assert_eq!(probe.packed_profile_block.leading_word_0, 0x00000000); + assert_eq!( + probe.packed_profile_block.map_path.as_deref(), + Some("test-map.gmp") + ); + assert_eq!( + probe.packed_profile_block.display_name.as_deref(), + Some("Test Map") + ); + assert_eq!(probe.packed_profile_block.profile_byte_0x77, 0x00); + assert_eq!(probe.packed_profile_block.profile_byte_0x82, 0x00); + assert_eq!(probe.packed_profile_block.profile_byte_0x97, 0x00); + assert_eq!(probe.packed_profile_block.profile_byte_0xc5, 0x00); +} + +#[test] +fn parses_rt3_105_packed_profile_probe() { + let mut bytes = vec![0u8; 0x9000]; + let block = 0x73c0usize; + bytes[block..block + 4].copy_from_slice(&0x00000003u32.to_le_bytes()); + bytes[block + 0x0c..block + 0x10].copy_from_slice(&0x01000000u32.to_le_bytes()); + bytes[block + 0x10..block + 0x1d].copy_from_slice(b"test-105.gmp\0"); + bytes[block + 0x43..block + 0x4c].copy_from_slice(b"Test 105\0"); + bytes[block + 0x77] = 0x07; + bytes[block + 0x82] = 0x4d; + bytes[block + 0x84..block + 0x88].copy_from_slice(&0x65010000u32.to_le_bytes()); + + let header_variant_probe = SmpHeaderVariantProbe { + variant_family: "rt3-105-common-header-v1".to_string(), + variant_evidence: vec![], + is_known_family: true, + }; + let probe = + parse_rt3_105_packed_profile_probe(&bytes, Some("gms"), Some(&header_variant_probe), None) + .expect("1.05 packed profile probe should parse"); + + assert_eq!(probe.profile_family, "rt3-105-save-analog-block-inferred"); + assert_eq!(probe.packed_profile_offset, 0x73c0); + assert_eq!(probe.packed_profile_len, 0x108); + assert_eq!( + probe.packed_profile_block.map_path.as_deref(), + Some("test-105.gmp") + ); + assert_eq!( + probe.packed_profile_block.display_name.as_deref(), + Some("Test 105") + ); + assert_eq!(probe.packed_profile_block.profile_byte_0x77, 0x07); + assert_eq!(probe.packed_profile_block.profile_byte_0x82, 0x4d); + assert_eq!(probe.packed_profile_block.profile_byte_0x97, 0x00); + assert_eq!(probe.packed_profile_block.profile_byte_0xc5, 0x00); +} + +#[test] +fn builds_classic_save_load_summary() { + let summary = build_save_load_summary( + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-classic-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + None, + None, + Some(&SmpClassicRehydrateProfileProbe { + 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_len_hex: "0x108".to_string(), + packed_profile_block: SmpClassicPackedProfileBlock { + 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: 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![], + }, + ascii_runs: vec![], + }), + None, + None, + ) + .expect("classic summary"); + + assert_eq!(summary.mechanism_family, "classic-save-rehydrate-v1"); + assert_eq!(summary.mechanism_confidence, "grounded"); + assert_eq!(summary.map_path.as_deref(), Some("British Isles.gmp")); + assert_eq!(summary.packed_profile_len, Some(0x108)); +} + +#[test] +fn builds_rt3_105_save_load_summary_with_candidate_table() { + let summary = build_save_load_summary( + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + None, + Some(&SmpRt3105PostSpanBridgeProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(), + bridge_evidence: vec![], + span_target_offset: 0x3678, + next_candidate_offset: Some(0x4f14), + next_candidate_delta_from_span_target: Some(0x189c), + packed_profile_offset: 0x73c0, + packed_profile_delta_from_span_target: 0x3d48, + next_candidate_delta_from_packed_profile: Some(-0x24ac), + selector_high_u16: 0x7110, + selector_high_hex: "0x7110".to_string(), + descriptor_high_u16: 0x7801, + descriptor_high_hex: "0x7801".to_string(), + next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515], + next_candidate_high_hex_words: vec![], + }), + None, + Some(&SmpRt3105PackedProfileProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + packed_profile_offset: 0x73c0, + packed_profile_len: 0x108, + packed_profile_len_hex: "0x108".to_string(), + 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: 1, + header_flag_word_3_hex: "0x00000001".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: 0, + profile_byte_0x97_hex: "0x00".to_string(), + profile_byte_0xc5: 0, + profile_byte_0xc5_hex: "0x00".to_string(), + stable_nonzero_words: vec![], + }, + ascii_runs: vec![], + }), + Some(&SmpRt3105SaveNameTableProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-bridge-secondary-block".to_string(), + semantic_family: "scenario-named-candidate-availability-table".to_string(), + semantic_alignment: vec![], + header_offset: 0x6a70, + header_word_0: 0, + header_word_0_hex: "0x00000000".to_string(), + header_word_1: 0, + header_word_1_hex: "0x00000000".to_string(), + header_word_2: 0x332e, + header_word_2_hex: "0x0000332e".to_string(), + entry_stride: 0x22, + entry_stride_hex: "0x22".to_string(), + header_prefix_word_count: 11, + observed_entry_capacity: 0x44, + observed_entry_count: 67, + zero_trailer_entry_count: 3, + nonzero_trailer_entry_count: 64, + distinct_trailer_words: vec![0, 1], + distinct_trailer_hex_words: vec!["0x00000000".to_string(), "0x00000001".to_string()], + zero_trailer_entry_names: vec![ + "Nuclear Power Plant".to_string(), + "Recycling Plant".to_string(), + "Uranium Mine".to_string(), + ], + entries_offset: 0x6ad1, + entries_end_offset: 0x73b7, + trailing_footer_hex: "dc3200001437000000".to_string(), + footer_progress_word_0: 0x32dc, + footer_progress_word_0_hex: "0x000032dc".to_string(), + footer_progress_word_1: 0x3714, + footer_progress_word_1_hex: "0x00003714".to_string(), + footer_trailing_byte: 0, + footer_trailing_byte_hex: "0x00".to_string(), + footer_grounded_alignments: vec![], + entries: vec![], + evidence: vec![], + }), + ) + .expect("1.05 summary"); + + assert_eq!(summary.mechanism_family, "rt3-105-save-post-span-bridge-v1"); + assert_eq!(summary.mechanism_confidence, "mixed"); + assert_eq!(summary.map_path.as_deref(), Some("Alternate USA.gmp")); + assert_eq!( + summary + .candidate_table + .as_ref() + .expect("candidate table") + .zero_availability_count, + 3 + ); +} + +#[test] +fn loads_classic_save_slice_from_report() { + let mut report = inspect_smp_bytes(&[]); + let classic_probe = SmpClassicRehydrateProfileProbe { + 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_len_hex: "0x108".to_string(), + packed_profile_block: SmpClassicPackedProfileBlock { + 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: 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![], + }, + ascii_runs: vec![], + }; + report.classic_rehydrate_profile_probe = Some(classic_probe.clone()); + report.save_load_summary = build_save_load_summary( + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-classic-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + None, + None, + Some(&classic_probe), + None, + None, + ); + + let slice = load_save_slice_from_report(&report).expect("classic save slice"); + assert_eq!(slice.mechanism_family, "classic-save-rehydrate-v1"); + assert_eq!( + slice + .profile + .as_ref() + .and_then(|profile| profile.map_path.as_deref()), + Some("British Isles.gmp") + ); + assert!(slice.candidate_availability_table.is_none()); +} + +#[test] +fn loads_event_runtime_collection_summary_from_report() { + let mut report = inspect_smp_bytes(&[]); + let classic_probe = SmpClassicRehydrateProfileProbe { + 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_len_hex: "0x108".to_string(), + packed_profile_block: SmpClassicPackedProfileBlock { + 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: 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![], + }, + ascii_runs: vec![], + }; + report.classic_rehydrate_profile_probe = Some(classic_probe.clone()); + report.save_load_summary = build_save_load_summary( + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-classic-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + None, + None, + Some(&classic_probe), + None, + None, + ); + report.event_runtime_collection_summary = Some(SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 5, + live_record_count: 3, + live_entry_ids: vec![1, 3, 5], + decoded_record_count: 0, + imported_runtime_record_count: 0, + records_with_trigger_kind: 0, + records_missing_trigger_kind: 3, + nondirect_compact_record_count: 0, + nondirect_compact_records_missing_trigger_kind: 0, + trigger_kinds_present: vec![], + add_building_dispatch_strip_record_indexes: vec![], + add_building_dispatch_strip_descriptor_labels: vec![], + add_building_dispatch_strip_records_with_trigger_kind: 0, + add_building_dispatch_strip_records_missing_trigger_kind: 0, + add_building_dispatch_strip_row_shape_families: vec![], + add_building_dispatch_strip_signature_families: vec![], + add_building_dispatch_strip_condition_tuple_families: vec![], + add_building_dispatch_strip_signature_condition_clusters: vec![], + control_lane_notes: vec![], + records: build_unsupported_event_runtime_record_summaries(&[1, 3, 5], "test summary"), + }); + + let slice = load_save_slice_from_report(&report).expect("classic save slice"); + assert_eq!( + slice + .event_runtime_collection + .as_ref() + .map(|summary| summary.live_entry_ids.clone()), + Some(vec![1, 3, 5]) + ); +} + +#[test] +fn loads_placed_structure_collection_from_report() { + let mut report = inspect_smp_bytes(&[]); + let classic_probe = SmpClassicRehydrateProfileProbe { + 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_len_hex: "0x108".to_string(), + packed_profile_block: SmpClassicPackedProfileBlock { + 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: 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![], + }, + ascii_runs: vec![], + }; + report.classic_rehydrate_profile_probe = Some(classic_probe.clone()); + report.save_load_summary = build_save_load_summary( + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-classic-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + None, + None, + Some(&classic_probe), + None, + None, + ); + report.save_region_record_triplet_probe = Some(SmpSaveRegionRecordTripletProbe { + profile_family: "rt3-classic-save-container-v1".to_string(), + source_kind: "save-region-record-triplets".to_string(), + semantic_family: "scenario-save-region-record-triplets".to_string(), + records_tag_offset: 0x3400, + close_tag_offset: 0x3500, + record_count: 2, + entries: vec![ + SmpSaveRegionRecordTripletEntryProbe { + record_index: 0, + name: "Marker09".to_string(), + record_payload_relative_offset: 0, + record_payload_relative_offset_hex: "0x0".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0x10, + profile_tag_relative_offset: 0x2e, + pre_name_prefix_len: 0, + pre_name_prefix_hex_bytes: Vec::new(), + pre_name_prefix_dword_candidates: Vec::new(), + policy_chunk_len: 0x1a, + profile_chunk_len: 0x40, + policy_leading_f32_0: 368.0, + policy_leading_f32_1: 0.0, + policy_leading_f32_2: 92.0, + policy_reserved_dwords: vec![0, 0, 0], + policy_reserved_dword_candidates: Vec::new(), + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_collection: Some(SmpSaveRegionProfileCollectionProbe { + direct_collection_flag: 1, + entry_stride: 0x22, + live_id_bound: 18, + live_record_count: 17, + entry_start_relative_offset: 0x4d, + trailing_padding_len: 2, + entries: vec![ + SmpSaveRegionProfileEntryProbe { + entry_index: 0, + row_relative_offset: 0x4d, + name: "House".to_string(), + trailing_weight_f32: 0.2, + }, + SmpSaveRegionProfileEntryProbe { + entry_index: 1, + row_relative_offset: 0x6f, + name: "Farm Corn".to_string(), + trailing_weight_f32: 0.2, + }, + ], + }), + }, + SmpSaveRegionRecordTripletEntryProbe { + record_index: 1, + name: "Marker10".to_string(), + record_payload_relative_offset: 0x6e, + record_payload_relative_offset_hex: "0x6e".to_string(), + name_tag_relative_offset: 0x76, + policy_tag_relative_offset: 0x86, + profile_tag_relative_offset: 0xa4, + pre_name_prefix_len: 8, + pre_name_prefix_hex_bytes: vec![ + "0xaa".to_string(), + "0xbb".to_string(), + "0xcc".to_string(), + "0xdd".to_string(), + ], + pre_name_prefix_dword_candidates: Vec::new(), + policy_chunk_len: 0x1a, + profile_chunk_len: 0x20, + policy_leading_f32_0: 552.0, + policy_leading_f32_1: 0.0, + policy_leading_f32_2: 276.0, + policy_reserved_dwords: vec![0, 4, 0], + policy_reserved_dword_candidates: Vec::new(), + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_collection: Some(SmpSaveRegionProfileCollectionProbe { + direct_collection_flag: 1, + entry_stride: 0x22, + live_id_bound: 26, + live_record_count: 24, + entry_start_relative_offset: 0x50, + trailing_padding_len: 0, + entries: vec![SmpSaveRegionProfileEntryProbe { + entry_index: 0, + row_relative_offset: 0x50, + name: "Farm Corn".to_string(), + trailing_weight_f32: 0.2, + }], + }), + }, + ], + evidence: vec![], + }); + report.save_region_fixed_row_run_candidate_probe = + Some(SmpSaveRegionFixedRowRunCandidateProbe { + profile_family: "rt3-classic-save-container-v1".to_string(), + source_kind: "save-region-fixed-row-run-candidates".to_string(), + semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), + target_row_count: 2, + target_row_stride: 0xbc, + target_row_stride_hex: "0xbc".to_string(), + scan_start_offset: 0x5200, + scan_start_offset_hex: "0x5200".to_string(), + scan_end_offset: 0x5600, + scan_end_offset_hex: "0x5600".to_string(), + candidates: vec![SmpSaveRegionFixedRowRunCandidate { + count_offset: 0x5300, + count_offset_hex: "0x5300".to_string(), + row_count: 2, + row_stride: 0xbc, + row_stride_hex: "0xbc".to_string(), + rows_offset: 0x5310, + rows_offset_hex: "0x5310".to_string(), + rows_end_offset: 0x5488, + rows_end_offset_hex: "0x5488".to_string(), + distance_to_region_metadata_tag: 0x110, + distance_to_region_metadata_tag_hex: "0x110".to_string(), + dword_lane_summaries: vec![], + shape_signature: "dword0:f32,dword1:zero".to_string(), + shape_family_signature: "family-a".to_string(), + trailing_byte_zero_count: 2, + trailing_byte_nonzero_count: 0, + trailing_byte_distinct_value_count: 1, + trailing_byte_sample_values_hex: vec!["0x00".to_string()], + best_probable_density_lane_relative_offset_hex: Some("0x24".to_string()), + }], + evidence: vec![], + }); + report.save_placed_structure_record_triplet_probe = + Some(SmpSavePlacedStructureRecordTripletProbe { + profile_family: "rt3-classic-save-container-v1".to_string(), + source_kind: "save-placed-structure-record-triplets".to_string(), + semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), + records_tag_offset: 0x3600, + close_tag_offset: 0x3800, + record_count: 2, + entries: vec![ + SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: 0, + primary_name: "FarmCorn".to_string(), + secondary_name: "FarmSet".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0x10, + profile_tag_relative_offset: 0x2e, + policy_chunk_len: 0x1a, + profile_chunk_len: 0x10, + policy_f32_lane_0: 1.0, + policy_f32_lane_1: 2.0, + policy_f32_lane_2: 3.0, + policy_f32_lane_3: 4.0, + policy_f32_lane_4: 5.0, + policy_reserved_dword: 0, + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_open_marker: 0x00005dc1, + profile_open_marker_hex: "0x00005dc1".to_string(), + profile_repeated_primary_name: "FarmCorn".to_string(), + profile_repeated_secondary_name: "FarmSet".to_string(), + profile_footer_relative_offset: 0x08, + profile_footer_relative_offset_hex: "0x8".to_string(), + profile_pre_footer_padding_len: 1, + profile_pre_footer_padding_hex_bytes: vec!["0x00".to_string()], + profile_companion_byte_u8: Some(0), + profile_companion_byte_hex: Some("0x00".to_string()), + profile_payload_dword: 0, + profile_payload_dword_hex: "0x00000000".to_string(), + profile_sentinel_i32: 4, + profile_status_kind: "farm_growth_stage_bucket".to_string(), + farm_growth_stage_index: Some(4), + profile_close_marker: 0x00005dc2, + profile_close_marker_hex: "0x00005dc2".to_string(), + }, + SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: 1, + primary_name: "StationA".to_string(), + secondary_name: "StationSetA".to_string(), + name_tag_relative_offset: 0x40, + policy_tag_relative_offset: 0x50, + profile_tag_relative_offset: 0x6e, + policy_chunk_len: 0x1a, + profile_chunk_len: 0x10, + policy_f32_lane_0: 0.0, + policy_f32_lane_1: 0.0, + policy_f32_lane_2: 0.0, + policy_f32_lane_3: 0.0, + policy_f32_lane_4: 0.0, + policy_reserved_dword: 0, + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_open_marker: 0x00005dc1, + profile_open_marker_hex: "0x00005dc1".to_string(), + profile_repeated_primary_name: "StationA".to_string(), + profile_repeated_secondary_name: "StationSetA".to_string(), + profile_footer_relative_offset: 0x08, + profile_footer_relative_offset_hex: "0x8".to_string(), + profile_pre_footer_padding_len: 1, + profile_pre_footer_padding_hex_bytes: vec!["0x07".to_string()], + profile_companion_byte_u8: Some(7), + profile_companion_byte_hex: Some("0x07".to_string()), + profile_payload_dword: 0x00005dc1, + profile_payload_dword_hex: "0x00005dc1".to_string(), + profile_sentinel_i32: 0, + profile_status_kind: "opaque_nondefault".to_string(), + farm_growth_stage_index: None, + profile_close_marker: 0x00005dc2, + profile_close_marker_hex: "0x00005dc2".to_string(), + }, + ], + evidence: vec![], + }); + report.save_placed_structure_dynamic_side_buffer_probe = + Some(SmpSavePlacedStructureDynamicSideBufferProbe { + profile_family: "rt3-classic-save-container-v1".to_string(), + source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(), + semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records" + .to_string(), + metadata_tag_offset: 0x3800, + records_tag_offset: 0x3900, + close_tag_offset: 0x3d00, + records_span_len: 0x400, + direct_record_stride: 6, + direct_record_stride_hex: "0x6".to_string(), + live_id_bound: 0x80, + live_id_bound_hex: "0x00000080".to_string(), + live_record_count: 118, + live_record_count_hex: "0x00000076".to_string(), + owner_shared_dword: 0xff0000ff, + owner_shared_dword_hex: "0xff0000ff".to_string(), + owner_shared_dword_relative_offset: 0, + owner_shared_dword_matches_first_compact_prefix_leading_dword: true, + first_record_child_count_after_owner_shared: Some(1), + first_record_child_count_after_owner_shared_hex: Some("0x0001".to_string()), + first_record_saved_primary_child_byte_after_owner_shared: Some(0xff), + first_record_saved_primary_child_byte_after_owner_shared_hex: Some("0xff".to_string()), + first_record_first_name_tag_relative_offset_after_owner_shared: Some(3), + prefix_leading_dword: 0xff0000ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 1, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + first_embedded_name_tag_relative_offset: 3, + embedded_name_tag_count: 118, + decoded_embedded_name_row_count: 118, + decoded_embedded_name_row_with_tertiary_name_count: 0, + unique_compact_prefix_pattern_count: 4, + prefix_leading_dword_matching_embedded_profile_tag_count: 0, + unique_embedded_name_pair_count: 9, + first_embedded_primary_name: Some("StationA".to_string()), + first_embedded_secondary_name: Some("StationSetA".to_string()), + first_embedded_tertiary_name: None, + embedded_name_row_samples: vec![], + compact_prefix_pattern_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary { + prefix_leading_dword: 0xff0000ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 1, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + count: 62, + first_name_tag_relative_offset: 3, + prefix_leading_dword_matches_embedded_profile_tag: false, + section_like_primary_name_count: 12, + cap_like_primary_name_count: 21, + other_primary_name_count: 29, + first_primary_name: Some("StationA".to_string()), + first_secondary_name: Some("StationSetA".to_string()), + }, + ], + name_pair_summaries: vec![SmpSavePlacedStructureDynamicSideBufferNamePairSummary { + primary_name: "StationA".to_string(), + secondary_name: "StationSetA".to_string(), + count: 14, + first_name_tag_relative_offset: 3, + unique_compact_prefix_pattern_count: 1, + dominant_prefix_leading_dword: 0xff0000ff, + dominant_prefix_leading_dword_hex: "0xff0000ff".to_string(), + dominant_prefix_trailing_word: 1, + dominant_prefix_trailing_word_hex: "0x0001".to_string(), + dominant_prefix_separator_byte: 0xff, + dominant_prefix_separator_byte_hex: "0xff".to_string(), + dominant_prefix_count: 14, + }], + payload_envelope_summary: None, + live_entry_prelude_summary: None, + evidence: vec![], + }); + let slice = load_save_slice_from_report(&report).expect("classic save slice"); + let region_collection = slice + .region_collection + .expect("region collection should project"); + assert_eq!(region_collection.source_kind, "save-region-record-triplets"); + assert_eq!(region_collection.observed_entry_count, 2); + assert_eq!(region_collection.entries[0].name, "Marker09"); + assert_eq!(region_collection.entries[1].pre_name_prefix_len, 8); + assert_eq!( + region_collection.entries[1].policy_reserved_dwords, + vec![0, 4, 0] + ); + assert_eq!( + region_collection.entries[0] + .profile_collection + .as_ref() + .map(|collection| collection.entries.len()), + Some(2) + ); + let region_fixed_row_run_summary = slice + .region_fixed_row_run_summary + .expect("region fixed-row summary should project"); + assert_eq!( + region_fixed_row_run_summary.source_kind, + "save-region-fixed-row-run-candidates" + ); + assert_eq!(region_fixed_row_run_summary.candidates.len(), 1); + assert_eq!( + region_fixed_row_run_summary.candidates[0].rows_offset_hex, + "0x5310" + ); + let collection = slice + .placed_structure_collection + .expect("placed structure collection should project"); + assert_eq!( + collection.source_kind, + "save-placed-structure-record-triplets" + ); + assert_eq!(collection.observed_entry_count, 2); + assert_eq!(collection.entries[0].primary_name, "FarmCorn"); + assert_eq!(collection.entries[0].farm_growth_stage_index, Some(4)); + assert_eq!(collection.entries[1].profile_companion_byte_u8, Some(7)); + let side_buffer_summary = slice + .placed_structure_dynamic_side_buffer_summary + .expect("side-buffer summary should project"); + assert_eq!( + side_buffer_summary.source_kind, + "save-placed-structure-dynamic-side-buffer-records" + ); + assert_eq!(side_buffer_summary.observed_entry_count, 118); + assert_eq!(side_buffer_summary.unique_embedded_name_pair_count, 9); + assert_eq!(side_buffer_summary.triplet_alignment_overlap_count, 1); + assert_eq!( + side_buffer_summary.triplet_alignment_side_buffer_only_name_pair_count, + 0 + ); + assert!(slice.notes.iter().any(|line| { + line.contains("loaded region triplet rows as first-class context") + && line.contains("3 embedded profile rows") + })); + assert!(slice.notes.iter().any(|line| { + line.contains("region fixed-row run summary") && line.contains("Some(\"0x5310\")") + })); + assert!(slice.notes.iter().any(|line| { + line.contains("placed-structure triplet rows as first-class context") && line.contains("2") + })); + assert!(slice.notes.iter().any(|line| { + line.contains("placed-structure dynamic side-buffer summary") + && line.contains("118 decoded name rows") + })); +} + +#[test] +fn loads_rt3_105_save_slice_from_report() { + let mut report = inspect_smp_bytes(&[]); + let packed_profile = SmpRt3105PackedProfileProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + packed_profile_offset: 0x73c0, + packed_profile_len: 0x108, + packed_profile_len_hex: "0x108".to_string(), + 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: 1, + header_flag_word_3_hex: "0x00000001".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: 0, + profile_byte_0x97_hex: "0x00".to_string(), + profile_byte_0xc5: 0, + profile_byte_0xc5_hex: "0x00".to_string(), + stable_nonzero_words: vec![], + }, + ascii_runs: vec![], + }; + let name_table = SmpRt3105SaveNameTableProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-bridge-secondary-block".to_string(), + semantic_family: "scenario-named-candidate-availability-table".to_string(), + semantic_alignment: vec![], + header_offset: 0x6a70, + header_word_0: 0, + header_word_0_hex: "0x00000000".to_string(), + header_word_1: 0, + header_word_1_hex: "0x00000000".to_string(), + header_word_2: 0x332e, + header_word_2_hex: "0x0000332e".to_string(), + entry_stride: 0x22, + entry_stride_hex: "0x22".to_string(), + header_prefix_word_count: 11, + observed_entry_capacity: 0x44, + observed_entry_count: 2, + zero_trailer_entry_count: 1, + nonzero_trailer_entry_count: 1, + distinct_trailer_words: vec![0, 1], + distinct_trailer_hex_words: vec!["0x00000000".to_string(), "0x00000001".to_string()], + zero_trailer_entry_names: vec!["Uranium Mine".to_string()], + entries_offset: 0x6ad1, + entries_end_offset: 0x6b15, + trailing_footer_hex: "dc3200001437000000".to_string(), + footer_progress_word_0: 0x32dc, + footer_progress_word_0_hex: "0x000032dc".to_string(), + footer_progress_word_1: 0x3714, + footer_progress_word_1_hex: "0x00003714".to_string(), + footer_trailing_byte: 0, + footer_trailing_byte_hex: "0x00".to_string(), + footer_grounded_alignments: vec![], + entries: vec![ + SmpRt3105SaveNameTableEntry { + index: 0, + offset: 0x6ad1, + text: "AutoPlant".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + SmpRt3105SaveNameTableEntry { + index: 1, + offset: 0x6af3, + text: "Uranium Mine".to_string(), + availability_dword: 0, + availability_dword_hex: "0x00000000".to_string(), + trailer_word: 0, + trailer_word_hex: "0x00000000".to_string(), + }, + ], + evidence: vec![], + }; + let named_locomotive_table = SmpRt3105SaveNamedLocomotiveAvailabilityProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-direct-locomotive-row-run".to_string(), + semantic_family: "scenario-named-locomotive-availability-table".to_string(), + semantic_alignment: vec![], + entries_offset: 0x7c78, + entry_stride: RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE, + entry_stride_hex: format!("0x{:x}", RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE), + observed_entry_count: 2, + zero_availability_count: 1, + zero_availability_names: vec!["Big Boy".to_string()], + entries_end_offset: 0x7c78 + 2 * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE, + entries: vec![ + SmpRt3105SaveNameTableEntry { + index: 0, + offset: 0x7c78, + text: "Big Boy".to_string(), + availability_dword: 0, + availability_dword_hex: "0x00000000".to_string(), + trailer_word: 0, + trailer_word_hex: "0x00000000".to_string(), + }, + SmpRt3105SaveNameTableEntry { + index: 1, + offset: 0x7cb9, + text: "GP7".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + ], + evidence: vec![], + }; + let bridge = SmpRt3105PostSpanBridgeProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(), + bridge_evidence: vec![], + span_target_offset: 0x3678, + next_candidate_offset: Some(0x4f14), + next_candidate_delta_from_span_target: Some(0x189c), + packed_profile_offset: 0x73c0, + packed_profile_delta_from_span_target: 0x3d48, + next_candidate_delta_from_packed_profile: Some(-0x24ac), + selector_high_u16: 0x7110, + selector_high_hex: "0x7110".to_string(), + descriptor_high_u16: 0x7801, + descriptor_high_hex: "0x7801".to_string(), + next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515], + next_candidate_high_hex_words: vec![], + }; + report.rt3_105_packed_profile_probe = Some(packed_profile.clone()); + report.rt3_105_save_name_table_probe = Some(name_table.clone()); + report.rt3_105_save_named_locomotive_availability_probe = Some(named_locomotive_table.clone()); + report.save_load_summary = build_save_load_summary( + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + None, + Some(&bridge), + None, + Some(&packed_profile), + Some(&name_table), + ); + + let slice = load_save_slice_from_report(&report).expect("1.05 save slice"); + assert_eq!(slice.mechanism_family, "rt3-105-save-post-span-bridge-v1"); + assert_eq!( + slice + .profile + .as_ref() + .and_then(|profile| profile.map_path.as_deref()), + Some("Alternate USA.gmp") + ); + assert_eq!( + slice + .candidate_availability_table + .as_ref() + .expect("candidate table") + .entries[1] + .text, + "Uranium Mine" + ); + assert_eq!( + slice + .named_locomotive_availability_table + .as_ref() + .expect("named locomotive availability table") + .entries[1] + .text, + "GP7" + ); + assert_eq!( + slice + .locomotive_catalog + .as_ref() + .expect("derived locomotive catalog") + .entries[0] + .name, + "Big Boy" + ); + assert_eq!( + slice + .locomotive_catalog + .as_ref() + .expect("derived locomotive catalog") + .entries[1] + .locomotive_id, + 2 + ); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/services/infrastructure.rs b/crates/rrt-runtime/src/inspect/smp/tests/services/infrastructure.rs new file mode 100644 index 0000000..25b8b14 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/services/infrastructure.rs @@ -0,0 +1,1124 @@ +use super::*; + +#[test] +fn builds_infrastructure_asset_trace_report_with_alias_disproved_status() { + let mut analysis = empty_analysis_report(); + analysis.placed_structure_record_triplets = Some(SmpSavePlacedStructureRecordTripletProbe { + profile_family: analysis.profile_family.clone(), + source_kind: "save-placed-structure-triplets".to_string(), + semantic_family: "placed-structure-triplets".to_string(), + records_tag_offset: 0, + close_tag_offset: 0, + record_count: 2057, + entries: Vec::new(), + evidence: Vec::new(), + }); + analysis.placed_structure_dynamic_side_buffer = + Some(SmpSavePlacedStructureDynamicSideBufferProbe { + profile_family: analysis.profile_family.clone(), + source_kind: "save-side-buffer".to_string(), + semantic_family: "infrastructure-asset".to_string(), + metadata_tag_offset: 0, + records_tag_offset: 0, + close_tag_offset: 0, + records_span_len: 0, + direct_record_stride: 6, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 3865, + live_id_bound_hex: "0x00000f19".to_string(), + live_record_count: 3865, + live_record_count_hex: "0x00000f19".to_string(), + owner_shared_dword: 0xff000000, + owner_shared_dword_hex: "0xff000000".to_string(), + owner_shared_dword_relative_offset: 0, + owner_shared_dword_matches_first_compact_prefix_leading_dword: true, + first_record_child_count_after_owner_shared: Some(1), + first_record_child_count_after_owner_shared_hex: Some("0x0001".to_string()), + first_record_saved_primary_child_byte_after_owner_shared: Some(0xff), + first_record_saved_primary_child_byte_after_owner_shared_hex: Some( + "0xff".to_string(), + ), + first_record_first_name_tag_relative_offset_after_owner_shared: Some(3), + prefix_leading_dword: 0xff000000, + prefix_leading_dword_hex: "0xff000000".to_string(), + prefix_trailing_word: 1, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + first_embedded_name_tag_relative_offset: 0x20, + embedded_name_tag_count: 138, + decoded_embedded_name_row_count: 138, + decoded_embedded_name_row_with_tertiary_name_count: 0, + unique_compact_prefix_pattern_count: 7, + prefix_leading_dword_matching_embedded_profile_tag_count: 17, + unique_embedded_name_pair_count: 5, + first_embedded_primary_name: Some("TrackCapST_Cap.3dp".to_string()), + first_embedded_secondary_name: Some("Infrastructure".to_string()), + first_embedded_tertiary_name: None, + embedded_name_row_samples: Vec::new(), + compact_prefix_pattern_summaries: Vec::new(), + name_pair_summaries: vec![SmpSavePlacedStructureDynamicSideBufferNamePairSummary { + primary_name: "TrackCapST_Cap.3dp".to_string(), + secondary_name: "Infrastructure".to_string(), + count: 12, + first_name_tag_relative_offset: 0x20, + unique_compact_prefix_pattern_count: 2, + dominant_prefix_leading_dword: 0xff0000ff, + dominant_prefix_leading_dword_hex: "0xff0000ff".to_string(), + dominant_prefix_trailing_word: 1, + dominant_prefix_trailing_word_hex: "0x0001".to_string(), + dominant_prefix_separator_byte: 0xff, + dominant_prefix_separator_byte_hex: "0xff".to_string(), + dominant_prefix_count: 9, + }], + payload_envelope_summary: Some( + SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSummary { + row_count_with_policy_tag_before_next_name: 120, + row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope: 118, + row_count_missing_policy_tag_before_next_name: 18, + row_count_missing_profile_tag_after_policy: 2, + unique_policy_chunk_lens: vec![0x1a, 0x24], + unique_profile_chunk_lens: vec![0x08, 0x14], + dominant_policy_chunk_len: Some(0x1a), + dominant_policy_chunk_len_count: 110, + dominant_profile_chunk_len: Some(0x08), + dominant_profile_chunk_len_count: 90, + short_profile_flag_pair_summary: Some( + SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSummary { + row_count_with_0x06_profile_span: 72, + unique_flag_pair_count: 2, + dominant_first_flag_byte: Some(0x01), + dominant_first_flag_byte_hex: Some("0x01".to_string()), + dominant_first_flag_byte_count: 60, + dominant_second_flag_byte: Some(0x00), + dominant_second_flag_byte_hex: Some("0x00".to_string()), + dominant_second_flag_byte_count: 55, + dominant_flag_pair: Some( + SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPair { + first_flag_byte: 0x01, + first_flag_byte_hex: "0x01".to_string(), + second_flag_byte: 0x00, + second_flag_byte_hex: "0x00".to_string(), + count: 48, + }, + ), + sample_rows: Vec::new(), + }, + ), + fixed_policy_summary: Some( + SmpSavePlacedStructureDynamicSideBufferFixedPolicySummary { + row_count_with_0x1a_policy_chunk: 118, + unique_trailing_word_count: 1, + dominant_trailing_word: Some(1), + dominant_trailing_word_hex: Some("0x0001".to_string()), + dominant_trailing_word_count: 118, + compact_prefix_correlations: Vec::new(), + sample_rows: Vec::new(), + }, + ), + name_prelude_candidate_summary: Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSummary { + row_count_with_candidate_window: 118, + unique_candidate_pattern_count: 2, + dominant_child_count_candidate: Some(1), + dominant_child_count_candidate_count: 110, + dominant_saved_primary_child_byte_candidate: Some(0xff), + dominant_saved_primary_child_byte_candidate_hex: Some( + "0xff".to_string(), + ), + dominant_saved_primary_child_byte_candidate_count: 110, + dominant_candidate_pattern: Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + count: 110, + }, + ), + candidate_pattern_correlations: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePatternCorrelation { + child_count_candidate: 2, + child_count_candidate_hex: "0x0002".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + row_count: 18, + unique_name_pair_count: 1, + unique_profile_span_count: 1, + dominant_primary_name: Some( + "BridgeSTWood_Section.3dp".to_string(), + ), + dominant_secondary_name: Some( + "Infrastructure".to_string(), + ), + dominant_name_pair_count: 18, + dominant_profile_span: Some(6), + dominant_profile_span_count: 10, + dominant_mode_family: Some("bridge".to_string()), + dominant_mode_family_count: 18, + mode_family_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "bridge".to_string(), + count: 18, + }, + ], + sample_rows: Vec::new(), + }, + ], + profile_span_correlations: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { + previous_profile_chunk_len_to_next_name_or_end: 3, + row_count: 17, + dominant_child_count_candidate: Some(1), + dominant_child_count_candidate_count: 17, + dominant_saved_primary_child_byte_candidate: Some(0xff), + dominant_saved_primary_child_byte_candidate_hex: Some( + "0xff".to_string(), + ), + dominant_saved_primary_child_byte_candidate_count: 17, + dominant_candidate_pattern: Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff" + .to_string(), + count: 17, + }, + ), + dominant_mode_family: Some("tunnel".to_string()), + dominant_mode_family_count: 15, + mode_family_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "track_cap".to_string(), + count: 2, + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "tunnel".to_string(), + count: 15, + }, + ], + compact_prefix_pattern_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { + prefix_leading_dword: 0x0000_55f3, + prefix_leading_dword_hex: "0x000055f3".to_string(), + prefix_trailing_word: 0x0001, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + count: 17, + }, + ], + sample_rows: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { + sample_index: 0, + name_tag_relative_offset: 1200, + primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + prefix_leading_dword: 0x0000_55f3, + prefix_leading_dword_hex: "0x000055f3".to_string(), + prefix_trailing_word: 0x0001, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + }, + ], + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { + previous_profile_chunk_len_to_next_name_or_end: 6, + row_count: 72, + dominant_child_count_candidate: Some(1), + dominant_child_count_candidate_count: 62, + dominant_saved_primary_child_byte_candidate: Some(0xff), + dominant_saved_primary_child_byte_candidate_hex: Some( + "0xff".to_string(), + ), + dominant_saved_primary_child_byte_candidate_count: 72, + dominant_candidate_pattern: Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff" + .to_string(), + count: 62, + }, + ), + dominant_mode_family: Some("bridge".to_string()), + dominant_mode_family_count: 72, + mode_family_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "bridge".to_string(), + count: 72, + }, + ], + compact_prefix_pattern_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { + prefix_leading_dword: 0xff00_0000, + prefix_leading_dword_hex: "0xff000000".to_string(), + prefix_trailing_word: 0x0001, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + count: 72, + }, + ], + sample_rows: vec![], + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { + previous_profile_chunk_len_to_next_name_or_end: 0x27, + row_count: 3, + dominant_child_count_candidate: Some(1), + dominant_child_count_candidate_count: 3, + dominant_saved_primary_child_byte_candidate: Some(0xff), + dominant_saved_primary_child_byte_candidate_hex: Some( + "0xff".to_string(), + ), + dominant_saved_primary_child_byte_candidate_count: 3, + dominant_candidate_pattern: Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff" + .to_string(), + count: 3, + }, + ), + dominant_mode_family: Some("bridge".to_string()), + dominant_mode_family_count: 2, + mode_family_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "bridge".to_string(), + count: 2, + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "tunnel".to_string(), + count: 1, + }, + ], + compact_prefix_pattern_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { + prefix_leading_dword: 0xff00_00ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 0x0001, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + count: 1, + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { + prefix_leading_dword: 0xff00_00ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 0x0002, + prefix_trailing_word_hex: "0x0002".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + count: 2, + }, + ], + sample_rows: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { + sample_index: 0, + name_tag_relative_offset: 2805, + primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + prefix_leading_dword: 0xff00_00ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 0x0001, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { + sample_index: 1, + name_tag_relative_offset: 3764, + primary_name: Some("BridgeSTWood_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + prefix_leading_dword: 0xff00_00ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 0x0002, + prefix_trailing_word_hex: "0x0002".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + }, + ], + }, + ], + compact_prefix_correlations: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { + prefix_leading_dword: 0x0000_55f3, + prefix_leading_dword_hex: "0x000055f3".to_string(), + prefix_trailing_word: 0x0001, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + row_count: 17, + unique_name_pair_count: 2, + unique_profile_span_count: 1, + dominant_primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + dominant_secondary_name: Some("Infrastructure".to_string()), + dominant_name_pair_count: 15, + dominant_profile_span: Some(3), + dominant_profile_span_count: 17, + dominant_candidate_pattern: Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + count: 17, + }, + ), + dominant_mode_family: Some("tunnel".to_string()), + dominant_mode_family_count: 15, + mode_family_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "track_cap".to_string(), + count: 2, + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "tunnel".to_string(), + count: 15, + }, + ], + name_pair_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { + primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + count: 15, + }, + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { + primary_name: Some("TrackCapST_Cap.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + count: 2, + }, + ], + profile_span_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { + previous_profile_chunk_len_to_next_name_or_end: 3, + count: 17, + }, + ], + rows_with_previous_short_profile_flag_pair: 17, + previous_short_profile_flag_pair_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { + first_flag_byte: 0x00, + first_flag_byte_hex: "0x00".to_string(), + second_flag_byte: 0x01, + second_flag_byte_hex: "0x01".to_string(), + count: 17, + }, + ], + sample_rows: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { + sample_index: 0, + name_tag_relative_offset: 1200, + primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + previous_profile_chunk_len_to_next_name_or_end: Some(3), + }, + ], + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { + prefix_leading_dword: 0xff00_00ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 0x0001, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + row_count: 1, + unique_name_pair_count: 1, + unique_profile_span_count: 1, + dominant_primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + dominant_secondary_name: Some("Infrastructure".to_string()), + dominant_name_pair_count: 1, + dominant_profile_span: Some(0x27), + dominant_profile_span_count: 1, + dominant_candidate_pattern: Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + count: 1, + }, + ), + dominant_mode_family: Some("tunnel".to_string()), + dominant_mode_family_count: 1, + mode_family_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "tunnel".to_string(), + count: 1, + }, + ], + name_pair_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { + primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + count: 1, + }, + ], + profile_span_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { + previous_profile_chunk_len_to_next_name_or_end: 0x27, + count: 1, + }, + ], + rows_with_previous_short_profile_flag_pair: 1, + previous_short_profile_flag_pair_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { + first_flag_byte: 0x00, + first_flag_byte_hex: "0x00".to_string(), + second_flag_byte: 0x01, + second_flag_byte_hex: "0x01".to_string(), + count: 1, + }, + ], + sample_rows: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { + sample_index: 0, + name_tag_relative_offset: 2805, + primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + previous_profile_chunk_len_to_next_name_or_end: Some(0x27), + }, + ], + }, + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { + prefix_leading_dword: 0xff00_00ff, + prefix_leading_dword_hex: "0xff0000ff".to_string(), + prefix_trailing_word: 0x0002, + prefix_trailing_word_hex: "0x0002".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + row_count: 2, + unique_name_pair_count: 1, + unique_profile_span_count: 1, + dominant_primary_name: Some("BridgeSTWood_Section.3dp".to_string()), + dominant_secondary_name: Some("Infrastructure".to_string()), + dominant_name_pair_count: 2, + dominant_profile_span: Some(0x27), + dominant_profile_span_count: 2, + dominant_candidate_pattern: Some( + SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + count: 2, + }, + ), + dominant_mode_family: Some("bridge".to_string()), + dominant_mode_family_count: 2, + mode_family_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { + mode_family: "bridge".to_string(), + count: 2, + }, + ], + name_pair_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { + primary_name: Some("BridgeSTWood_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + count: 2, + }, + ], + profile_span_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { + previous_profile_chunk_len_to_next_name_or_end: 0x27, + count: 2, + }, + ], + rows_with_previous_short_profile_flag_pair: 2, + previous_short_profile_flag_pair_counts: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { + first_flag_byte: 0x00, + first_flag_byte_hex: "0x00".to_string(), + second_flag_byte: 0x01, + second_flag_byte_hex: "0x01".to_string(), + count: 2, + }, + ], + sample_rows: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { + sample_index: 0, + name_tag_relative_offset: 3764, + primary_name: Some("BridgeSTWood_Section.3dp".to_string()), + secondary_name: Some("Infrastructure".to_string()), + child_count_candidate: 1, + child_count_candidate_hex: "0x0001".to_string(), + saved_primary_child_byte_candidate: 0xff, + saved_primary_child_byte_candidate_hex: "0xff".to_string(), + previous_profile_chunk_len_to_next_name_or_end: Some(0x27), + }, + ], + }, + ], + sample_rows: Vec::new(), + }, + ), + dominant_profile_span_class_summary: None, + sample_rows: Vec::new(), + }, + ), + live_entry_prelude_summary: Some( + SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSummary { + live_entry_directory_row_count: 3865, + decoded_live_entry_id_count: 3865, + payload_relative_offset_monotonic: true, + rows_with_payload_pointer_inside_records_span: 138, + rows_with_zero_child_count: 0, + rows_with_nonzero_child_count: 138, + rows_with_first_name_tag_after_prelude: 138, + rows_with_first_name_tag_at_offset_3: 138, + unique_child_count_values: vec![1], + unique_first_name_tag_relative_offsets: vec![3], + dominant_child_count: Some(1), + dominant_child_count_count: 138, + dominant_saved_primary_child_byte: Some(0), + dominant_saved_primary_child_byte_hex: Some("0x00".to_string()), + dominant_saved_primary_child_byte_count: 138, + dominant_first_name_tag_relative_offset: Some(3), + dominant_first_name_tag_relative_offset_count: 138, + sample_rows: Vec::new(), + }, + ), + evidence: Vec::new(), + }); + analysis.placed_structure_dynamic_side_buffer_alignment = + Some(SmpSavePlacedStructureDynamicSideBufferAlignmentProbe { + unique_side_buffer_name_pair_count: 5, + unique_triplet_name_pair_count: 56, + overlapping_name_pair_count: 0, + side_buffer_row_count: 138, + side_buffer_rows_with_matching_triplet_name_pair_count: 0, + side_buffer_rows_without_matching_triplet_name_pair_count: 138, + triplet_name_pairs_without_side_buffer_match_count: 56, + matched_name_pair_samples: Vec::new(), + unmatched_side_buffer_name_pair_samples: Vec::new(), + evidence: Vec::new(), + }); + + let trace = build_infrastructure_asset_trace_report(&analysis); + assert!(trace.side_buffer_present); + assert_eq!(trace.triplet_alignment_overlap_count, 0); + assert_eq!(trace.known_owner_bridge_fields.len(), 7); + assert_eq!(trace.known_bridge_helpers.len(), 21); + assert_eq!(trace.next_owner_questions.len(), 4); + assert!(trace.next_owner_questions.iter().any(|line| { + line.contains("compact-prefix regimes subdivide") + && line.contains("0x0a BallastCap") + && line.contains("0x0b TrackCap") + && line.contains("0x02 Tunnel") + && line.contains("0x01 Bridge") + })); + assert!(trace.next_owner_questions.iter().any(|line| { + line.contains("direct route-entry bridge helpers") + && line.contains("0x00448a70/0x00493660/0x0048b660") + && line.contains("[this+0x248]") + && line.contains("cache/cleanup state") + })); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004a2c80/0x004a34e0") + && line.contains("paired upstream infrastructure composition choosers") + && line.contains("BallastCap/Overpass")) + ); + assert!(trace.known_bridge_helpers.iter().any( + |line| line.contains("0x0048a340") && line.contains("[0x226]/[0x219]/[0x251]/bit 0x20") + )); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00455a40") && line.contains("slot +0x44")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004559d0") && line.contains("0x55f1/0x55f2/0x55f3")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00490960") && line.contains("selector propagator")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00490200") && line.contains("route/link comparator")) + ); + assert_eq!(trace.candidate_consumer_hypotheses.len(), 3); + assert_eq!( + trace.candidate_consumer_hypotheses[0].status, + "highest_priority_static_mapping_target" + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x004a2c80 routes the DT family") + && line.contains("0x004a34e0 routes the ST family") + && line.contains("0x0048a340/0x0048f4c0/0x00490200/0x00490960")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x621a44/0x621a54 feed BridgeST") + && line.contains("0x621a94 feeds TunnelDT variants") + && line.contains("BallastCapDT/ST") + && line.contains("OverpassDT/ST")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("[this+0x226]==1 routes bridge families") + && line.contains("[this+0x226]==2 routes tunnel families") + && line.contains("[this+0x226]==3 routes overpass/ballast families") + && line.contains("bit 0x20 in [this+0x24c]") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("0x0048a340 as the exact chooser-state setter") + && line.contains("[this+0x226]") + && line.contains("[this+0x219]") + && line.contains("[this+0x251]") + && line.contains("[this+0x24c]") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("[this+0x219] indexes Steel/Stone/Wood tables") + && line.contains("value 2 takes the special suspension-cap path") + && line.contains("[this+0x251] selects Brick versus Concrete") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("[this+0x252]") + && line.contains("R10, L10, 12, 14, 16, and 18") + && line.contains("BridgeDT/BridgeST suspension-cap literals")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .blockers + .iter() + .any( + |line| line.contains("remaining mixed exact compact-prefix classes") + && line.contains("0xff0000ff/0x0002/0xff is pure bridge") + && line.contains("0x0005d368/0x0001/0xff is pure track-cap") + && line.contains("0xff0000ff/0x0001/0xff") + && line.contains("0x000055f3/0x0001/0xff") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains( + "profile-span mode-family correlations now also split the previous 0x55f3 spans directly" + ) && line.contains("span=0x6 rows=72") + && line.contains("span=0x3 rows=17") + && line.contains("span=0x27 rows=3")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains( + "exact compact-prefix correlations now split the residual prelude classes directly" + ) && line.contains("0x000055f3/0x0001/0xff") + && line.contains("0xff0000ff/0x0001/0xff") + && line.contains("0xff0000ff/0x0002/0xff")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("short 0x03-byte post-profile gaps") + && line.contains("track_cap:2") + && line.contains("tunnel:15") + && line.contains("0x000055f3/0x0001/0xff:17")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("sparse 0x27 post-profile outlier") + && line.contains("0xff0000ff/0x0001/0xff:1") + && line.contains("0xff0000ff/0x0002/0xff:2") + && line.contains("TunnelSTBrick_Section.3dp") + && line.contains("BridgeSTWood_Section.3dp")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("bridge-only two-child class is now grounded save-side") + && line.contains("0x0002") + && line.contains("BridgeSTWood_Section.3dp") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line + .contains("0xff0000ff/0x0001/0xff compact-prefix class is now explicit") + && line.contains("dominant prelude=0x0001/0xff")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line + .contains("0x000055f3/0x0001/0xff compact-prefix class is now explicit") + && line.contains("dominant prelude=0x0001/0xff")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line + .contains("0xff0000ff/0x0002/0xff compact-prefix class is now explicit") + && line.contains("dominant prelude=")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("slot +0x44 = 0x004559d0") + && line.contains("+0x40 = 0x00455fc0") + && line.contains("+0x4c = 0x00455930")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x004559d0 writing 0x55f1") + && line.contains("[this+0x206/+0x20a/+0x20e]") + && line.contains("0x52ec50") + && line.contains("0x55f3")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("chooser siblings calling 0x00490960 directly") + && line.contains("0x0048a340") + && line.contains("0x0048f4c0") + && line.contains("0x00490200") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("0x00490960 copying selector fields into the child object") + && line.contains("0x00455b70") + && line.contains("0x005cfd74") + && line.contains("[this+0x248]") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00490200 reading the seeded lanes") + && line.contains("0x006cfca8") + && line.contains("[this+0x216/+0x218/+0x201/+0x202]")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x0048e140/0x0048e160/0x0048e180") + && line.contains("[this+0x206/+0x20a/+0x20e]") + && line.contains("0x0048e1a0") + && line.contains("[this+0x202]")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x0048ed30") + && line.contains("[this+0x248]") + && line.contains("[this+0x08]") + && line.contains("0x455d20/0x455650/0x53b080")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x004a2eba/0x004a30f9/0x004a339c") + && line.contains("0x005cb138 = BallastCapDT_Cap.3dp") + && line.contains("0x004909e2")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("mode 0x0b") + && line.contains("TrackCapDT/ST_Cap") + && line.contains("mode 0x03") + && line.contains("OverpassST_section") + && line.contains("mode 0x02") + && line.contains("mode 0x01")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("objdump on 0x00490960") + && line.contains("stem at [esp+0x14]") + && line.contains("[esp+0x18]/[esp+0x1c] feed 0x539530") + && line.contains("[esp+0x20] feeds 0x53a5b0") + && line.contains("[esp+0x34] gates whether the new child is cached") + && line.contains("selector-copy block") + && line.contains("[esp+0x28]/[esp+0x2c]/[esp+0x30]") + && line.contains("0x0048ed01/0x0048ed20") + && line.contains("bypass") + && line.contains("0x004a17eb/0x004a1995/0x004a1b44/0x004a1b7d") + && line.contains("0x004a1b95") + && line.contains("arg7/arg8/arg9 = -1/-1/0") + && line.contains("arg8 fixed at 1") + && line.contains("arg9 fixed at 0")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x004a17eb/0x004a1995") + && line.contains("0x621a94/0x621a64") + && line.contains("one-bit selector (0 or 1)") + && line.contains("0x004a1b44/0x004a1b7d") + && line.contains("0x621a9c/0x621a6c") + && line.contains("0x004a1b95") + && line.contains("0x0048ed01/0x0048ed20") + && line.contains("0x005cb198 versus 0x005cb1ac")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("objdump on 0x00455b70") + && line.contains("[this+0x206/+0x20a/+0x20e]") + && line.contains("0x51d820") + && line.contains("0x005c87a8") + && line.contains("0x005cfd74 = \"Infrastructure\"")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("objdump on 0x51d820") + && line.contains("owned heap strings") + && line.contains("0x5a1145") + && line.contains("0x5a125d") + && line.contains("byte-for-byte")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("objdump on 0x52ec50") + && line.contains("bit 5 of [this+0x20]") + && line.contains("bit 6 of [this+0x20]") + && line.contains("0x531030")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("objdump on 0x531030/0x5a464d/0x5a44a8") + && line.contains("0x531030 just forwards") + && line.contains("0x5a44a8 is the shared chunked stream write path") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("cannot come from selector-copy state alone") + && line.contains("0xff0000ff/0x0001/0xff") + && line.contains("dominant TrackCap rows") + && line.contains("tunnel residue") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any( + |line| line.contains("BridgeSTWood_Section.3dp aligns with mode 0x01 Bridge") + && line.contains("TunnelSTBrick_Cap/Section.3dp with mode 0x02 Tunnel") + && line.contains("BallastCapST_Cap.3dp with mode 0x0a BallastCap") + && line.contains("TrackCapST_Cap.3dp with mode 0x0b TrackCap") + ) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains( + "mode-family correlations now also split the candidate patterns directly" + ) && line.contains("0x0002/0xff rows=18") + && line.contains("bridge")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| { + line.contains("0x00518140") + && line.contains("12-byte row") + && line.contains("[collection+0x3c]") + }) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00518380") && line.contains("ordinal")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x005181f0/0x00518260") + && line.contains("previous live id")) + ); + assert!(trace.notes.iter().any(|line| { + line.contains("pure bridge-only 0x0002/0xff candidate class is grounded save-side") + && line.contains("paired DT/ST siblings at 0x004a2c80 and 0x004a34e0") + && line.contains("grounded top-level branch meaning") + && line.contains("grounded bridge/tunnel material selector roles") + && line.contains("concrete child-construction/write-side chain through 0x00490960") + && line.contains("0x004559d0") + })); + assert!(trace.notes.iter().any(|line| line.contains("ST-only") + && line.contains("ST chooser sibling") + && line.contains("DT sibling remains grounded statically"))); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| { + line.contains("0x52ebd0/0x52ec50") + && (line.contains("bits 0x20/0x40") || line.contains("bits 0x20 and 0x40")) + }) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| { + (line.contains("0x455870/0x455930") + || (line.contains("0x455870") && line.contains("0x455930"))) + && (line.contains("six 4-byte lanes") || line.contains("six dword lanes")) + }) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| { + line.contains("0x530720") + && line.contains("0x52e8b0") + && line.contains("[this+0x4b/+0x4f/+0x53]") + }) + ); + assert!( + trace.candidate_consumer_hypotheses[2] + .evidence + .iter() + .any(|line| line.contains("0x00448a70") + && line.contains("[world+0x15e1/+0x162d]") + && line.contains("0x00448af0") + && line.contains("[world+0x2139/+0x213d/+0x2141]")) + ); + assert!( + trace.candidate_consumer_hypotheses[2] + .evidence + .iter() + .any(|line| line.contains("0x00493660") + && line.contains("[child+0x218]") + && line.contains("[child+0x226]") + && line.contains("0x006cfc9c") + && line.contains("0x487960")) + ); + assert!( + trace.candidate_consumer_hypotheses[2] + .evidence + .iter() + .any(|line| line.contains("0x0048b660") + && line.contains("[child+0x216]") + && line.contains("[child+0x201]") + && line.contains("0x53a350")) + ); + assert!( + trace.candidate_consumer_hypotheses[2] + .evidence + .iter() + .any(|line| line.contains("0x0048e2c0") + && line.contains("bit 0x20 in [child+0x201]") + && line.contains("0x53a3a0") + && line.contains("0x48a9e0") + && line.contains("0x0048e3c0") + && line.contains("[child+0x22e]") + && line.contains("0x006cfcb4") + && line.contains("bit 0x02 in [child+0x24c]")) + ); + assert!( + trace.candidate_consumer_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("u16 child count") + && line.contains("saved primary-child byte")) + ); + assert_eq!(trace.branches[0].status, "grounded_separate_owner_seam"); + assert_eq!(trace.branches[1].status, "disproved_by_grounded_probe"); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/services/mod.rs b/crates/rrt-runtime/src/inspect/smp/tests/services/mod.rs new file mode 100644 index 0000000..bb3ed46 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/services/mod.rs @@ -0,0 +1,8 @@ +use super::*; + +pub(super) use crate::inspect::smp::services::*; + +mod infrastructure; +mod periodic_company; +mod region; +mod shared; diff --git a/crates/rrt-runtime/src/inspect/smp/tests/services/periodic_company.rs b/crates/rrt-runtime/src/inspect/smp/tests/services/periodic_company.rs new file mode 100644 index 0000000..85ea5d1 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/services/periodic_company.rs @@ -0,0 +1,1838 @@ +use super::*; + +#[test] +fn builds_periodic_company_service_trace_report_with_candidate_consumers() { + let mut analysis = empty_analysis_report(); + analysis.selected_company_id = Some(7); + analysis.placed_structure_record_triplets = Some(SmpSavePlacedStructureRecordTripletProbe { + profile_family: analysis.profile_family.clone(), + source_kind: "save-placed-structure-record-triplets".to_string(), + semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), + records_tag_offset: 0, + close_tag_offset: 0, + record_count: 2, + entries: vec![ + SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: 0, + primary_name: "StationA".to_string(), + secondary_name: "StationSetA".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0, + profile_tag_relative_offset: 0, + policy_chunk_len: 0x1a, + profile_chunk_len: 0x20, + policy_f32_lane_0: 0.0, + policy_f32_lane_1: 0.0, + policy_f32_lane_2: 0.0, + policy_f32_lane_3: 0.0, + policy_f32_lane_4: 0.0, + policy_reserved_dword: 0, + policy_trailing_word: 0x0101, + policy_trailing_word_hex: "0x0101".to_string(), + profile_open_marker: 0x5dc1, + profile_open_marker_hex: "0x00005dc1".to_string(), + profile_repeated_primary_name: "StationA".to_string(), + profile_repeated_secondary_name: "StationSetA".to_string(), + profile_footer_relative_offset: 0, + profile_footer_relative_offset_hex: "0x0".to_string(), + profile_pre_footer_padding_len: 1, + profile_pre_footer_padding_hex_bytes: vec!["0x01".to_string()], + profile_companion_byte_u8: Some(1), + profile_companion_byte_hex: Some("0x01".to_string()), + profile_payload_dword: 0x0e373880, + profile_payload_dword_hex: "0x0e373880".to_string(), + profile_sentinel_i32: -1, + profile_status_kind: "unset".to_string(), + farm_growth_stage_index: None, + profile_close_marker: 0x5dc2, + profile_close_marker_hex: "0x00005dc2".to_string(), + }, + SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: 1, + primary_name: "StationB".to_string(), + secondary_name: "StationSetB".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0, + profile_tag_relative_offset: 0, + policy_chunk_len: 0x1a, + profile_chunk_len: 0x20, + policy_f32_lane_0: 0.0, + policy_f32_lane_1: 0.0, + policy_f32_lane_2: 0.0, + policy_f32_lane_3: 0.0, + policy_f32_lane_4: 0.0, + policy_reserved_dword: 0, + policy_trailing_word: 0x0101, + policy_trailing_word_hex: "0x0101".to_string(), + profile_open_marker: 0x5dc1, + profile_open_marker_hex: "0x00005dc1".to_string(), + profile_repeated_primary_name: "StationB".to_string(), + profile_repeated_secondary_name: "StationSetB".to_string(), + profile_footer_relative_offset: 0, + profile_footer_relative_offset_hex: "0x0".to_string(), + profile_pre_footer_padding_len: 1, + profile_pre_footer_padding_hex_bytes: vec!["0x00".to_string()], + profile_companion_byte_u8: Some(0), + profile_companion_byte_hex: Some("0x00".to_string()), + profile_payload_dword: 0x0e373500, + profile_payload_dword_hex: "0x0e373500".to_string(), + profile_sentinel_i32: -1, + profile_status_kind: "unset".to_string(), + farm_growth_stage_index: None, + profile_close_marker: 0x5dc2, + profile_close_marker_hex: "0x00005dc2".to_string(), + }, + ], + evidence: Vec::new(), + }); + analysis.region_fixed_row_run_candidates = Some(SmpSaveRegionFixedRowRunCandidateProbe { + profile_family: analysis.profile_family.clone(), + source_kind: "save-region-fixed-row-run-candidates".to_string(), + semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), + target_row_count: 4, + target_row_stride: 0xbc, + target_row_stride_hex: "0xbc".to_string(), + scan_start_offset: 0, + scan_start_offset_hex: "0x0".to_string(), + scan_end_offset: 0x2000, + scan_end_offset_hex: "0x2000".to_string(), + candidates: vec![SmpSaveRegionFixedRowRunCandidate { + count_offset: 0x40, + count_offset_hex: "0x40".to_string(), + row_count: 4, + row_stride: 0xbc, + row_stride_hex: "0xbc".to_string(), + rows_offset: 0x5310, + rows_offset_hex: "0x5310".to_string(), + rows_end_offset: 0x5600, + rows_end_offset_hex: "0x5600".to_string(), + distance_to_region_metadata_tag: 0x80, + distance_to_region_metadata_tag_hex: "0x80".to_string(), + dword_lane_summaries: Vec::new(), + shape_signature: "shape-a".to_string(), + shape_family_signature: "family-a".to_string(), + trailing_byte_zero_count: 4, + trailing_byte_nonzero_count: 0, + trailing_byte_distinct_value_count: 1, + trailing_byte_sample_values_hex: vec!["0x00".to_string()], + best_probable_density_lane_relative_offset_hex: Some("0x24".to_string()), + }], + evidence: Vec::new(), + }); + analysis + .company_entries + .push(SmpSaveCompanyRecordAnalysisEntry { + company_id: 7, + name: "Test Company".to_string(), + active: true, + linked_chairman_profile_id: Some(3), + outstanding_shares: 1000, + debt: 0, + bond_count: 0, + live_bond_slots: Vec::new(), + largest_live_bond_principal: None, + highest_coupon_live_bond_principal: None, + available_track_laying_capacity: Some(5), + company_value_scalar_f32: 1.0, + cached_share_support_scalar_f32: 1.0, + cached_share_price_f32: 1.0, + chairman_salary_baseline: 0, + chairman_salary_current: 0, + chairman_bonus_year: 0, + chairman_bonus_amount: 0, + founding_year: 1900, + last_bankruptcy_year: 0, + last_dividend_year: 0, + preferred_locomotive_engine_type_raw_u8: 2, + preferred_locomotive_engine_type_raw_hex: "0x02".to_string(), + city_connection_latch: true, + linked_transit_latch: false, + linked_transit_autoroute_site_score_cache_refresh_absolute_counter: 0x31380, + linked_transit_site_peer_cache_refresh_absolute_counter: 0x7ff80, + linked_transit_route_anchor_entry_id: Some(77), + linked_transit_route_anchor_fallback_counts: vec![3, 5, 8], + merger_cooldown_year: 0, + takeover_cooldown_year: 0, + scalar_dword_candidates: Vec::new(), + post_capacity_dword_candidates: Vec::new(), + stat_band_root_0cfb_candidates: Vec::new(), + stat_band_root_0d7f_candidates: Vec::new(), + stat_band_root_1c47_candidates: Vec::new(), + }); + + let trace = build_periodic_company_service_trace_report(&analysis); + assert_eq!(trace.selected_company_id, Some(7)); + assert_eq!(trace.atlas_candidate_consumers.len(), 9); + assert_eq!(trace.known_bridge_helpers.len(), 84); + assert_eq!(trace.next_owner_questions.len(), 5); + assert_eq!( + trace.linked_transit_shellless_readiness_status, + "timed_cache_and_train_side_followons_grounded_site_cache_input_owners_missing" + ); + assert_eq!( + trace.linked_transit_minimum_persisted_identity_inputs.len(), + 5 + ); + assert_eq!(trace.linked_transit_live_rebuilt_cache_lanes.len(), 5); + assert_eq!(trace.linked_transit_runtime_backed_input_families.len(), 18); + assert_eq!(trace.linked_transit_remaining_owner_gaps.len(), 2); + assert_eq!(trace.companies.len(), 1); + assert_eq!( + trace.companies[0].linked_transit_autoroute_site_score_cache_refresh_absolute_counter, + 0x31380 + ); + assert_eq!( + trace.companies[0].linked_transit_site_peer_cache_refresh_absolute_counter, + 0x7ff80 + ); + assert_eq!( + trace.peer_site_selector_candidate_owner_strip, + "0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70" + ); + assert_eq!( + trace.peer_site_selector_candidate_persisted_tag_hex, + "0x5dc1" + ); + assert_eq!( + trace.peer_site_selector_candidate_selector_lane, + "[owner+0x23e]" + ); + assert_eq!( + trace.peer_site_selector_candidate_secondary_payload_lane, + "[owner+0x242]" + ); + assert!( + trace + .peer_site_selector_candidate_post_secondary_byte_status + .contains("post-secondary discriminator byte") + ); + assert_eq!( + trace.peer_site_selector_candidate_class_identity_status, + "grounded_direct_local_helper_strip" + ); + assert_eq!(trace.peer_site_selector_candidate_helper_linkage.len(), 4); + assert_eq!( + trace + .peer_site_selector_candidate_saved_payload_summaries + .len(), + 2 + ); + assert_eq!( + trace.peer_site_selector_candidate_saved_payload_summaries[0].profile_payload_dword_hex, + "0x0e373500" + ); + assert_eq!( + trace.peer_site_selector_candidate_saved_payload_summaries[0].count, + 1 + ); + assert_eq!( + trace.peer_site_selector_candidate_saved_payload_delta_summaries[0].delta_hex, + "0x00000380" + ); + assert_eq!( + trace.peer_site_selector_candidate_saved_footer_padding_summaries[0].padding_len, + 1 + ); + assert_eq!( + trace.peer_site_selector_candidate_saved_companion_byte_summaries[0].companion_byte_hex, + "0x00" + ); + assert_eq!( + trace.peer_site_selector_candidate_saved_policy_trailing_word_summaries[0] + .policy_trailing_word_hex, + "0x0101" + ); + assert_eq!( + trace.peer_site_selector_candidate_saved_policy_trailing_word_summaries[0].count, + 2 + ); + assert_eq!( + trace.peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries[0] + .primary_name, + "StationA" + ); + assert!( + trace + .peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries + .is_empty() + ); + assert_eq!(trace.peer_site_persisted_selector_bundle_fields.len(), 4); + assert_eq!(trace.peer_site_rebuilt_transient_followon_fields.len(), 4); + assert_eq!( + trace.peer_site_shellless_minimum_persisted_identity_status, + "name_pair_and_post_secondary_byte_minimum_identity_subset_child_runtime_bundle_rebuild_followon" + ); + assert_eq!( + trace + .peer_site_shellless_minimum_persisted_identity_inputs + .len(), + 5 + ); + assert_eq!(trace.peer_site_restore_input_fields.len(), 5); + assert_eq!(trace.peer_site_runtime_input_fields.len(), 3); + assert_eq!( + trace.peer_site_runtime_reconstruction_status, + "restore_subset_and_bring_up_reconstruct_runtime_subset" + ); + assert_eq!(trace.peer_site_runtime_reconstruction_steps.len(), 4); + assert_eq!(trace.near_city_acquisition_region_input_fields.len(), 5); + assert_eq!(trace.near_city_acquisition_peer_input_fields.len(), 7); + assert_eq!(trace.near_city_acquisition_company_input_fields.len(), 6); + assert_eq!( + trace.near_city_acquisition_shellless_readiness_status, + "peer_and_company_inputs_grounded_runtime_projection_and_periodic_service_present" + ); + assert_eq!( + trace.near_city_acquisition_site_owner_company_projection_status, + "best_effort_runtime_projection_from_save_collections_and_company_latches" + ); + assert_eq!( + trace.near_city_acquisition_site_self_id_projection_status, + "live_meaning_grounded_reconstructible_from_collection_identity" + ); + assert_eq!( + trace.near_city_acquisition_site_cached_tri_lane_projection_status, + "best_effort_runtime_projection_from_side_buffer_and_region_row_signatures" + ); + assert_eq!( + trace.near_city_acquisition_nontransport_persisted_source_status, + "best_effort_runtime_projection_from_company_market_and_name_pair_alignment" + ); + assert_eq!( + trace + .near_city_acquisition_nontransport_persisted_source_candidates + .len(), + 5 + ); + assert_eq!( + trace.near_city_acquisition_tri_lane_save_shape_family_status, + "save_shape_family_candidates_present_fixed_offset_ruled_down" + ); + assert_eq!( + trace + .near_city_acquisition_tri_lane_save_shape_family_candidates + .len(), + 1 + ); + assert_eq!( + trace.near_city_acquisition_tri_lane_save_shape_family_candidates[0].shape_family_signature, + "family-a" + ); + assert_eq!( + trace.near_city_acquisition_tri_lane_live_service_status, + "candidate_gate_grounded_runtime_projection_and_periodic_commit_present" + ); + assert_eq!( + trace.near_city_acquisition_candidate_subtype_projection_status, + "cached_candidate_id_bridge_grounded_via_stream_load" + ); + assert_eq!( + trace.near_city_acquisition_backing_record_projection_status, + "stream_load_callback_grounded_via_0x40ce60" + ); + assert_eq!( + trace + .near_city_acquisition_tri_lane_live_owner_families + .len(), + 5 + ); + assert_eq!( + trace + .near_city_acquisition_tri_lane_candidate_gate_fields + .len(), + 5 + ); + assert_eq!( + trace + .near_city_acquisition_tri_lane_runtime_writer_roles + .len(), + 5 + ); + assert_eq!( + trace + .near_city_acquisition_tri_lane_direct_caller_families + .len(), + 5 + ); + assert_eq!( + trace + .near_city_acquisition_tri_lane_formula_input_lanes + .len(), + 5 + ); + assert_eq!(trace.near_city_acquisition_projection_hypotheses.len(), 3); + assert_eq!( + trace.near_city_acquisition_projection_hypotheses[0].label, + "site_owner_replay_from_post_load_refresh_self_id_reconstructible" + ); + assert_eq!( + trace.near_city_acquisition_projection_hypotheses[0].status, + "ordinary_replay_and_stream_load_ruled_down_tuple_finalize_positive_path_grounded" + ); + assert_eq!( + trace.near_city_acquisition_projection_hypotheses[1].label, + "site_cached_tri_lane_payload_or_restore_owner" + ); + assert_eq!( + trace.near_city_acquisition_projection_hypotheses[1].status, + "checked_in_save_seams_ruled_down_live_scoring_family_grounded_exact_semantics_open" + ); + assert_eq!( + trace.near_city_acquisition_projection_hypotheses[2].label, + "cached_source_candidate_id_to_subtype_projection" + ); + assert_eq!( + trace.near_city_acquisition_projection_hypotheses[2].status, + "grounded_stream_load_callback_0x40ce60" + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .candidate_consumers + .iter() + .any(|line| line.contains("0x004133b0")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .candidate_consumers + .iter() + .any(|line| line.contains("0x004134d0") && line.contains("0x0040f6d0")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .candidate_consumers + .iter() + .any(|line| line.contains("0x00403ef3 / 0x00404489")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .candidate_consumers + .iter() + .any(|line| line.contains("0x0046f073 / 0x004707ff")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00444690 -> 0x004133b0") + && line.contains("ordinary bring-up replay family")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00444467") + && line.contains("0x00413280") + && line.contains("0x004444d8") + && line.contains("0x00481210") + && line.contains("0x00444690")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("after 0x00444690 -> 0x004133b0") + && line.contains("0x004134d0 / 0x0040f6d0 / 0x0040ef10")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("[site+0x27a]") + && line.contains("0x0042125d") + && line.contains("0x0040f793") + && line.contains("0x0040dfec") + && line.contains("0x004269e4") + && line.contains("0x00426a44..0x00426a90") + && line.contains("0x00426ad8")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x0040ee10") + && line.contains("[site+0x3cc]") + && line.contains("0x0040e360..0x0040edf6") + && line.contains("[site+0x2a8]") + && line.contains("[site+0x2a4]") + && line.contains("[site+0x276]") + && line.contains("0x00480710") + && line.contains("0x00426b10") + && line.contains("0x00455860") + && line.contains("reads/queries")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x004134d0") + && line.contains("0x00518900") + && line.contains("0x0040f6d0") + && line.contains("[site+0x2a4]") + && line.contains("[site+0x276]")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x0040f6d0") + && line.contains("[site+0x2a8/+0x272/+0x27a/+0x29e]") + && line.contains("[site+0x3d4/+0x3d5]")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00403ef3 / 0x00404489") + && line.contains("0x0046f073 / 0x004707ff") + && line.contains("0x0040ef10")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("[+0x00/+0x04/+0x0c]") + && line.contains("0x0040ef1c") + && line.contains("0x0040f5d4") + && line.contains("[site+0x276]") + && line.contains("[site+0x27a]")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x004707ff") + && line.contains("0x004706b0") + && line.contains("selector-0x13") + && line.contains("0x004197e0 / 0x004134d0 / 0x0040eba0 / 0x0052eb90 / 0x0040ef10")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00472b40") + && line.contains("selector-0x72") + && line.contains("0x00472bef / 0x00472d03")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00422bb4") + && line.contains("0x0062b2fc") + && line.contains("literal flags 1/0") + && line.contains("out-param")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00508fd1 / 0x005098eb") + && line.contains("[this+0x7c]") + && line.contains("vtable slot +0x58 plus 0x00507cf0") + && line.contains("arg3 forced to zero")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00473c20") + && line.contains("0x006ce808..0x006ce988") + && line.contains("0x00473c98") + && line.contains("queued id slot")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x0042128d") + && line.contains("0x00422305") + && line.contains("0x004269c9/0x00426a2a") + && line.contains("0x004282a9 / 0x004300d6")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00413440") + && line.contains("0x36b1 / 0x36b2 / 0x36b3") + && line.contains("save side") + && line.contains("slot +0x44")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00413280") + && line.contains("slot +0x40") + && line.contains("0x0040ce60")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x0040f047") + && line.contains("0x0040f5d4") + && line.contains("[site+0x27a]")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .candidate_consumers + .iter() + .any(|line| line.contains("0x36b1/0x36b2/0x36b3")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .candidate_consumers + .iter() + .any(|line| line.contains("0x0040d450")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .candidate_consumers + .iter() + .any(|line| line.contains("0x00410b30..0x004118f4") && line.contains("0x00412560")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x00481430 -> 0x0047d8e0")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x0040c9a0")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x0040a3a1..0x0040a4d3") + && line.contains("0x0040fcc0..0x0040fe28") + && line.contains("0x00422c62..0x00422d3c")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x0040d4aa/0x0040d4b0") + && line.contains("0x0041114a7/0x004111572") + && line.contains("0x0041118aa/0x0041118f4")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x0040d450") + && line.contains("[site+0x276]") + && line.contains("0x00436590") + && line.contains("[site+0x310]")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x00410b30..0x004118f4") + && line.contains("0xbc-stride") + && line.contains("[site+0x310/+0x338/+0x360]")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x00412560") + && line.contains("0x006cec78") + && line.contains("0x0062ba8c")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[2] + .evidence + .iter() + .any(|line| line.contains("0x00413280") && line.contains("0x0040ce60")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[2] + .candidate_consumers + .iter() + .any(|line| line.contains("0x0040cd70 cached source/candidate resolver")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .blockers + .iter() + .any(|line| line.contains("0x00413440") + && line.contains("0x36b1/0x36b2/0x36b3") + && line.contains("load-save strip")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .blockers + .iter() + .any(|line| line.contains("0x00413280") + && line.contains("0x0040ce60") + && line.contains("stream-load bridge")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .blockers + .iter() + .any(|line| line.contains("0x00481210") + && line.contains("0x004133b0") + && line.contains("0x0040ef10")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .blockers + .iter() + .any(|line| line.contains("0x00431b20") + && line.contains("0x00433130") + && line.contains("0x0042db20") + && line.contains("0x0042e050") + && line.contains("0x0062be18") + && line.contains("[event+0x7ef]") + && line.contains("kind 8")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x00433130") + && line.contains("0x0062be18") + && line.contains("0x4e21/0x4e22") + && line.contains("0x004d90ba..0x004d91ed") + && line.contains("0x4e98..0x4ea2") + && line.contains("0x004d91b3")) + ); + assert!( + trace.near_city_acquisition_projection_hypotheses[0] + .evidence + .iter() + .any(|line| line.contains("0x004db02a") + && line.contains("0x004db1b8..0x004db309") + && line.contains("0x4e84") + && line.contains("0x004db9e5..0x004db9f1") + && line.contains("0x00432ea0")) + ); + assert_eq!( + trace + .near_city_acquisition_runtime_backed_input_families + .len(), + 25 + ); + assert_eq!(trace.near_city_acquisition_remaining_owner_gaps.len(), 2); + assert_eq!(trace.near_city_acquisition_region_lane_statuses.len(), 4); + assert!(trace.atlas_candidate_consumers.iter().any(|line| { + line.contains("0x00420030 / 0x00420280") + && line.contains("0x006cec20") + && line.contains("peer-site boolean/selector pair") + })); + assert!(trace.atlas_candidate_consumers.iter().any(|line| { + line.contains("0x0040dc40") && line.contains("linked-site mutation validator/apply owner") + })); + assert!(trace.atlas_candidate_consumers.iter().any(|line| { + line.contains("0x00402cb0") && line.contains("direct-placement builder owner") + })); + assert!(trace.next_owner_questions.iter().any(|line| { + line.contains("repopulates placed-structure owner-company field [site+0x276]") + && line.contains("0x00431b20") + })); + assert!(trace.next_owner_questions.iter().any(|line| { + line.contains("0x0040d450 / 0x00410b30..0x004118f4") + && line.contains("0x00412560") + && line.contains("[site+0x2b4/+0x2b8/+0x2bc]") + })); + assert!( + trace + .linked_transit_minimum_persisted_identity_inputs + .iter() + .any(|line| line.contains("[site+0x276]") + && line.contains("[site+0x04]") + && line.contains("0x0047efe0 / 0x0047fd50")) + ); + assert!( + trace + .linked_transit_minimum_persisted_identity_inputs + .iter() + .any(|line| line.contains("[site+0x2a4]") + && line.contains("[site+0x2a8]") + && line.contains("[peer+0x04/+0x08]")) + ); + assert!( + trace + .linked_transit_live_rebuilt_cache_lanes + .iter() + .any(|line| line.contains("0x004093d0") + && line.contains("[company+0x0d3e]") + && line.contains("+0x02/+0x06/+0x0a")) + ); + assert!( + trace + .linked_transit_live_rebuilt_cache_lanes + .iter() + .any(|line| line.contains("0x00407bd0") && line.contains("[site+0x0e/+0x12/+0x16]")) + ); + assert!( + trace + .linked_transit_live_rebuilt_cache_lanes + .iter() + .any(|line| line.contains("0x00481910") + && line.contains("0x004819b0") + && line.contains("0x004a9340")) + ); + assert!( + trace + .linked_transit_live_rebuilt_cache_lanes + .iter() + .any(|line| line.contains("0x004aee2b") + && line.contains("[site+0x5c5]") + && line.contains("[world+0x15]")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00429c10") + && line.contains("0x004093d0") + && line.contains("live company roster")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00444690") + && line.contains("0x004133b0") + && line.contains("0x0040ee10") + && line.contains("0x00480710") + && line.contains("0x004160aa")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("[company+0x0d3e]") && line.contains("0x00409720")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("[company+0x0d3a]") && line.contains("0x00407bd0")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0062ba8c") + && line.contains("0x0041f4e0") + && line.contains("0x0041ede0") + && line.contains("0x0041e970")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x004a6360") + && line.contains("0x004a6630") + && line.contains("0x00494fb0")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00408280") && line.contains("0x00408380")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00409770") && line.contains("0x00409830")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("[site+0x5bd]") + && line.contains("0x00407780") + && line.contains("0x004077e0")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("+0x00/+0x01") + && line.contains("+0x02") + && line.contains("+0x06") + && line.contains("+0x0a") + && line.contains("+0x0e/+0x12/+0x16")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("[site+0x5c1]") + && line.contains("0x00481910") + && line.contains("0x004a9340") + && line.contains("0x004819b0")) + ); + assert!( + trace + .linked_transit_runtime_backed_input_families + .iter() + .any(|line| line.contains("[site+0x5c5]") && line.contains("0x004aee2b")) + ); + assert!( + trace + .linked_transit_remaining_owner_gaps + .iter() + .any(|line| line.contains("0x0040e360..0x0040edf6") + && line.contains("[site+0x276]") + && line.contains("earlier restore or service owner")) + ); + assert!(trace.linked_transit_remaining_owner_gaps.iter().any( + |line| line.contains("0x006cec20") + && line.contains("0x0041f4e0") + && line.contains("0x00494fb0") + )); + assert!( + trace + .near_city_acquisition_region_input_fields + .iter() + .any(|line| line.contains("[site+0x276]")) + ); + assert!( + trace + .near_city_acquisition_peer_input_fields + .iter() + .any(|line| line.contains("[site+0x04]")) + ); + assert!( + trace + .near_city_acquisition_peer_input_fields + .iter() + .any(|line| line.contains("[cell+0xd4]") && line.contains("[cell+0xd6]")) + ); + assert!( + trace + .near_city_acquisition_company_input_fields + .iter() + .any(|line| line.contains("0x2329/0x0d")) + ); + assert!( + trace + .near_city_acquisition_company_input_fields + .iter() + .any(|line| line.contains("[company+0x0d35]") && line.contains("0x00401860")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("[company+0x0d35]") + && line.contains("[company+0x7664/+0x7668/+0x766c]")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x004134d0") + && line.contains("0x0040f6d0") + && line.contains("[site+0x2a4]") + && line.contains("[site+0x276]") + && line.contains("[site+0x3d4/+0x3d5]")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0040ef10") + && line.contains("0x00403ef3 / 0x00404489") + && line.contains("0x0046f073 / 0x004707ff")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0046f073 / 0x004707ff") + && line.contains("[+0x00/+0x04/+0x0c]") + && line.contains("0x0040ef10") + && line.contains("0x0040f5d4")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x004707ff") + && line.contains("0x004706b0") + && line.contains("selector-0x13")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00472b40") + && line.contains("selector-0x72") + && line.contains("0x00472bef / 0x00472d03")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00422bb4") + && line.contains("live args") + && line.contains("literal flags 1/0") + && line.contains("out-param")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00508fd1 / 0x005098eb") + && line.contains("[this+0x7c]") + && line.contains("0x0040eba0") + && line.contains("hard zero third arg")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00473c20") + && line.contains("0x006ce808..0x006ce988") + && line.contains("0x00473c98") + && line.contains("post-create refresh path")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0042128d") + && line.contains("0x00422305") + && line.contains("0x004269c9/0x00426a2a") + && line.contains("0x004282a9/0x004300d6")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00413440") + && line.contains("0x36b1/0x36b2/0x36b3") + && line.contains("slot +0x44")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0047efe0") && line.contains("[site+0x276]")) + ); + let linked_transit_branch = trace.companies[0] + .branches + .iter() + .find(|branch| branch.branch_name == "linked_transit_roster_maintenance") + .expect("linked-transit branch"); + assert_eq!( + linked_transit_branch.status, + "blocked_missing_site_cache_input_owner_mapping" + ); + assert!( + linked_transit_branch + .grounded_inputs + .iter() + .any(|line| line.contains("[company+0x0d3e]")) + ); + assert!( + linked_transit_branch + .grounded_inputs + .iter() + .any(|line| line.contains("[company+0x0d3a]")) + ); + assert!( + linked_transit_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x00409720")) + ); + assert!( + linked_transit_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x00408f70")) + ); + assert!( + linked_transit_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x00408280")) + ); + assert!( + linked_transit_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x00408380")) + ); + assert!( + linked_transit_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x00409770")) + ); + assert!( + linked_transit_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x00409830")) + ); + assert!( + linked_transit_branch + .blocking_inputs + .iter() + .any(|line| line.contains("0x00408280 / 0x00408380")) + ); + assert!(trace.notes.iter().any(|line| { + line.contains("0x00409720") + && line.contains("[company+0x0d3e]") + && line.contains("[company+0x0d3a]") + && line.contains("0x00409950") + })); + assert!(trace.notes.iter().any(|line| { + line.contains("0x00408280") + && line.contains("[site+0x16]") + && line.contains("0x00409770") + && line.contains("0x00409830") + })); + assert!(trace.notes.iter().any(|line| { + line.contains("0x00481910") + && line.contains("0x004819b0") + && line.contains("[site+0x5c1]") + && line.contains("0x004a9340") + && line.contains("0x004aee2b") + && line.contains("[site+0x5c5]") + })); + assert!(trace.notes.iter().any(|line| { + line.contains("0x00407780") + && line.contains("0x004077e0") + && line.contains("[site+0x5bd]") + && line.contains("0x1a-byte") + })); + assert!(trace.notes.iter().any(|line| { + line.contains("0x004093d0") + && line.contains("0x00407bd0") + && line.contains("+0x02") + && line.contains("+0x06") + && line.contains("+0x0a") + && line.contains("+0x0e/+0x12/+0x16") + })); + assert!(trace.notes.iter().any(|line| { + line.contains("0x0062ba8c") + && line.contains("0x0041f4e0") + && line.contains("0x00494fb0") + && line.contains("remaining linked-transit gap is narrower again") + })); + assert!(trace.notes.iter().any(|line| { + line.contains("0x00409720") + && line.contains("0x004093d0") + && line.contains("0x00407bd0") + && line.contains("0x00409950") + })); + assert!(trace.notes.iter().any(|line| { + line.contains("0x0040eba0") + && line.contains("[site+0x2a4]") + && line.contains("0x004814c0 / 0x00481480") + && line.contains("0x0042c9f0 / 0x0042c9a0") + })); + assert!(trace.notes.iter().any(|line| { + line.contains("0x0040ea96..0x0040eb65") + && line.contains("[site+0x276]") + && line.contains("consumes") + && line.contains("rather than rehydrating") + })); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0041f7e0 / 0x0041f810 / 0x0041f850") + && line.contains("[site+0x2a4]")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x004269b0") + && line.contains("0x0062b26c") + && line.contains("[site+0x2a4]")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any( + |line| line.contains("0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0") + && line.contains("[candidate+0x32]") + ) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0040dc40") + && line.contains("[site+0x276]") + && line.contains("0x0040d1f0 / 0x00480710")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line + .contains("0x00402cb0 -> 0x00403ed5/0x0040446b -> 0x004134d0 -> 0x0040ef10")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x00431b20") + && line.contains("0x0061039c") + && line.contains("0x00430040 / 0x00426d60 / 0x0042fc90")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0040d450") + && line.contains("[site+0x310]") + && line.contains("0x00410b30..0x004118f4") + && line.contains("0x00412560")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x0042bbf0") + && line.contains("0x0042bbb0") + && line.contains("0x0042c9f0") + && line.contains("0x0042c9a0")) + ); + assert!( + trace + .near_city_acquisition_runtime_backed_input_families + .iter() + .any(|line| line.contains("0x2329/0x0d")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_live_owner_families + .iter() + .any(|line| line.contains("0x0040d450") && line.contains("[site+0x310]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_live_owner_families + .iter() + .any(|line| line.contains("0x00410b30..0x004118f4") + && line.contains("[site+0x310/+0x338/+0x360]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_live_owner_families + .iter() + .any(|line| line.contains("0x00412560")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_candidate_gate_fields + .iter() + .any(|line| line.contains("[+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_candidate_gate_fields + .iter() + .any(|line| line.contains("0x006cec78")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_candidate_gate_fields + .iter() + .any(|line| line.contains("0x0062ba8c")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_candidate_gate_fields + .iter() + .any(|line| line.contains("vtable slot +0x80") && line.contains("[site+0x246]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_candidate_gate_fields + .iter() + .any(|line| line.contains("0x0040fb8d") + && line.contains("0x00410721") + && line.contains("0x004126d3")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_runtime_writer_roles + .iter() + .any(|line| line.contains("0x0040d450") && line.contains("[site+0x310]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_runtime_writer_roles + .iter() + .any(|line| line.contains("0x00410b30..0x004118f4") + && line.contains("[site+0x310/+0x338/+0x360]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_runtime_writer_roles + .iter() + .any(|line| line.contains("0x0041114a7/0x004111572") + && line.contains("0x0041114b7/0x004111582")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_runtime_writer_roles + .iter() + .any(|line| line.contains("0x0041118aa/0x0041118f4")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_runtime_writer_roles + .iter() + .any(|line| line.contains("0x0040c9a0") && line.contains("[site+0x2b4/+0x2b8/+0x2bc]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_direct_caller_families + .iter() + .any(|line| line.contains("0x0040fb70") + && line.contains("vtable slot +0x80") + && line.contains("owner-present flag") + && line.contains("0x00412560")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_direct_caller_families + .iter() + .any(|line| line.contains("0x004b4052 / 0x004b46ec") && line.contains("0x0062b26c")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_direct_caller_families + .iter() + .any(|line| line.contains("0x00401633") && line.contains("0x2329/0x0d")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_direct_caller_families + .iter() + .any(|line| line.contains("0x0044b81a") + && line.contains("0x00436590") + && line.contains("0x65")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_direct_caller_families + .iter() + .any(|line| line.contains("0x004b70f5 / 0x004b7979") + && line.contains("0x004337a0") + && line.contains("0x00540120 / 0x00518140")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_formula_input_lanes + .iter() + .any(|line| line.contains("[+0x20/+0x22]") + && line.contains("[+0x24/+0x28]") + && line.contains("[+0x44]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_formula_input_lanes + .iter() + .any(|line| line.contains("[world+0x0d]") && line.contains("[world+0x4afb]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_formula_input_lanes + .iter() + .any(|line| line.contains("0x00455810 / 0x00455800 / 0x0044ad60") + && line.contains("[site+0x276]") + && line.contains("0x66/0x68")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_formula_input_lanes + .iter() + .any(|line| line.contains("[+0x18/+0x1c/+0x2a/+0x2c/+0x44]") + && line.contains("[site+0x78c]") + && line.contains("[site+0x391]")) + ); + assert!( + trace + .near_city_acquisition_tri_lane_formula_input_lanes + .iter() + .any(|line| line.contains("[world+0x4caa]") + && line.contains("[company+0x0d5d]") + && line.contains("[site+0x310]") + && line.contains("[site+0x360]")) + ); + assert!( + trace + .near_city_acquisition_remaining_owner_gaps + .iter() + .any(|line| line.contains("[site+0x276]") + && line.contains("0x36b1/0x36b2/0x36b3") + && line.contains("0x0047efe0") + && line.contains("0x004134d0 / 0x0040f6d0") + && line.contains("0x0040ef10") + && line.contains("[+0x0c]") + && line.contains("best-effort owner projection")) + ); + assert!( + !trace + .near_city_acquisition_remaining_owner_gaps + .iter() + .any(|line| line.contains("[site+0x2a4]")) + ); + assert!( + trace + .near_city_acquisition_remaining_owner_gaps + .iter() + .any(|line| line.contains("0x0040d450") + && line.contains("0x00410b30..0x004118f4") + && line.contains("best-effort tri-lane projection")) + ); + assert!( + trace + .near_city_acquisition_region_lane_statuses + .iter() + .any(|line| line.contains("[site+0x276]") + && line.contains("0x004014b0") + && line.contains("0x36b1/0x36b2/0x36b3") + && line.contains("0x0047efe0") + && line.contains("0x004134d0 / 0x0040f6d0") + && line.contains("best-effort projected owner lane")) + ); + assert!( + trace + .near_city_acquisition_region_lane_statuses + .iter() + .any(|line| line.contains("[site+0x2a4]") + && line.contains("record's own site id") + && line.contains("0x00480210") + && line.contains("0x004269b0") + && line.contains("reconstructible from collection identity")) + ); + assert!( + trace + .near_city_acquisition_region_lane_statuses + .iter() + .any(|line| line.contains("[site+0x310/+0x338/+0x360]") + && line.contains("0x0040cac0") + && line.contains("0x0040c9a0") + && line.contains("0x0040d450") + && line.contains("0x00410b30..0x004118f4") + && line.contains("0x00412560")) + ); + assert!( + trace + .near_city_acquisition_region_lane_statuses + .iter() + .any( + |line| line.contains("0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0") + && line.contains("0x0040ce60") + && line.contains("[site+0x3cc/+0x3d0]") + ) + ); + assert!( + trace + .peer_site_runtime_reconstruction_steps + .iter() + .any(|line| { + line.contains("[cell+0xd4]") + && line.contains("[cell+0xd6]") + && line.contains("0x0042bbf0/0x0042bbb0") + }) + ); + assert!(trace.next_owner_questions.iter().any(|line| { + line.contains("0x004160aa") && line.contains("0x0040ee10") && line.contains("0x0040edf6") + })); + let acquisition_branch = trace.companies[0] + .branches + .iter() + .find(|branch| branch.branch_name == "industry_acquisition_side_branch") + .expect("missing acquisition branch"); + assert_eq!( + acquisition_branch.status, + "blocked_missing_near-city_owner_mapping" + ); + assert!( + acquisition_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x004014b0")) + ); + assert!( + acquisition_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x004019e0")) + ); + assert!( + acquisition_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x004010f0")) + ); + assert!( + acquisition_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x00420030 / 0x00420280")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00405920")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0041f6e0") && line.contains("0x0042b2d0")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00480710") && line.contains("route-entry-anchor")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0042bbf0") + && line.contains("0x0042bbb0") + && line.contains("[cell+0xd4]")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0042c9f0") + && line.contains("0x0042c9a0") + && line.contains("[cell+0xd6]")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004269b0") + && line.contains("0x0062b26c") + && line.contains("[site+0x276]/[site+0x27a]")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00426dce..0x00426ea1") + && line.contains("0x0062b26c") + && line.contains("non-subtype-4")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00430040..0x004300d6") && line.contains("0x09/0x0b/0x0c")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00431b20") + && line.contains("0x0061039c") + && line.contains("0x00430040") + && line.contains("0x00426d60") + && line.contains("0x0042fc90")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0042fc90") + && line.contains("0x0062b26c") + && line.contains("[site+0x276]") + && line.contains("vtable slot +0x70")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004134d0") && line.contains("0x0040f6d0")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00403ef3 / 0x00404489")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0046f073 / 0x004707ff")) + ); + assert!(trace.known_bridge_helpers.iter().any(|line| { + line.contains("0x0046f073 / 0x004707ff") + && line.contains("[+0x0c]") + && line.contains("0x0040f5d4") + })); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004706b0") + && line.contains("selector-0x13") + && line.contains("0x0040ef10")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00472b40") + && line.contains("selector-0x72") + && line.contains("0x00472bef / 0x00472d03")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00422bb4") + && line.contains("0x0062b2fc") + && line.contains("literal flags 1/0") + && line.contains("out-param")) + ); + assert!(trace.known_bridge_helpers.iter().any(|line| { + line.contains("0x00508fd1 / 0x005098eb") + && line.contains("[this+0x7c]") + && line.contains("0x0040eba0") + && line.contains("arg3 forced to zero") + })); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00473c20") + && line.contains("0x006ce808..0x006ce988") + && line.contains("0x00473c98")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0042128d") + && line.contains("0x00421430") + && line.contains("[site+0x276]")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00422305") + && line.contains("event 0x7") + && line.contains("[site+0x276]")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004269c9 / 0x00426a2a") + && line.contains("[site+0x276]/[site+0x27a]")) + ); + assert!(trace.known_bridge_helpers.iter().any(|line| { + line.contains("0x004282a9 / 0x004300d6") + && line.contains("owner-transfer") + && line.contains("placed-structure") + })); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00413440") + && line.contains("0x36b1/0x36b2/0x36b3") + && line.contains("slot +0x44")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040f6d0") + && line.contains("0x00481390") + && line.contains("0x00480210")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040df27 / 0x0040e00a / 0x0040edf6")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00444690") && line.contains("0x004133b0")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040e360..0x0040edf6") + && line.contains("[site+0x2a8/+0x2a4/+0x276]") + && line.contains("0x00426b10") + && line.contains("0x00455860")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040e450") && line.contains("queued site-id")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004160aa") && line.contains("0x0040ee10")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040dc40") + && line.contains("[site+0x276]") + && line.contains("0x2329/0x0d")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00417840 / 0x004197e0 / 0x004142c0 / 0x004142d0")) + ); + assert!(trace.known_bridge_helpers.iter().any(|line| { + line.contains("0x0040d1f0 / 0x00480710 / 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70") + })); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00402cb0 / 0x00403ed5 / 0x0040446b")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004134d0 / 0x0040ef10")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00481430 / 0x0047d8e0")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040c9a0") + && line.contains("[site+0x310/+0x338/+0x360]") + && line.contains("[site+0x2b4/+0x2b8/+0x2bc]")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040d450") + && line.contains("[site+0x310]") + && line.contains("0x00436590")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040fb70") + && line.contains("vtable slot +0x80") + && line.contains("0x00412560")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00412560") + && line.contains("0x0062ba8c") + && line.contains("world date/flags")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00410b30..0x004118f4") + && line.contains("[site+0x310/+0x338/+0x360]") + && line.contains("0x00412560")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x00401633") && line.contains("0x2329/0x0d")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0044b81a") + && line.contains("0x00436590") + && line.contains("0x65")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x004b4052 / 0x004b46ec") && line.contains("0x0062b26c")) + ); + assert!(trace.known_bridge_helpers.iter().any(|line| { + line.contains("0x004b70f5 / 0x004b7979") + && line.contains("0x004337a0") + && line.contains("0x00540120 / 0x00518140") + })); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040a3a1..0x0040a4d3") && line.contains("0x0040c9a0")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x0040fcc0..0x0040fe28") + && line.contains("0x00422c62..0x00422d3c") + && line.contains("0x0040cac0")) + ); + assert!( + trace + .known_bridge_helpers + .iter() + .any(|line| line.contains("0x005c8c50 +0x40") + && line.contains("0x0040ce60") + && line.contains("0x0040cd70 / 0x0045c150")) + ); + let city_branch = trace.companies[0] + .branches + .iter() + .find(|branch| branch.branch_name == "city_connection_announcement") + .expect("missing city branch"); + assert!( + city_branch + .candidate_consumers + .iter() + .any(|line| line.contains("0x00406050")) + ); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/services/region.rs b/crates/rrt-runtime/src/inspect/smp/tests/services/region.rs new file mode 100644 index 0000000..209599a --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/services/region.rs @@ -0,0 +1,225 @@ +use super::*; + +#[test] +fn builds_region_service_trace_report_with_explicit_latch_blockers() { + let mut analysis = empty_analysis_report(); + analysis.region_record_triplets = Some(SmpSaveRegionRecordTripletProbe { + profile_family: analysis.profile_family.clone(), + source_kind: "save-region-record-triplets".to_string(), + semantic_family: "marker09".to_string(), + records_tag_offset: 0, + close_tag_offset: 0, + record_count: 1, + entries: vec![SmpSaveRegionRecordTripletEntryProbe { + record_index: 0, + name: "Marker09".to_string(), + record_payload_relative_offset: 0, + record_payload_relative_offset_hex: "0x0".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0, + profile_tag_relative_offset: 0, + pre_name_prefix_len: 0, + pre_name_prefix_hex_bytes: Vec::new(), + pre_name_prefix_dword_candidates: Vec::new(), + policy_chunk_len: 0, + profile_chunk_len: 0, + policy_leading_f32_0: 368.0, + policy_leading_f32_1: 0.0, + policy_leading_f32_2: 92.0, + policy_reserved_dwords: Vec::new(), + policy_reserved_dword_candidates: Vec::new(), + policy_trailing_word: 0, + policy_trailing_word_hex: "0x0000".to_string(), + profile_collection: Some(SmpSaveRegionProfileCollectionProbe { + direct_collection_flag: 1, + entry_stride: 0x22, + live_id_bound: 17, + live_record_count: 17, + entry_start_relative_offset: 0, + trailing_padding_len: 0, + entries: Vec::new(), + }), + }], + evidence: Vec::new(), + }); + + let trace = build_region_service_trace_report(&analysis); + assert_eq!(trace.region_record_triplet_count, 1); + assert_eq!(trace.queued_notice_record_count, 0); + assert!(!trace.atlas_candidate_consumers.is_empty()); + assert_eq!(trace.known_owner_bridge_fields.len(), 6); + assert_eq!(trace.known_bridge_helpers.len(), 16); + assert_eq!(trace.next_owner_questions.len(), 5); + assert_eq!(trace.candidate_consumer_hypotheses.len(), 6); + assert_eq!( + trace.candidate_consumer_hypotheses[0].status, + "highest_priority_static_mapping_target" + ); + assert_eq!( + trace.candidate_consumer_hypotheses[2].status, + "secondary_candidate_after_pending_service" + ); + assert_eq!( + trace.candidate_consumer_hypotheses[3].status, + "next_global_restore_handoff_target" + ); + assert_eq!( + trace.candidate_consumer_hypotheses[1].status, + "parallel_static_mapping_target" + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x5209") + && line.contains("0x520a") + && line.contains("0x520b")) + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x0041f5c0") + && line.contains("[region+0x37f]") + && line.contains("[region+0x385]")) + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x00444887") + && line.contains("0x00487c20") + && line.contains("0x0040b5d0")) + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x00421ce0") && line.contains("0x0041fb00")) + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x00421730") + && line.contains("[region+0x242/+0x246/+0x24a/+0x24e/+0x252]")) + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x00448740..0x0044881f") + && line.contains("0x006cfc9c") + && line.contains("0x53b070") + && line.contains("0x00487bd0")) + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x00487de0") + && line.contains("0x00533cf0") + && line.contains("0x00536ea0")) + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x0044c4b0") + && line.contains("0x00455f60") + && line.contains("bit 0x10")) + ); + assert!( + trace.candidate_consumer_hypotheses[3] + .candidate_consumers + .iter() + .any(|line| line.contains("0x00444887")) + ); + assert!( + trace.candidate_consumer_hypotheses[3] + .candidate_consumers + .iter() + .any(|line| line.contains("0x00487c20")) + ); + assert!( + trace.candidate_consumer_hypotheses[3] + .candidate_consumers + .iter() + .any(|line| line.contains("0x0040b5d0")) + ); + assert!( + trace.candidate_consumer_hypotheses[3] + .evidence + .iter() + .any(|line| line.contains("0x00444887") && line.contains("0x00421510")) + ); + assert!( + trace.candidate_consumer_hypotheses[3] + .evidence + .iter() + .any(|line| line.contains("0x00444b90") && line.contains("0x00420560")) + ); + assert_eq!( + trace.candidate_consumer_hypotheses[4].status, + "next_post_load_owner_family" + ); + assert!( + trace.candidate_consumer_hypotheses[4] + .candidate_consumers + .iter() + .any(|line| line.contains("0x004384d0")) + ); + assert!( + trace.candidate_consumer_hypotheses[4] + .candidate_consumers + .iter() + .any(|line| line.contains("0x004133b0")) + ); + assert!( + trace.candidate_consumer_hypotheses[4] + .candidate_consumers + .iter() + .any(|line| line.contains("0x00421c20")) + ); + assert!( + trace.candidate_consumer_hypotheses[4] + .evidence + .iter() + .any(|line| line.contains("0x00446d40") && line.contains("0x004384d0")) + ); + assert!( + trace.candidate_consumer_hypotheses[4] + .evidence + .iter() + .any(|line| line.contains("0x004133b0") && line.contains("0x00480710")) + ); + assert!( + trace.candidate_consumer_hypotheses[4] + .evidence + .iter() + .any(|line| line.contains("0x00421c20") && line.contains("0x004235c0")) + ); + assert!( + trace.candidate_consumer_hypotheses[1] + .evidence + .iter() + .any(|line| line.contains("0x004881b0") + && line.contains("[region+0x3d]") + && line.contains("[region+0x41]")) + ); + assert_eq!(trace.entries.len(), 1); + assert_eq!( + trace.entries[0].branches[0].status, + "blocked_missing_pending_bonus_owner_lane" + ); + assert_eq!( + trace.entries[0].branches[1].status, + "blocked_missing_completion_and_one_shot_latches" + ); + assert!( + trace + .notes + .iter() + .any(|line| { line.contains("pre-name prefix lengths") && line.contains("[0]") }) + ); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/services/shared.rs b/crates/rrt-runtime/src/inspect/smp/tests/services/shared.rs new file mode 100644 index 0000000..9f0ea2f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/services/shared.rs @@ -0,0 +1,59 @@ +use super::*; +use crate::inspect::smp::services::SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry; + +#[test] +fn summarizes_nonzero_companion_building_family_overlaps() { + let overlaps = + summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_overlaps(&[ + SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { + companion_byte_hex: "0x01".to_string(), + primary_name: "TextileMill".to_string(), + secondary_name: "TextileMill".to_string(), + count: 9, + }, + SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { + companion_byte_hex: "0x01".to_string(), + primary_name: "Toolndie".to_string(), + secondary_name: "Toolndie".to_string(), + count: 2, + }, + SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { + companion_byte_hex: "0x01".to_string(), + primary_name: "MunitionsFactory".to_string(), + secondary_name: "MunitionsFactory".to_string(), + count: 1, + }, + ]); + + assert_eq!(overlaps.len(), 2); + assert_eq!(overlaps[0].primary_name, "TextileMill"); + assert!(overlaps[0].primary_matches_nonzero_stock_building_header_family); + assert!(overlaps[0].secondary_matches_nonzero_stock_building_header_family); + assert_eq!(overlaps[1].primary_name, "Toolndie"); + assert!(overlaps[1].primary_matches_nonzero_stock_building_header_family); + assert!(overlaps[1].secondary_matches_nonzero_stock_building_header_family); +} + +#[test] +fn summarizes_nonzero_companion_building_family_residues() { + let residues = + summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_residues(&[ + SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { + companion_byte_hex: "0x01".to_string(), + primary_name: "TextileMill".to_string(), + secondary_name: "TextileMill".to_string(), + count: 9, + }, + SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { + companion_byte_hex: "0x01".to_string(), + primary_name: "MunitionsFactory".to_string(), + secondary_name: "MunitionsFactory".to_string(), + count: 1, + }, + ]); + + assert_eq!(residues.len(), 1); + assert_eq!(residues[0].primary_name, "MunitionsFactory"); + assert_eq!(residues[0].secondary_name, "MunitionsFactory"); + assert_eq!(residues[0].companion_byte_hex, "0x01"); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/special_conditions.rs b/crates/rrt-runtime/src/inspect/smp/tests/special_conditions.rs new file mode 100644 index 0000000..e3d90b6 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/special_conditions.rs @@ -0,0 +1,613 @@ +use super::*; + +#[test] +fn parses_zeroed_post_special_conditions_scalar_window() { + let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET]; + let sentinel_offset = SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; + bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); + + let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None) + .expect("special-conditions probe should parse"); + let probe = parse_post_special_conditions_scalar_probe( + &bytes, + Some("gmp"), + Some(&SmpContainerProfile { + profile_family: "rt3-map-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&special_conditions_probe), + ) + .expect("post-special-conditions probe should parse"); + + assert_eq!(probe.window_offset, POST_SPECIAL_CONDITIONS_SCALAR_OFFSET); + assert_eq!( + probe.window_end_offset, + POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + ); + assert_eq!(probe.dword_count, 79); + assert_eq!( + probe.overlap_end_offset, + POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET + ); + assert_eq!(probe.overlap_dword_count, 14); + assert_eq!(probe.overlap_nonzero_dword_count, 0); + assert!(probe.overlap_nonzero_relative_offset_hexes.is_empty()); + assert_eq!( + probe.tail_offset, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET + ); + assert_eq!(probe.tail_dword_count, 65); + assert_eq!( + probe.tail_runtime_object_offset, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET + ); + assert_eq!( + probe.tail_runtime_object_end_offset, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET + ); + assert!(!probe.tail_runtime_object_validated_byte_mirror); + assert_eq!( + probe.tail_grounded_live_field_offset, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET + ); + assert_eq!( + probe.tail_grounded_live_field_copy_len, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN + ); + assert_eq!( + probe.tail_grounded_live_field_copy_end_offset, + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET + ); + assert!(probe.tail_window_cuts_through_grounded_live_field); + assert_eq!( + probe.tail_grounded_live_field_remaining_file_window_offset, + POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + ); + assert_eq!( + probe.tail_grounded_live_field_remaining_file_window_len, + 0x28 + ); + assert_eq!( + probe.tail_grounded_live_field_remaining_file_window_nonzero_byte_count, + 0 + ); + assert_eq!(probe.tail_next_grounded_dword_field_offset_hex, "0x4c80"); + assert_eq!( + probe.tail_next_grounded_dword_field_file_offset_hex, + "0x0f65" + ); + assert_eq!(probe.tail_second_grounded_dword_field_offset_hex, "0x4c8c"); + assert_eq!( + probe.tail_second_grounded_dword_field_file_offset_hex, + "0x0f71" + ); + assert!(!probe.post_text_field_file_alignment_matches_grounded_dword_fields); + assert!( + probe + .tail_grounded_live_field_remaining_file_window_first_nonzero_offset + .is_none() + ); + assert!( + probe + .tail_grounded_live_field_remaining_file_window_last_nonzero_offset + .is_none() + ); + assert_eq!(probe.tail_nonzero_dword_count, 0); + assert!(probe.tail_first_nonzero_offset.is_none()); + assert!(probe.tail_last_nonzero_offset.is_none()); + assert!(probe.tail_nonzero_relative_offset_hexes.is_empty()); + assert_eq!(probe.nonzero_dword_count, 0); + assert!(probe.first_nonzero_offset.is_none()); + assert!(probe.last_nonzero_offset.is_none()); + assert!(probe.nonzero_lanes.is_empty()); +} + +#[test] +fn parses_zeroed_smp_aligned_runtime_rule_band() { + let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET]; + let sentinel_offset = SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; + bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); + + let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None) + .expect("special-conditions probe should parse"); + let probe = parse_smp_aligned_runtime_rule_band_probe( + &bytes, + Some("gmp"), + Some(&SmpContainerProfile { + profile_family: "rt3-map-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&special_conditions_probe), + ) + .expect("aligned runtime-rule band probe should parse"); + + assert_eq!(probe.band_offset, SPECIAL_CONDITIONS_OFFSET); + assert_eq!(probe.band_end_offset, SMP_ALIGNED_RUNTIME_RULE_END_OFFSET); + assert_eq!(probe.dword_count, SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT); + assert_eq!( + probe.known_editor_rule_dword_count, + SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT + ); + assert_eq!( + probe.post_window_overlap_start_index, + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX + ); + assert_eq!( + probe.post_window_overlap_dword_count, + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT + ); + assert_eq!(probe.nonzero_lane_count, 1); + assert_eq!(probe.nonzero_band_indices, vec![35]); + assert!(probe.nonzero_post_window_overlap_band_indices.is_empty()); + assert!( + probe + .nonzero_post_window_overlap_post_relative_offset_hexes + .is_empty() + ); + assert_eq!( + probe.nonzero_lanes[0].lane_kind, + "known-special-condition-dword" + ); + assert_eq!( + probe.nonzero_lanes[0].known_label.as_deref(), + Some("Hidden sentinel") + ); +} + +#[test] +fn parses_nonzero_post_special_conditions_scalar_window() { + let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET]; + let sentinel_offset = SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; + bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); + bytes[0x0df8..0x0dfc].copy_from_slice(&0x413d298cu32.to_le_bytes()); + bytes[0x0e00..0x0e04].copy_from_slice(&0x40e6b756u32.to_le_bytes()); + bytes[0x0f0c..0x0f10].copy_from_slice(&0x42574909u32.to_le_bytes()); + bytes[0x0f34] = 0xaa; + bytes[0x0f4e] = 0xbb; + + let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) + .expect("special-conditions probe should parse"); + let probe = parse_post_special_conditions_scalar_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&special_conditions_probe), + ) + .expect("post-special-conditions probe should parse"); + + assert_eq!(probe.nonzero_dword_count, 3); + assert_eq!(probe.first_nonzero_offset, Some(0x0df8)); + assert_eq!(probe.last_nonzero_offset, Some(0x0f0c)); + assert_eq!(probe.overlap_nonzero_dword_count, 2); + assert_eq!( + probe.overlap_nonzero_relative_offset_hexes, + vec!["0x4".to_string(), "0xc".to_string()] + ); + assert_eq!(probe.tail_nonzero_dword_count, 1); + assert_eq!(probe.tail_first_nonzero_offset, Some(0x0f0c)); + assert_eq!(probe.tail_last_nonzero_offset, Some(0x0f0c)); + assert_eq!( + probe.tail_nonzero_relative_offset_hexes, + vec!["0x118".to_string()] + ); + assert_eq!(probe.tail_runtime_object_offset_hex, "0x4b47"); + assert_eq!(probe.tail_runtime_object_end_offset_hex, "0x4c4b"); + assert_eq!( + probe.tail_grounded_live_field_copy_end_offset_hex, + "0x4c73".to_string() + ); + assert_eq!( + probe.tail_grounded_live_field_name, + "victory-or-outcome status text buffer" + ); + assert!(probe.tail_window_cuts_through_grounded_live_field); + assert_eq!( + probe.tail_grounded_live_field_remaining_file_window_len_hex, + "0x28" + ); + assert_eq!( + probe.tail_grounded_live_field_remaining_file_window_nonzero_byte_count, + 2 + ); + assert_eq!( + probe.tail_grounded_live_field_remaining_file_window_first_nonzero_offset, + Some(0x0f34) + ); + assert_eq!( + probe.tail_grounded_live_field_remaining_file_window_last_nonzero_offset, + Some(0x0f4e) + ); + assert_eq!(probe.tail_next_grounded_dword_field_file_offset, 0x0f65); + assert_eq!(probe.tail_second_grounded_dword_field_file_offset, 0x0f71); + assert!(!probe.post_text_field_file_alignment_matches_grounded_dword_fields); + assert_eq!(probe.nonzero_lanes[0].relative_offset, 0x04); + assert_eq!(probe.nonzero_lanes[1].relative_offset, 0x0c); + assert_eq!(probe.nonzero_lanes[2].relative_offset, 0x118); + assert!( + probe + .nonzero_lanes + .iter() + .all(|lane| lane.probable_f32_le.is_some()) + ); +} + +#[test] +fn parses_post_text_field_neighborhood_probe() { + let mut bytes = vec![0u8; POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET]; + let sentinel_offset = SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; + bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); + bytes[0x0f59] = 0x01; + bytes[0x0f5d] = 0x02; + bytes[0x0f61] = 0x03; + bytes[0x0f6d] = 0x04; + bytes[0x0f5c..0x0f60].copy_from_slice(&0x40f33333u32.to_le_bytes()); + bytes[0x0f6c..0x0f70].copy_from_slice(&0x40c08cfbu32.to_le_bytes()); + + let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) + .expect("special-conditions probe should parse"); + let probe = parse_post_text_field_neighborhood_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-scenario-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&special_conditions_probe), + ) + .expect("post-text field neighborhood probe should parse"); + + assert_eq!(probe.window_offset, POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET); + assert_eq!( + probe.window_end_offset, + POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET + ); + assert_eq!(probe.grounded_field_observations.len(), 6); + assert_eq!( + probe.grounded_field_observations[0].field_name, + "Auto-Show Grade During Track Lay" + ); + assert_eq!(probe.grounded_field_observations[0].value_u8, Some(0x01)); + assert_eq!( + probe.grounded_field_observations[3].field_name, + "leftover simulation time accumulator" + ); + assert_eq!(probe.one_byte_early_float_candidates.len(), 2); + assert_eq!( + probe.one_byte_early_float_candidates[0].grounded_field_name, + "Starting Building Density Level" + ); + assert_eq!( + probe.one_byte_early_float_candidates[0].candidate_offset_hex, + "0x0f5c" + ); + assert_eq!( + probe.one_byte_early_float_candidates[1].grounded_field_name, + "selected-year lane snapshot" + ); + assert_eq!( + probe.one_byte_early_float_candidates[1].candidate_offset_hex, + "0x0f6c" + ); +} + +#[test] +fn parses_locomotive_policy_neighborhood_probe() { + let mut bytes = vec![0u8; LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET]; + let sentinel_offset = SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; + bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); + bytes[0x0f78] = 0x01; + bytes[0x0f7c] = 0x02; + bytes[0x0f7d] = 0x03; + bytes[0x0f7e] = 0x04; + bytes[0x0f9c..0x0fa0].copy_from_slice(&0x42c1c036u32.to_le_bytes()); + bytes[0x0fa0..0x0fa4].copy_from_slice(&0x433a7abeu32.to_le_bytes()); + + let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) + .expect("special-conditions probe should parse"); + let probe = parse_locomotive_policy_neighborhood_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-scenario-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&special_conditions_probe), + ) + .expect("locomotive policy neighborhood probe should parse"); + + assert_eq!(probe.window_offset, LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET); + assert_eq!( + probe.window_end_offset, + LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET + ); + assert_eq!(probe.grounded_field_observations.len(), 9); + assert_eq!( + probe.grounded_field_observations[0].field_name, + "selected-year bucket companion scalar" + ); + assert_eq!( + probe.grounded_field_observations[4].field_name, + "All Steam Locos Avail." + ); + assert_eq!(probe.grounded_field_observations[4].value_u8, Some(0x02)); + assert_eq!( + probe.grounded_field_observations[8].field_name, + "cached available-locomotive rating" + ); + assert_eq!(probe.three_byte_early_float_candidates.len(), 2); + assert_eq!( + probe.three_byte_early_float_candidates[0].grounded_field_name, + "station-list selected station id" + ); + assert_eq!( + probe.three_byte_early_float_candidates[0].candidate_offset_hex, + "0x0f9c" + ); + assert_eq!( + probe.three_byte_early_float_candidates[1].grounded_field_name, + "cached available-locomotive rating" + ); + assert_eq!( + probe.three_byte_early_float_candidates[1].candidate_offset_hex, + "0x0fa0" + ); +} + +#[test] +fn parses_pre_recipe_scalar_plateau_probe() { + let mut bytes = vec![0u8; PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET]; + let sentinel_offset = SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; + bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); + bytes[0x0fa7..0x0fab].copy_from_slice(&0x82839300u32.to_le_bytes()); + bytes[0x0fab..0x0faf].copy_from_slice(&0x948c9949u32.to_le_bytes()); + bytes[0x0faf..0x0fb3].copy_from_slice(&0x8000003fu32.to_le_bytes()); + bytes[0x0fb3..0x0fb7].copy_from_slice(&0x75c28f3fu32.to_le_bytes()); + bytes[0x0fcb..0x0fcf].copy_from_slice(&0x00300000u32.to_le_bytes()); + bytes[0x0fdb..0x0fdf].copy_from_slice(&0x00ffea22u32.to_le_bytes()); + + let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) + .expect("special-conditions probe should parse"); + let probe = parse_pre_recipe_scalar_plateau_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&special_conditions_probe), + ) + .expect("pre-recipe scalar plateau probe should parse"); + + assert_eq!(probe.window_offset, PRE_RECIPE_SCALAR_PLATEAU_OFFSET); + assert_eq!( + probe.window_end_offset, + PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET + ); + assert_eq!(probe.family_signature, "rt3-105-base-pre-recipe-plateau-v1"); + assert_eq!(probe.nonzero_lanes[0].absolute_offset_hex, "0x0fa7"); + assert_eq!(probe.nonzero_lanes[2].absolute_offset_hex, "0x0faf"); + assert_eq!(probe.nonzero_lanes[2].value_hex, "0x8000003f"); +} + +#[test] +fn parses_recipe_book_summary_probe() { + let mut bytes = vec![0u8; RECIPE_BOOK_SUMMARY_END_OFFSET]; + let sentinel_offset = SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; + bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); + + let book0 = RECIPE_BOOK_ROOT_OFFSET; + bytes[book0..book0 + 16].copy_from_slice(&[ + 0x11, 0x22, 0x33, 0x44, 0xaa, 0xbb, 0xcc, 0xdd, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, + 0x80, + ]); + bytes[book0 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + ..book0 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + 4] + .copy_from_slice(&0x41200000u32.to_le_bytes()); + bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + ..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + RECIPE_BOOK_LINE_AREA_LEN] + .fill(0xcd); + bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 4] + .copy_from_slice(&0x00000003u32.to_le_bytes()); + bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 4..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 8] + .copy_from_slice(&0x41a00000u32.to_le_bytes()); + bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 8..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 12] + .copy_from_slice(&0x00000017u32.to_le_bytes()); + bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 0x1c..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 0x20] + .copy_from_slice(&0x0000002au32.to_le_bytes()); + + let book1 = RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_STRIDE; + bytes[book1 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + ..book1 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + 4] + .copy_from_slice(&0x00000000u32.to_le_bytes()); + + let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None) + .expect("special-conditions probe should parse"); + let probe = parse_recipe_book_summary_probe( + &bytes, + Some("gmp"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-map-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&special_conditions_probe), + ) + .expect("recipe-book summary probe should parse"); + + assert_eq!(probe.root_offset, RECIPE_BOOK_ROOT_OFFSET); + assert_eq!(probe.book_count, RECIPE_BOOK_COUNT); + assert_eq!(probe.book_stride, RECIPE_BOOK_STRIDE); + assert_eq!( + probe.max_annual_production_relative_offset, + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + ); + assert_eq!(probe.books[0].head_kind, "mixed"); + assert_eq!(probe.books[0].line_area_kind, "mixed"); + assert_eq!(probe.books[0].max_annual_production_word_hex, "0x41200000"); + assert_eq!( + probe.books[0] + .max_annual_production_probable_f32_le + .as_deref(), + Some("10.000000") + ); + assert_eq!(probe.books[0].lines.len(), RECIPE_BOOK_LINE_COUNT); + assert_eq!(probe.books[0].lines[0].line_kind, "mixed"); + assert_eq!(probe.books[0].lines[0].mode_word_hex, "0x00000003"); + assert_eq!(probe.books[0].lines[0].annual_amount_word_hex, "0x41a00000"); + assert_eq!( + probe.books[0].lines[0] + .annual_amount_probable_f32_le + .as_deref(), + Some("20.000000") + ); + assert_eq!( + probe.books[0].lines[0].supplied_cargo_token_word_hex, + "0x00000017" + ); + assert_eq!( + probe.books[0].lines[0].supplied_cargo_token_window_hex, + "17000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd2a000000" + ); + assert_eq!( + probe.books[0].lines[0].supplied_cargo_token_window_ascii, + "....................*..." + ); + assert!(probe.books[0].lines[0].supplied_cargo_token_active_in_runtime_import); + assert_eq!( + probe.books[0].lines[0].demanded_cargo_token_word_hex, + "0x0000002a" + ); + assert_eq!( + probe.books[0].lines[0].demanded_cargo_token_window_hex, + "2a000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" + ); + assert_eq!( + probe.books[0].lines[0].demanded_cargo_token_window_ascii, + "*..................." + ); + assert!(probe.books[0].lines[0].demanded_cargo_token_active_in_runtime_import); + assert_eq!(probe.books[1].head_kind, "zero"); + assert_eq!(probe.books[1].line_area_kind, "zero"); + assert_eq!(probe.books[1].lines[0].line_kind, "zero"); +} + +#[test] +fn decodes_probable_recipe_token_high16_ascii_stem() { + assert_eq!( + probable_recipe_token_high16_ascii_stem(0x72470000).as_deref(), + Some("Gr") + ); + assert_eq!( + probable_recipe_token_high16_ascii_stem(0x68430000).as_deref(), + Some("Ch") + ); + assert_eq!(probable_recipe_token_high16_ascii_stem(0x000040a0), None); + assert_eq!(probable_recipe_token_high16_ascii_stem(0x00170000), None); +} + +#[test] +fn classifies_recipe_token_layouts() { + assert_eq!(classify_recipe_token_layout(0x00000000), "zero"); + assert_eq!( + classify_recipe_token_layout(0x72470000), + "high16-ascii-stem" + ); + assert_eq!(classify_recipe_token_layout(0x00170000), "high16-numeric"); + assert_eq!(classify_recipe_token_layout(0x000040a0), "low16-marker"); +} + +#[test] +fn classifies_recipe_line_signatures() { + assert_eq!( + classify_recipe_line_signature(0x00000000, 0x00000000, 0x00010000), + "demand-numeric-entry" + ); + assert_eq!( + classify_recipe_line_signature(0x00000000, 0x00000000, 0x72470000), + "demand-stem-entry" + ); + assert_eq!( + classify_recipe_line_signature(0x00000000, 0x00170000, 0x00000000), + "supply-numeric-entry" + ); + assert_eq!( + classify_recipe_line_signature(0x00110000, 0x000040a0, 0x00000000), + "supply-marker-entry" + ); +} + +#[test] +fn classifies_recipe_runtime_import_branches() { + assert_eq!( + classify_recipe_runtime_import_branch(0), + "zero-mode-skipped" + ); + assert_eq!( + classify_recipe_runtime_import_branch(1), + "mode1-demand-branch" + ); + assert_eq!( + classify_recipe_runtime_import_branch(3), + "mode3-dual-branch" + ); + assert_eq!( + classify_recipe_runtime_import_branch(0x00110000), + "nonzero-supply-branch" + ); +} + +#[test] +fn parses_nonzero_smp_aligned_runtime_rule_band() { + let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET]; + let sentinel_offset = SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; + bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); + bytes[0x0df8..0x0dfc].copy_from_slice(&0x413d298cu32.to_le_bytes()); + bytes[0x0e00..0x0e04].copy_from_slice(&0x40e6b756u32.to_le_bytes()); + bytes[0x0e18..0x0e1c].copy_from_slice(&0x41d4ccceu32.to_le_bytes()); + bytes[0x0e24..0x0e28].copy_from_slice(&0x3fd2b549u32.to_le_bytes()); + + let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) + .expect("special-conditions probe should parse"); + let probe = parse_smp_aligned_runtime_rule_band_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-scenario-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + Some(&special_conditions_probe), + ) + .expect("aligned runtime-rule band probe should parse"); + + assert_eq!(probe.nonzero_band_indices, vec![35, 37, 39, 45, 48]); + assert_eq!( + probe.nonzero_post_window_overlap_band_indices, + vec![37, 39, 45, 48] + ); + assert_eq!( + probe.nonzero_post_window_overlap_post_relative_offset_hexes, + vec![ + "0x4".to_string(), + "0xc".to_string(), + "0x24".to_string(), + "0x30".to_string() + ] + ); + assert_eq!(probe.nonzero_lanes[1].relative_offset, 0x94); + assert_eq!( + probe.nonzero_lanes[1].lane_kind, + "unlabeled-editor-rule-dword" + ); + assert!(probe.nonzero_lanes[1].probable_f32_le.is_some()); + assert_eq!(probe.nonzero_lanes.last().unwrap().band_index, 48); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/structures.rs b/crates/rrt-runtime/src/inspect/smp/tests/structures.rs new file mode 100644 index 0000000..b3895be --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/structures.rs @@ -0,0 +1,685 @@ +use super::*; + +#[test] +fn parses_placed_structure_record_triplet_probe_from_dual_name_rows() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x260usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x000036b1u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x000036b2u32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000036b3u32.to_le_bytes()); + let mut cursor = records_tag_offset + 4; + for (primary, secondary, lane0, lane1, lane2, lane3, lane4) in [ + ( + "StationA", + "StationSetA", + 43111.92f32, + 1385.5f32, + 34581.95f32, + 0.0f32, + 5.9760494f32, + ), + ( + "StationB", + "StationSetB", + 44000.0f32, + 1200.0f32, + 33000.0f32, + 0.0f32, + 4.5f32, + ), + ] { + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); + bytes[cursor + 4] = primary.len() as u8; + bytes[cursor + 5..cursor + 5 + primary.len()].copy_from_slice(primary.as_bytes()); + let second_len_offset = cursor + 5 + primary.len(); + bytes[second_len_offset] = secondary.len() as u8; + bytes[second_len_offset + 1..second_len_offset + 1 + secondary.len()] + .copy_from_slice(secondary.as_bytes()); + cursor += 0x19; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); + bytes[cursor + 4..cursor + 8].copy_from_slice(&lane0.to_bits().to_le_bytes()); + bytes[cursor + 8..cursor + 12].copy_from_slice(&lane1.to_bits().to_le_bytes()); + bytes[cursor + 12..cursor + 16].copy_from_slice(&lane2.to_bits().to_le_bytes()); + bytes[cursor + 16..cursor + 20].copy_from_slice(&lane3.to_bits().to_le_bytes()); + bytes[cursor + 20..cursor + 24].copy_from_slice(&lane4.to_bits().to_le_bytes()); + bytes[cursor + 28..cursor + 30].copy_from_slice(&0x0101u16.to_le_bytes()); + cursor += 0x1e; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); + bytes[cursor + 4..cursor + 8].copy_from_slice(&0x5dc1u32.to_le_bytes()); + let mut payload_cursor = cursor + 8; + bytes[payload_cursor] = primary.len() as u8; + bytes[payload_cursor + 1..payload_cursor + 1 + primary.len()] + .copy_from_slice(primary.as_bytes()); + payload_cursor += 1 + primary.len(); + bytes[payload_cursor] = 0; + payload_cursor += 1; + bytes[payload_cursor] = secondary.len() as u8; + bytes[payload_cursor + 1..payload_cursor + 1 + secondary.len()] + .copy_from_slice(secondary.as_bytes()); + payload_cursor += 1 + secondary.len(); + bytes[payload_cursor] = 0; + payload_cursor += 1; + bytes[payload_cursor..payload_cursor + 4].copy_from_slice(&0x0e373500u32.to_le_bytes()); + bytes[payload_cursor + 4..payload_cursor + 8].copy_from_slice(&(-1i32).to_le_bytes()); + bytes[payload_cursor + 8..payload_cursor + 12].copy_from_slice(&0x5dc2u32.to_le_bytes()); + cursor += 0x18 + primary.len() + secondary.len(); + } + + let header_probe = SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-placed-structure-tagged-header-counts".to_string(), + semantic_family: "scenario-save-placed-structure-header-counts".to_string(), + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + direct_collection_flag: 0, + direct_collection_flag_hex: "0x00000000".to_string(), + direct_record_stride: 0x06, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 3, + live_id_bound_hex: "0x00000003".to_string(), + live_record_count: 2, + live_record_count_hex: "0x00000002".to_string(), + header_words: vec![0, 6, 0x0a, 0x14, 3, 2], + header_hex_words: vec![], + evidence: vec![], + }; + let triplet_probe = + parse_save_placed_structure_record_triplet_probe(&bytes, Some(&header_probe)) + .expect("placed-structure triplet probe should parse"); + + assert_eq!(triplet_probe.record_count, 2); + assert_eq!(triplet_probe.entries[0].primary_name, "StationA"); + assert_eq!(triplet_probe.entries[0].secondary_name, "StationSetA"); + assert_eq!(triplet_probe.entries[0].policy_chunk_len, 0x1a); + assert_eq!(triplet_probe.entries[0].policy_f32_lane_4, 5.9760494); + assert_eq!(triplet_probe.entries[0].policy_trailing_word, 0x0101); + assert_eq!( + triplet_probe.entries[0].profile_open_marker_hex, + "0x00005dc1" + ); + assert_eq!( + triplet_probe.entries[0].profile_repeated_primary_name, + "StationA" + ); + assert_eq!( + triplet_probe.entries[0].profile_repeated_secondary_name, + "StationSetA" + ); + assert_eq!( + triplet_probe.entries[0].profile_footer_relative_offset_hex, + "0x1b" + ); + assert_eq!(triplet_probe.entries[0].profile_pre_footer_padding_len, 1); + assert_eq!( + triplet_probe.entries[0].profile_pre_footer_padding_hex_bytes, + vec!["0x00".to_string()] + ); + assert_eq!(triplet_probe.entries[0].profile_companion_byte_u8, Some(0)); + assert_eq!( + triplet_probe.entries[0] + .profile_companion_byte_hex + .as_deref(), + Some("0x00") + ); + assert_eq!( + triplet_probe.entries[0].profile_payload_dword_hex, + "0x0e373500" + ); + assert_eq!(triplet_probe.entries[0].profile_sentinel_i32, -1); + assert_eq!(triplet_probe.entries[0].profile_status_kind, "unset"); + assert_eq!(triplet_probe.entries[0].farm_growth_stage_index, None); + assert_eq!( + triplet_probe.entries[0].profile_close_marker_hex, + "0x00005dc2" + ); + assert_eq!(triplet_probe.entries[1].primary_name, "StationB"); + assert_eq!(triplet_probe.entries[1].secondary_name, "StationSetB"); + assert_eq!(triplet_probe.entries[1].policy_f32_lane_1, 1200.0); +} + +#[test] +fn derives_placed_structure_farm_growth_stage_from_nonnegative_status() { + assert_eq!( + derive_save_placed_structure_profile_status("FarmCorn", "FarmSet", 4), + ("farm_growth_stage_bucket", Some(4)) + ); + assert_eq!( + derive_save_placed_structure_profile_status("StationA", "StationSetA", -1), + ("unset", None) + ); + assert_eq!( + derive_save_placed_structure_profile_status("StationA", "StationSetA", 4), + ("opaque_nondefault", None) + ); +} + +#[test] +fn parses_placed_structure_dynamic_side_buffer_probe_from_embedded_name_row() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x220usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x000038a5u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x000038a6u32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000038a7u32.to_le_bytes()); + let header_words = [ + 0u32, 0x06, 1000, 500, 1000, 388, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + let payload_offset = records_tag_offset + 4; + bytes[payload_offset..payload_offset + 4].copy_from_slice(&0x0005d368u32.to_le_bytes()); + bytes[payload_offset + 4..payload_offset + 6].copy_from_slice(&0x0001u16.to_le_bytes()); + bytes[payload_offset + 6] = 0xff; + let name_tag_offset = payload_offset + 7; + bytes[name_tag_offset..name_tag_offset + 2] + .copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); + let first_name = "TrackCapST_Cap.3dp"; + let second_name = "Infrastructure"; + bytes[name_tag_offset + 4] = first_name.len() as u8; + bytes[name_tag_offset + 5..name_tag_offset + 5 + first_name.len()] + .copy_from_slice(first_name.as_bytes()); + let second_len_offset = name_tag_offset + 5 + first_name.len(); + bytes[second_len_offset] = second_name.len() as u8; + bytes[second_len_offset + 1..second_len_offset + 1 + second_name.len()] + .copy_from_slice(second_name.as_bytes()); + + let probe = parse_save_placed_structure_dynamic_side_buffer_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("placed-structure dynamic side-buffer probe should parse"); + + assert_eq!(probe.direct_record_stride, 0x06); + assert_eq!(probe.live_id_bound, 1000); + assert_eq!(probe.live_record_count, 388); + assert_eq!(probe.owner_shared_dword_hex, "0x0005d368"); + assert_eq!(probe.owner_shared_dword_relative_offset, 0); + assert!(probe.owner_shared_dword_matches_first_compact_prefix_leading_dword); + assert_eq!(probe.first_record_child_count_after_owner_shared, Some(1)); + assert_eq!( + probe + .first_record_saved_primary_child_byte_after_owner_shared_hex + .as_deref(), + Some("0xff") + ); + assert_eq!( + probe.first_record_first_name_tag_relative_offset_after_owner_shared, + Some(3) + ); + assert_eq!(probe.prefix_leading_dword_hex, "0x0005d368"); + assert_eq!(probe.prefix_trailing_word_hex, "0x0001"); + assert_eq!(probe.prefix_separator_byte_hex, "0xff"); + assert_eq!(probe.first_embedded_name_tag_relative_offset, 7); + assert_eq!(probe.embedded_name_tag_count, 1); + assert_eq!(probe.decoded_embedded_name_row_count, 1); + assert_eq!(probe.decoded_embedded_name_row_with_tertiary_name_count, 0); + assert_eq!(probe.unique_compact_prefix_pattern_count, 1); + assert_eq!( + probe.prefix_leading_dword_matching_embedded_profile_tag_count, + 0 + ); + assert_eq!(probe.unique_embedded_name_pair_count, 1); + assert_eq!( + probe.first_embedded_primary_name.as_deref(), + Some("TrackCapST_Cap.3dp") + ); + assert_eq!( + probe.first_embedded_secondary_name.as_deref(), + Some("Infrastructure") + ); + assert_eq!(probe.first_embedded_tertiary_name.as_deref(), None); + assert_eq!(probe.compact_prefix_pattern_summaries.len(), 1); + assert_eq!( + probe.compact_prefix_pattern_summaries[0].prefix_leading_dword_hex, + "0x0005d368" + ); + assert_eq!(probe.compact_prefix_pattern_summaries[0].count, 1); + assert_eq!( + probe.compact_prefix_pattern_summaries[0].cap_like_primary_name_count, + 1 + ); + assert_eq!( + probe.compact_prefix_pattern_summaries[0].section_like_primary_name_count, + 0 + ); + assert_eq!(probe.name_pair_summaries.len(), 1); + assert_eq!(probe.name_pair_summaries[0].count, 1); + assert_eq!( + probe.name_pair_summaries[0].dominant_prefix_leading_dword_hex, + "0x0005d368" + ); + let payload_envelope_summary = probe + .payload_envelope_summary + .as_ref() + .expect("payload envelope summary should be present"); + assert_eq!( + payload_envelope_summary.row_count_with_policy_tag_before_next_name, + 0 + ); + assert_eq!( + payload_envelope_summary.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, + 0 + ); + assert_eq!( + payload_envelope_summary.row_count_missing_policy_tag_before_next_name, + 1 + ); +} + +#[test] +fn summarizes_placed_structure_dynamic_side_buffer_compact_prefix_patterns() { + let mut bytes = vec![0u8; 0x600]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x320usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x000038a5u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x000038a6u32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000038a7u32.to_le_bytes()); + let header_words = [ + 0u32, 0x06, 1000, 500, 1000, 388, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + let mut cursor = records_tag_offset + 4; + for (leading_dword, primary_name) in [ + (0x000055f3u32, "TunnelSTBrick_Section.3dp"), + (0x000055f3u32, "TunnelSTBrick_Cap.3dp"), + (0xff0000ffu32, "TunnelSTBrick_Cap.3dp"), + ] { + bytes[cursor..cursor + 4].copy_from_slice(&leading_dword.to_le_bytes()); + bytes[cursor + 4..cursor + 6].copy_from_slice(&0x0001u16.to_le_bytes()); + bytes[cursor + 6] = 0xff; + let name_tag_offset = cursor + 7; + bytes[name_tag_offset..name_tag_offset + 2] + .copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); + let secondary_name = "Infrastructure"; + bytes[name_tag_offset + 4] = primary_name.len() as u8; + bytes[name_tag_offset + 5..name_tag_offset + 5 + primary_name.len()] + .copy_from_slice(primary_name.as_bytes()); + let second_len_offset = name_tag_offset + 5 + primary_name.len(); + bytes[second_len_offset] = secondary_name.len() as u8; + bytes[second_len_offset + 1..second_len_offset + 1 + secondary_name.len()] + .copy_from_slice(secondary_name.as_bytes()); + cursor = second_len_offset + 1 + secondary_name.len(); + } + + let probe = parse_save_placed_structure_dynamic_side_buffer_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("placed-structure dynamic side-buffer probe should parse"); + + assert_eq!(probe.embedded_name_tag_count, 3); + assert_eq!(probe.decoded_embedded_name_row_count, 3); + assert_eq!(probe.decoded_embedded_name_row_with_tertiary_name_count, 0); + assert_eq!(probe.unique_compact_prefix_pattern_count, 2); + assert_eq!( + probe.prefix_leading_dword_matching_embedded_profile_tag_count, + 2 + ); + assert_eq!(probe.unique_embedded_name_pair_count, 2); + assert_eq!(probe.compact_prefix_pattern_summaries.len(), 2); + assert_eq!( + probe.compact_prefix_pattern_summaries[0].prefix_leading_dword_hex, + "0x000055f3" + ); + assert_eq!(probe.compact_prefix_pattern_summaries[0].count, 2); + assert_eq!( + probe.compact_prefix_pattern_summaries[0].section_like_primary_name_count, + 1 + ); + assert_eq!( + probe.compact_prefix_pattern_summaries[0].cap_like_primary_name_count, + 1 + ); + assert!( + probe.compact_prefix_pattern_summaries[0].prefix_leading_dword_matches_embedded_profile_tag + ); + assert_eq!( + probe.compact_prefix_pattern_summaries[1].prefix_leading_dword_hex, + "0xff0000ff" + ); + assert_eq!(probe.compact_prefix_pattern_summaries[1].count, 1); + assert_eq!(probe.name_pair_summaries.len(), 2); + assert_eq!(probe.name_pair_summaries[0].count, 2); + assert_eq!( + probe.name_pair_summaries[0].dominant_prefix_leading_dword_hex, + "0x000055f3" + ); + assert_eq!(probe.name_pair_summaries[1].count, 1); + let payload_envelope_summary = probe + .payload_envelope_summary + .as_ref() + .expect("payload envelope summary should be present"); + assert_eq!( + payload_envelope_summary.row_count_with_policy_tag_before_next_name, + 0 + ); + assert_eq!( + payload_envelope_summary.row_count_missing_policy_tag_before_next_name, + 3 + ); +} + +#[test] +fn parses_save_len_prefixed_ascii_name_triplet_with_optional_third_name() { + let bytes = [ + 5u8, b'F', b'i', b'r', b's', b't', 0, 6, b'S', b'e', b'c', b'o', b'n', b'd', 0, 5, b'T', + b'h', b'i', b'r', b'd', + ]; + let parsed = parse_save_len_prefixed_ascii_name_triplet(&bytes) + .expect("triplet parser should decode three len-prefixed ascii names"); + assert_eq!(parsed.0, "First"); + assert_eq!(parsed.1, "Second"); + assert_eq!(parsed.2.as_deref(), Some("Third")); +} + +#[test] +fn parses_save_len_prefixed_ascii_name_triplet_with_extended_length_prefix() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[0x80, 0x03, b'A', b'B', b'C']); + bytes.extend_from_slice(&[0, 1, b'X']); + let parsed = parse_save_len_prefixed_ascii_name_triplet(&bytes) + .expect("triplet parser should decode extended-length prefix"); + assert_eq!(parsed.0, "ABC"); + assert_eq!(parsed.1, "X"); + assert_eq!(parsed.2, None); +} + +#[test] +fn parses_save_len_prefixed_ascii_name_triplet_with_consumed_len() { + let bytes = [ + 5u8, b'F', b'i', b'r', b's', b't', 0, 6, b'S', b'e', b'c', b'o', b'n', b'd', 0, 5, b'T', + b'h', b'i', b'r', b'd', 0xff, + ]; + let (parsed, consumed_len) = + parse_save_len_prefixed_ascii_name_triplet_and_consumed_len(&bytes) + .expect("triplet parser should decode consumed len"); + assert_eq!(parsed.0, "First"); + assert_eq!(parsed.1, "Second"); + assert_eq!(parsed.2.as_deref(), Some("Third")); + assert_eq!(consumed_len, 21); +} + +#[test] +fn aligns_placed_structure_dynamic_side_buffer_name_pairs_with_triplets() { + let side_buffer = SmpSavePlacedStructureDynamicSideBufferProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(), + semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records".to_string(), + metadata_tag_offset: 0, + records_tag_offset: 0, + close_tag_offset: 0, + records_span_len: 0, + direct_record_stride: 6, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 1000, + live_id_bound_hex: "0x000003e8".to_string(), + live_record_count: 10, + live_record_count_hex: "0x0000000a".to_string(), + owner_shared_dword: 0, + owner_shared_dword_hex: "0x00000000".to_string(), + owner_shared_dword_relative_offset: 0, + owner_shared_dword_matches_first_compact_prefix_leading_dword: true, + first_record_child_count_after_owner_shared: None, + first_record_child_count_after_owner_shared_hex: None, + first_record_saved_primary_child_byte_after_owner_shared: None, + first_record_saved_primary_child_byte_after_owner_shared_hex: None, + first_record_first_name_tag_relative_offset_after_owner_shared: None, + prefix_leading_dword: 0, + prefix_leading_dword_hex: "0x00000000".to_string(), + prefix_trailing_word: 1, + prefix_trailing_word_hex: "0x0001".to_string(), + prefix_separator_byte: 0xff, + prefix_separator_byte_hex: "0xff".to_string(), + first_embedded_name_tag_relative_offset: 7, + embedded_name_tag_count: 3, + decoded_embedded_name_row_count: 3, + decoded_embedded_name_row_with_tertiary_name_count: 0, + unique_compact_prefix_pattern_count: 2, + prefix_leading_dword_matching_embedded_profile_tag_count: 2, + unique_embedded_name_pair_count: 2, + first_embedded_primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), + first_embedded_secondary_name: Some("Infrastructure".to_string()), + first_embedded_tertiary_name: None, + embedded_name_row_samples: vec![], + compact_prefix_pattern_summaries: vec![], + name_pair_summaries: vec![ + SmpSavePlacedStructureDynamicSideBufferNamePairSummary { + primary_name: "TunnelSTBrick_Section.3dp".to_string(), + secondary_name: "Infrastructure".to_string(), + count: 2, + first_name_tag_relative_offset: 7, + unique_compact_prefix_pattern_count: 1, + dominant_prefix_leading_dword: 0x55f3, + dominant_prefix_leading_dword_hex: "0x000055f3".to_string(), + dominant_prefix_trailing_word: 1, + dominant_prefix_trailing_word_hex: "0x0001".to_string(), + dominant_prefix_separator_byte: 0xff, + dominant_prefix_separator_byte_hex: "0xff".to_string(), + dominant_prefix_count: 2, + }, + SmpSavePlacedStructureDynamicSideBufferNamePairSummary { + primary_name: "BridgeSTWood_Section.3dp".to_string(), + secondary_name: "Infrastructure".to_string(), + count: 1, + first_name_tag_relative_offset: 27, + unique_compact_prefix_pattern_count: 1, + dominant_prefix_leading_dword: 0xff000000, + dominant_prefix_leading_dword_hex: "0xff000000".to_string(), + dominant_prefix_trailing_word: 1, + dominant_prefix_trailing_word_hex: "0x0001".to_string(), + dominant_prefix_separator_byte: 0xff, + dominant_prefix_separator_byte_hex: "0xff".to_string(), + dominant_prefix_count: 1, + }, + ], + payload_envelope_summary: None, + live_entry_prelude_summary: None, + evidence: vec![], + }; + let triplets = SmpSavePlacedStructureRecordTripletProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-placed-structure-record-triplets".to_string(), + semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), + records_tag_offset: 0, + close_tag_offset: 0, + record_count: 2, + entries: vec![ + SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: 0, + primary_name: "TunnelSTBrick_Section.3dp".to_string(), + secondary_name: "Infrastructure".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0, + profile_tag_relative_offset: 0, + policy_chunk_len: 0, + profile_chunk_len: 0, + policy_f32_lane_0: 0.0, + policy_f32_lane_1: 0.0, + policy_f32_lane_2: 0.0, + policy_f32_lane_3: 0.0, + policy_f32_lane_4: 0.0, + policy_reserved_dword: 0, + policy_trailing_word: 0, + policy_trailing_word_hex: "0x0000".to_string(), + profile_open_marker: 0, + profile_open_marker_hex: "0x00000000".to_string(), + profile_repeated_primary_name: "TunnelSTBrick_Section.3dp".to_string(), + profile_repeated_secondary_name: "Infrastructure".to_string(), + profile_footer_relative_offset: 0, + profile_footer_relative_offset_hex: "0x0".to_string(), + profile_pre_footer_padding_len: 0, + profile_pre_footer_padding_hex_bytes: Vec::new(), + profile_companion_byte_u8: None, + profile_companion_byte_hex: None, + profile_payload_dword: 0, + profile_payload_dword_hex: "0x00000000".to_string(), + profile_sentinel_i32: -1, + profile_status_kind: "unset".to_string(), + farm_growth_stage_index: None, + profile_close_marker: 0, + profile_close_marker_hex: "0x00000000".to_string(), + }, + SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: 1, + primary_name: "TrackCapST_Cap.3dp".to_string(), + secondary_name: "Infrastructure".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0, + profile_tag_relative_offset: 0, + policy_chunk_len: 0, + profile_chunk_len: 0, + policy_f32_lane_0: 0.0, + policy_f32_lane_1: 0.0, + policy_f32_lane_2: 0.0, + policy_f32_lane_3: 0.0, + policy_f32_lane_4: 0.0, + policy_reserved_dword: 0, + policy_trailing_word: 0, + policy_trailing_word_hex: "0x0000".to_string(), + profile_open_marker: 0, + profile_open_marker_hex: "0x00000000".to_string(), + profile_repeated_primary_name: "TrackCapST_Cap.3dp".to_string(), + profile_repeated_secondary_name: "Infrastructure".to_string(), + profile_footer_relative_offset: 0, + profile_footer_relative_offset_hex: "0x0".to_string(), + profile_pre_footer_padding_len: 0, + profile_pre_footer_padding_hex_bytes: Vec::new(), + profile_companion_byte_u8: None, + profile_companion_byte_hex: None, + profile_payload_dword: 0, + profile_payload_dword_hex: "0x00000000".to_string(), + profile_sentinel_i32: -1, + profile_status_kind: "unset".to_string(), + farm_growth_stage_index: None, + profile_close_marker: 0, + profile_close_marker_hex: "0x00000000".to_string(), + }, + ], + evidence: vec![], + }; + + let alignment = + summarize_placed_structure_dynamic_side_buffer_alignment(&side_buffer, &triplets); + + assert_eq!(alignment.unique_side_buffer_name_pair_count, 2); + assert_eq!(alignment.unique_triplet_name_pair_count, 2); + assert_eq!(alignment.overlapping_name_pair_count, 1); + assert_eq!( + alignment.side_buffer_rows_with_matching_triplet_name_pair_count, + 2 + ); + assert_eq!( + alignment.side_buffer_rows_without_matching_triplet_name_pair_count, + 1 + ); + assert_eq!( + alignment.triplet_name_pairs_without_side_buffer_match_count, + 1 + ); + assert_eq!(alignment.matched_name_pair_samples.len(), 1); + assert_eq!(alignment.unmatched_side_buffer_name_pair_samples.len(), 1); +} + +#[test] +fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x180usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x000036b1u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x000036b2u32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000036b3u32.to_le_bytes()); + let header_words = [ + 0u32, 0x06, 0x0a, 0x14, 0x7ee, 0x7ea, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + + let probe = parse_save_placed_structure_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("placed structure header probe should parse"); + + assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); + assert_eq!(probe.records_tag_offset, records_tag_offset); + assert_eq!(probe.close_tag_offset, close_tag_offset); + assert_eq!(probe.direct_collection_flag, 0); + assert_eq!(probe.direct_record_stride, 0x06); + assert_eq!(probe.live_id_bound, 0x7ee); + assert_eq!(probe.live_record_count, 0x7ea); +} + +#[test] +fn scans_unclassified_tagged_collection_header_probe_from_adjacent_low_tags() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x1c0usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00007001u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x00007002u32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x00007003u32.to_le_bytes()); + let header_words = [ + 0u32, 0x12, 0x0a, 0x14, 0x90, 0x78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + + let probes = scan_save_unclassified_tagged_collection_header_probes( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ); + + let probe = probes + .iter() + .find(|probe| probe.metadata_tag == 0x7001) + .expect("should include synthetic unclassified tag family"); + assert_eq!(probe.records_tag, 0x7002); + assert_eq!(probe.close_tag, 0x7003); + assert_eq!(probe.direct_record_stride, 0x12); + assert_eq!(probe.live_id_bound, 0x90); + assert_eq!(probe.live_record_count, 0x78); + assert_eq!( + probe.records_span_len, + close_tag_offset - (records_tag_offset + 4) + ); +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/support.rs b/crates/rrt-runtime/src/inspect/smp/tests/support.rs new file mode 100644 index 0000000..4dd56ac --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/support.rs @@ -0,0 +1,238 @@ +use super::*; + +pub(super) fn encode_len_prefixed_string(text: &str) -> Vec { + let mut bytes = Vec::with_capacity(1 + text.len()); + bytes.push(text.len() as u8); + bytes.extend_from_slice(text.as_bytes()); + bytes +} + +pub(super) fn encode_template( + record_id: u32, + trigger_kind: u8, + flags: u8, + actions: &[Vec], +) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC); + bytes.extend_from_slice(&record_id.to_le_bytes()); + bytes.push(trigger_kind); + bytes.push(flags); + bytes.push(actions.len() as u8); + bytes.push(0); + for action in actions { + bytes.extend_from_slice(action); + } + bytes +} + +pub(super) fn encode_action_set_world_flag(key: &str, value: bool) -> Vec { + let mut bytes = vec![0x01]; + bytes.extend_from_slice(&encode_len_prefixed_string(key)); + bytes.push(u8::from(value)); + bytes +} + +pub(super) fn encode_action_set_special_condition(label: &str, value: u32) -> Vec { + let mut bytes = vec![0x05]; + bytes.extend_from_slice(&encode_len_prefixed_string(label)); + bytes.extend_from_slice(&value.to_le_bytes()); + bytes +} + +pub(super) fn encode_action_adjust_company_cash_ids(ids: &[u32], delta: i64) -> Vec { + let mut bytes = vec![0x02, 0x01, ids.len() as u8]; + for id in ids { + bytes.extend_from_slice(&id.to_le_bytes()); + } + bytes.extend_from_slice(&delta.to_le_bytes()); + bytes +} + +pub(super) fn encode_action_append_template(template: Vec) -> Vec { + let mut bytes = vec![0x06]; + bytes.extend_from_slice(&(template.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&template); + bytes +} + +pub(super) fn build_synthetic_event_record( + trigger_kind: u8, + flags: u8, + standalone_count: u8, + grouped_counts: [u8; 4], + text_bands: [&[u8]; 6], + actions: &[Vec], +) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC); + bytes.push(trigger_kind); + bytes.push(flags); + bytes.push(standalone_count); + bytes.push(actions.len() as u8); + bytes.extend_from_slice(&grouped_counts); + for band in text_bands { + bytes.extend_from_slice(&(band.len() as u16).to_le_bytes()); + bytes.extend_from_slice(band); + } + for action in actions { + bytes.extend_from_slice(action); + } + bytes +} + +pub(super) fn encode_real_optional_string(text: &str) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(text.len() as u16).to_le_bytes()); + bytes.extend_from_slice(text.as_bytes()); + bytes +} + +pub(super) fn build_real_condition_row( + raw_condition_id: i32, + subtype: u8, + flag_seed: u8, + candidate_name: Option<&str>, +) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(raw_condition_id as u32).to_le_bytes()); + bytes.push(subtype); + while bytes.len() < PACKED_EVENT_REAL_CONDITION_ROW_LEN { + bytes.push(flag_seed.wrapping_add(bytes.len() as u8)); + } + match candidate_name { + Some(text) => bytes.extend_from_slice(&encode_real_optional_string(text)), + None => bytes.extend_from_slice(&0u16.to_le_bytes()), + } + bytes +} + +pub(super) fn build_real_condition_row_with_threshold( + raw_condition_id: i32, + subtype: u8, + threshold: i32, + candidate_name: Option<&str>, +) -> Vec { + let mut bytes = build_real_condition_row(raw_condition_id, subtype, 0, candidate_name); + bytes[5..9].copy_from_slice(&threshold.to_le_bytes()); + bytes +} + +pub(super) struct RealGroupedEffectRowSpec<'a> { + pub(super) descriptor_id: u32, + pub(super) opcode: u8, + pub(super) raw_scalar_value: i32, + pub(super) value_byte_0x09: u8, + pub(super) value_dword_0x0d: u32, + pub(super) value_byte_0x11: u8, + pub(super) value_byte_0x12: u8, + pub(super) value_word_0x14: u16, + pub(super) value_word_0x16: u16, + pub(super) locomotive_name: Option<&'a str>, +} + +pub(super) fn build_real_grouped_effect_row(spec: RealGroupedEffectRowSpec<'_>) -> Vec { + let mut bytes = vec![0; PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN]; + bytes[0..4].copy_from_slice(&spec.descriptor_id.to_le_bytes()); + bytes[4..8].copy_from_slice(&(spec.raw_scalar_value as u32).to_le_bytes()); + bytes[8] = spec.opcode; + bytes[9] = spec.value_byte_0x09; + bytes[0x0d..0x11].copy_from_slice(&spec.value_dword_0x0d.to_le_bytes()); + bytes[0x11] = spec.value_byte_0x11; + bytes[0x12] = spec.value_byte_0x12; + bytes[0x14..0x16].copy_from_slice(&spec.value_word_0x14.to_le_bytes()); + bytes[0x16..0x18].copy_from_slice(&spec.value_word_0x16.to_le_bytes()); + match spec.locomotive_name { + Some(text) => bytes.extend_from_slice(&encode_real_optional_string(text)), + None => bytes.extend_from_slice(&0u16.to_le_bytes()), + } + bytes +} + +#[derive(Clone, Copy)] +pub(super) struct RealCompactControlSpec { + pub(super) mode_byte_0x7ef: u8, + pub(super) primary_selector_0x7f0: u32, + pub(super) grouped_mode_0x7f4: u8, + pub(super) one_shot_header_0x7f5: u32, + pub(super) modifier_flag_0x7f9: u8, + pub(super) modifier_flag_0x7fa: u8, + pub(super) grouped_target_scope_ordinals_0x7fb: [u8; PACKED_EVENT_REAL_GROUP_COUNT], + pub(super) grouped_scope_checkboxes_0x7ff: [u8; PACKED_EVENT_REAL_GROUP_COUNT], + pub(super) summary_toggle_0x800: u8, + pub(super) grouped_territory_selectors_0x80f: [i32; PACKED_EVENT_REAL_GROUP_COUNT], +} + +pub(super) fn build_real_compact_control(spec: RealCompactControlSpec) -> Vec { + let mut bytes = Vec::with_capacity(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN); + bytes.push(spec.mode_byte_0x7ef); + bytes.extend_from_slice(&spec.primary_selector_0x7f0.to_le_bytes()); + bytes.push(spec.grouped_mode_0x7f4); + bytes.extend_from_slice(&spec.one_shot_header_0x7f5.to_le_bytes()); + bytes.push(spec.modifier_flag_0x7f9); + bytes.push(spec.modifier_flag_0x7fa); + bytes.extend_from_slice(&spec.grouped_target_scope_ordinals_0x7fb); + bytes.extend_from_slice(&spec.grouped_scope_checkboxes_0x7ff); + bytes.push(spec.summary_toggle_0x800); + for selector in spec.grouped_territory_selectors_0x80f { + bytes.extend_from_slice(&selector.to_le_bytes()); + } + bytes +} + +pub(super) fn build_real_event_record( + text_bands: [&[u8]; 6], + compact_control: Option, + condition_rows: &[Vec], + grouped_rows: [&[Vec]; 4], +) -> Vec { + let mut bytes = Vec::new(); + for band in text_bands { + bytes.extend_from_slice(&(band.len() as u16).to_le_bytes()); + bytes.extend_from_slice(band); + } + if let Some(spec) = compact_control { + bytes.extend_from_slice(&build_real_compact_control(spec)); + } + bytes.extend_from_slice(&PACKED_EVENT_REAL_CONDITION_MARKER.to_le_bytes()); + bytes.extend_from_slice(&(condition_rows.len() as u16).to_le_bytes()); + for row in condition_rows { + bytes.extend_from_slice(row); + } + bytes.extend_from_slice(&PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER.to_le_bytes()); + for rows in grouped_rows { + bytes.extend_from_slice(&(rows.len() as u16).to_le_bytes()); + } + for rows in grouped_rows { + for row in rows { + bytes.extend_from_slice(row); + } + } + bytes +} + +pub(super) fn empty_analysis_report() -> SmpSaveCompanyChairmanAnalysisReport { + SmpSaveCompanyChairmanAnalysisReport { + profile_family: "rt3-105-scenario-save-container-v1".to_string(), + selected_company_id: None, + selected_chairman_profile_id: None, + world_selection_context: None, + world_issue_37: None, + world_economic_tuning: None, + world_finance_neighborhood: None, + train_collection_header: None, + train_collection_directory: None, + region_collection_header: None, + region_record_triplets: None, + region_queued_notice_records: None, + region_fixed_row_run_candidates: None, + placed_structure_collection_header: None, + placed_structure_record_triplets: None, + placed_structure_dynamic_side_buffer: None, + placed_structure_dynamic_side_buffer_alignment: None, + unclassified_tagged_collection_headers: Vec::new(), + company_entries: Vec::new(), + chairman_entries: Vec::new(), + notes: Vec::new(), + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/tests/world.rs b/crates/rrt-runtime/src/inspect/smp/tests/world.rs new file mode 100644 index 0000000..61a53cd --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/tests/world.rs @@ -0,0 +1,1401 @@ +use super::*; +use crate::event::metrics::{ + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN, +}; + +#[test] +fn parses_save_world_selection_context_probe_from_fixed_world_block() { + let mut bytes = vec![0u8; 0x8000]; + let chunk_tag_offset = 0x3ceusize; + let payload_offset = chunk_tag_offset + 4; + bytes[chunk_tag_offset..chunk_tag_offset + 4] + .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes()); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET + ..payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET + 4] + .copy_from_slice(&7u32.to_le_bytes()); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET + ..payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET + 4] + .copy_from_slice(&9u32.to_le_bytes()); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET + ..payload_offset + + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET + + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT] + .copy_from_slice(&[3, 1, 4, 1, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET] = 1; + for (slot_index, role_gate) in [2u8, 1, 0, 2].into_iter().enumerate() { + bytes[payload_offset + + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET + + slot_index * RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE] = role_gate; + } + let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; + bytes[next_chunk_offset..next_chunk_offset + 4] + .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); + + let probe = parse_save_world_selection_context_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("selection-context probe should parse"); + + assert_eq!(probe.chunk_tag_offset, chunk_tag_offset); + assert_eq!(probe.payload_offset, payload_offset); + assert_eq!(probe.selected_company_id, 7); + assert_eq!(probe.selected_chairman_profile_id, 9); + assert_eq!(probe.chairman_slot_selectors[..6], [3, 1, 4, 1, 5, 9]); + assert_eq!(probe.campaign_override_flag, 1); + assert_eq!(probe.chairman_role_gate_bytes[..4], [2, 1, 0, 2]); +} + +#[test] +fn parses_save_world_economic_tuning_probe_from_fixed_world_block() { + let mut bytes = vec![0u8; 0x8000]; + let chunk_tag_offset = 0x3ceusize; + let payload_offset = chunk_tag_offset + 4; + bytes[chunk_tag_offset..chunk_tag_offset + 4] + .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes()); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET + ..payload_offset + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET + 4] + .copy_from_slice(&1.5f32.to_bits().to_le_bytes()); + for (lane_index, relative_offset) in + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS + .iter() + .copied() + .enumerate() + { + let value = (lane_index as f32) + 10.25f32; + bytes[payload_offset + relative_offset..payload_offset + relative_offset + 4] + .copy_from_slice(&value.to_bits().to_le_bytes()); + } + let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; + bytes[next_chunk_offset..next_chunk_offset + 4] + .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); + + let probe = parse_save_world_economic_tuning_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("world economic tuning probe should parse"); + + assert_eq!(probe.chunk_tag_offset, chunk_tag_offset); + assert_eq!(probe.payload_offset, payload_offset); + assert_eq!(probe.mirror_lane.relative_offset_hex, "0xbda"); + assert_eq!(probe.mirror_lane.value_f32, 1.5); + assert_eq!(probe.tuning_lanes.len(), 6); + assert_eq!(probe.tuning_lanes[0].relative_offset_hex, "0xbde"); + assert_eq!(probe.tuning_lanes[0].value_f32, 10.25); + assert_eq!(probe.tuning_lanes[5].relative_offset_hex, "0xbf2"); + assert_eq!(probe.tuning_lanes[5].value_f32, 15.25); +} + +#[test] +fn parses_save_world_issue_37_probe_from_fixed_world_block() { + let mut bytes = vec![0u8; 0x8000]; + let chunk_tag_offset = 0x3ceusize; + let payload_offset = chunk_tag_offset + 4; + bytes[chunk_tag_offset..chunk_tag_offset + 4] + .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes()); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + ..payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 4] + .copy_from_slice(&3u32.to_le_bytes()); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET + ..payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET + 4] + .copy_from_slice(&0.06f32.to_bits().to_le_bytes()); + let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; + bytes[next_chunk_offset..next_chunk_offset + 4] + .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); + + let probe = parse_save_world_issue_37_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("world issue-0x37 probe should parse"); + + assert_eq!(probe.chunk_tag_offset, chunk_tag_offset); + assert_eq!(probe.payload_offset, payload_offset); + assert_eq!(probe.issue_value_lane.relative_offset_hex, "0x29"); + assert_eq!(probe.issue_value_lane.value_i32, 3); + assert_eq!(probe.issue_37_raw_u8, 3); + assert_eq!(probe.issue_37_raw_hex, "0x03"); + assert_eq!(probe.issue_38_raw_u8, 0); + assert_eq!(probe.issue_38_raw_hex, "0x00"); + assert_eq!(probe.issue_39_raw_u8, 0); + assert_eq!(probe.issue_39_raw_hex, "0x00"); + assert_eq!(probe.issue_3a_raw_u8, 0); + assert_eq!(probe.issue_3a_raw_hex, "0x00"); + assert_eq!(probe.multiplier_lane.relative_offset_hex, "0x25"); + assert!((probe.multiplier_lane.value_f32 - 0.06).abs() < f32::EPSILON); +} + +#[test] +fn parses_save_world_finance_neighborhood_probe_from_fixed_world_block() { + let mut bytes = vec![0u8; 0x8000]; + let chunk_tag_offset = 0x3ceusize; + let payload_offset = chunk_tag_offset + 4; + bytes[chunk_tag_offset..chunk_tag_offset + 4] + .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes()); + for index in 0..RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS { + let relative_offset = + RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET + index * 4; + bytes[payload_offset + relative_offset..payload_offset + relative_offset + 4] + .copy_from_slice(&((index as u32) + 1).to_le_bytes()); + } + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET] = 1; + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET] = 2; + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET] = 3; + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET] = 4; + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET + ..payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET + 4] + .copy_from_slice(&2u32.to_le_bytes()); + let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; + bytes[next_chunk_offset..next_chunk_offset + 4] + .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); + + let probe = parse_save_world_finance_neighborhood_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("world finance neighborhood probe should parse"); + + assert_eq!(probe.chunk_tag_offset, chunk_tag_offset); + assert_eq!(probe.payload_offset, payload_offset); + assert_eq!( + probe.dword_candidates.len(), + RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS + ); + assert_eq!( + probe.current_calendar_tuple_word_lane.relative_offset_hex, + "0xd" + ); + assert_eq!(probe.packed_year_word_raw_u16, 1); + assert_eq!(probe.packed_year_word_raw_hex, "0x0001"); + assert_eq!(probe.partial_year_progress_raw_u8, 0); + assert_eq!(probe.partial_year_progress_raw_hex, "0x00"); + assert_eq!(probe.stock_policy_raw_u8, 1); + assert_eq!(probe.stock_policy_raw_hex, "0x01"); + assert_eq!(probe.bond_policy_raw_u8, 2); + assert_eq!(probe.bond_policy_raw_hex, "0x02"); + assert_eq!(probe.bankruptcy_policy_raw_u8, 3); + assert_eq!(probe.bankruptcy_policy_raw_hex, "0x03"); + assert_eq!(probe.dividend_policy_raw_u8, 4); + assert_eq!(probe.dividend_policy_raw_hex, "0x04"); + assert_eq!(probe.building_density_growth_setting_lane.raw_u32, 2); + assert_eq!( + probe + .building_density_growth_setting_lane + .relative_offset_hex, + "0x4c78" + ); + assert_eq!(probe.current_calendar_tuple_word_lane.value_i32, 1); + assert_eq!( + probe.current_calendar_tuple_word_2_lane.relative_offset_hex, + "0x11" + ); + assert_eq!(probe.current_calendar_tuple_word_2_lane.value_i32, 2); + assert_eq!(probe.absolute_counter_lane.relative_offset_hex, "0x15"); + assert_eq!(probe.absolute_counter_lane.value_i32, 3); + assert_eq!( + probe.absolute_counter_mirror_lane.relative_offset_hex, + "0x19" + ); + assert_eq!(probe.absolute_counter_mirror_lane.value_i32, 4); + assert_eq!( + probe.dword_candidates[0].label, + "current_calendar_tuple_word" + ); + assert_eq!(probe.dword_candidates[0].relative_offset_hex, "0xd"); + assert_eq!(probe.dword_candidates[0].value_i32, 1); + assert_eq!(probe.dword_candidates[6].label, "issue_0x37_multiplier"); + assert_eq!(probe.dword_candidates[6].relative_offset_hex, "0x25"); + assert_eq!( + probe.dword_candidates[10].label, + "issue_neighbor_candidate_2" + ); + assert_eq!(probe.dword_candidates[10].relative_offset_hex, "0x35"); + assert_eq!(probe.dword_candidates[10].value_i32, 11); + assert_eq!( + probe.dword_candidates[11].label, + "finance_neighborhood_word_12" + ); + assert_eq!(probe.dword_candidates[11].relative_offset_hex, "0x39"); + assert_eq!(probe.dword_candidates[11].value_i32, 12); + assert_eq!( + probe.dword_candidates[16].label, + "finance_neighborhood_word_17" + ); + assert_eq!(probe.dword_candidates[16].relative_offset_hex, "0x4d"); + assert_eq!(probe.dword_candidates[16].value_i32, 17); +} + +#[test] +fn loads_selection_only_company_and_chairman_context_from_save_world_probe() { + let mut report = inspect_smp_bytes(&[]); + report.save_load_summary = Some(SmpSaveLoadSummary { + file_extension_hint: Some("gms".to_string()), + container_profile_family: Some("rt3-105-save-container-v1".to_string()), + mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), + mechanism_confidence: "mixed".to_string(), + packed_profile_kind: None, + packed_profile_family: None, + packed_profile_offset: None, + packed_profile_len: None, + map_path: None, + display_name: None, + profile_byte_0x77: None, + profile_byte_0x77_hex: None, + profile_byte_0x82: None, + profile_byte_0x82_hex: None, + profile_byte_0x97: None, + profile_byte_0x97_hex: None, + profile_byte_0xc5: None, + profile_byte_0xc5_hex: None, + trailer_family: Some("rt3-105-save-trailer-v1".to_string()), + bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), + candidate_table: None, + notes: vec![], + }); + report.save_world_selection_context_probe = Some(SmpSaveWorldSelectionContextProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-selected-company-and-chairman-context".to_string(), + chunk_tag_offset: 0x3ce, + payload_offset: 0x3d2, + payload_len: RT3_SAVE_WORLD_BLOCK_LEN, + payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), + selected_company_id_offset: 0x3ef, + selected_company_id: 1, + selected_company_id_hex: "0x00000001".to_string(), + selected_chairman_profile_id_offset: 0x3f3, + selected_chairman_profile_id: 9, + selected_chairman_profile_id_hex: "0x00000009".to_string(), + chairman_slot_selector_offset: 0x455, + chairman_slot_selectors: vec![3, 1, 4, 1, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + campaign_override_flag_offset: 0x493, + campaign_override_flag: 1, + campaign_override_flag_hex: "0x01".to_string(), + chairman_role_gate_offset: 0xf91, + chairman_role_gate_bytes: vec![2, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + evidence: vec![], + }); + report.save_world_issue_37_probe = Some(SmpSaveWorldIssue37Probe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-save-world-issue-0x37".to_string(), + chunk_tag_offset: 0x3ce, + payload_offset: 0x3d2, + payload_len: RT3_SAVE_WORLD_BLOCK_LEN, + payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), + issue_37_raw_u8: 3, + issue_37_raw_hex: "0x03".to_string(), + issue_38_raw_u8: 1, + issue_38_raw_hex: "0x01".to_string(), + issue_39_raw_u8: 2, + issue_39_raw_hex: "0x02".to_string(), + issue_3a_raw_u8: 4, + issue_3a_raw_hex: "0x04".to_string(), + issue_value_lane: SmpSaveDwordCandidate { + label: "issue_0x37_value".to_string(), + relative_offset: 0x29, + relative_offset_hex: "0x29".to_string(), + raw_u32: 3, + raw_u32_hex: "0x00000003".to_string(), + value_i32: 3, + value_f32: f32::from_bits(3), + }, + multiplier_lane: SmpSaveDwordCandidate { + label: "issue_0x37_multiplier".to_string(), + relative_offset: 0x25, + relative_offset_hex: "0x25".to_string(), + raw_u32: 0x3d75c28f, + raw_u32_hex: "0x3d75c28f".to_string(), + value_i32: 1031127695, + value_f32: 0.06, + }, + issue_opinion_base_terms_raw_i32: Vec::new(), + evidence: vec![], + }); + report.save_world_economic_tuning_probe = Some(SmpSaveWorldEconomicTuningProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-save-world-economic-tuning".to_string(), + chunk_tag_offset: 0x3ce, + payload_offset: 0x3d2, + payload_len: RT3_SAVE_WORLD_BLOCK_LEN, + payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), + mirror_lane: SmpSaveDwordCandidate { + label: "economic_tuning_mirror_lane_0".to_string(), + relative_offset: 0xbda, + relative_offset_hex: "0xbda".to_string(), + raw_u32: 0x3f46d093, + raw_u32_hex: "0x3f46d093".to_string(), + value_i32: 1061605523, + value_f32: 0.7766201, + }, + tuning_lanes: vec![ + SmpSaveDwordCandidate { + label: "economic_tuning_lane_0".to_string(), + relative_offset: 0xbde, + relative_offset_hex: "0xbde".to_string(), + raw_u32: 0x3f400000, + raw_u32_hex: "0x3f400000".to_string(), + value_i32: 1061158912, + value_f32: 0.75, + }, + SmpSaveDwordCandidate { + label: "economic_tuning_lane_1".to_string(), + relative_offset: 0xbe2, + relative_offset_hex: "0xbe2".to_string(), + raw_u32: 0x3be56042, + raw_u32_hex: "0x3be56042".to_string(), + value_i32: 1004888130, + value_f32: 0.007, + }, + ], + evidence: vec![], + }); + report.save_company_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-company-tagged-header-counts".to_string(), + semantic_family: "scenario-save-company-header-counts".to_string(), + metadata_tag_offset: 0x1000, + records_tag_offset: 0x1100, + close_tag_offset: 0x1200, + direct_collection_flag: 1, + direct_collection_flag_hex: "0x00000001".to_string(), + direct_record_stride: 0x7684, + direct_record_stride_hex: "0x00007684".to_string(), + live_id_bound: 5, + live_id_bound_hex: "0x00000005".to_string(), + live_record_count: 1, + live_record_count_hex: "0x00000001".to_string(), + header_words: vec![1, 0x7684, 5, 5, 5, 1], + header_hex_words: vec![ + "0x00000001".to_string(), + "0x00007684".to_string(), + "0x00000005".to_string(), + "0x00000005".to_string(), + "0x00000005".to_string(), + "0x00000001".to_string(), + ], + evidence: vec![], + }); + report.save_chairman_profile_collection_header_probe = + Some(SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-chairman-profile-tagged-header-counts".to_string(), + semantic_family: "scenario-save-chairman-profile-header-counts".to_string(), + metadata_tag_offset: 0x2000, + records_tag_offset: 0x2100, + close_tag_offset: 0x2200, + direct_collection_flag: 1, + direct_collection_flag_hex: "0x00000001".to_string(), + direct_record_stride: 0xcab, + direct_record_stride_hex: "0x00000cab".to_string(), + live_id_bound: 8, + live_id_bound_hex: "0x00000008".to_string(), + live_record_count: 2, + live_record_count_hex: "0x00000002".to_string(), + header_words: vec![1, 0xcab, 8, 6, 8, 2], + header_hex_words: vec![ + "0x00000001".to_string(), + "0x00000cab".to_string(), + "0x00000008".to_string(), + "0x00000006".to_string(), + "0x00000008".to_string(), + "0x00000002".to_string(), + ], + evidence: vec![], + }); + report.save_train_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-train-tagged-header-counts".to_string(), + semantic_family: "scenario-save-train-header-counts".to_string(), + metadata_tag_offset: 0x3000, + records_tag_offset: 0x3100, + close_tag_offset: 0x3200, + direct_collection_flag: 1, + direct_collection_flag_hex: "0x00000001".to_string(), + direct_record_stride: 0x1d5, + direct_record_stride_hex: "0x000001d5".to_string(), + live_id_bound: 0x32, + live_id_bound_hex: "0x00000032".to_string(), + live_record_count: 0x14, + live_record_count_hex: "0x00000014".to_string(), + header_words: vec![1, 0x1d5, 0x32, 0x14, 0x32, 0x14], + header_hex_words: vec![ + "0x00000001".to_string(), + "0x000001d5".to_string(), + "0x00000032".to_string(), + "0x00000014".to_string(), + "0x00000032".to_string(), + "0x00000014".to_string(), + ], + evidence: vec![], + }); + report.save_train_collection_directory_probe = Some(SmpSaveTrainCollectionDirectoryProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-train-live-directory".to_string(), + semantic_family: "scenario-save-train-live-directory".to_string(), + metadata_tag_offset: 0x3000, + records_tag_offset: 0x3100, + close_tag_offset: 0x3200, + directory_root_dword_index: 16, + directory_entry_dword_count: 3, + live_record_count: 0x14, + live_id_bound: 0x32, + chain_head_live_entry_id: Some(1), + chain_tail_live_entry_id: Some(20), + entries: vec![ + SmpSaveTrainCollectionDirectoryEntryProbe { + live_entry_id: 1, + payload_relative_offset: 0x2af8, + payload_relative_offset_hex: "0x00002af8".to_string(), + payload_absolute_offset: 0x5afc, + previous_live_entry_id: 0, + previous_live_entry_id_hex: "0x00000000".to_string(), + next_live_entry_id: 2, + next_live_entry_id_hex: "0x00000002".to_string(), + }, + SmpSaveTrainCollectionDirectoryEntryProbe { + live_entry_id: 2, + payload_relative_offset: 0x2ee0, + payload_relative_offset_hex: "0x00002ee0".to_string(), + payload_absolute_offset: 0x5ee4, + previous_live_entry_id: 1, + previous_live_entry_id_hex: "0x00000001".to_string(), + next_live_entry_id: 0, + next_live_entry_id_hex: "0x00000000".to_string(), + }, + ], + evidence: vec![], + }); + report.save_region_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-region-tagged-header-counts".to_string(), + semantic_family: "scenario-save-region-header-counts".to_string(), + metadata_tag_offset: 0x5000, + records_tag_offset: 0x5100, + close_tag_offset: 0x5200, + direct_collection_flag: 0, + direct_collection_flag_hex: "0x00000000".to_string(), + direct_record_stride: 0x06, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 0x96, + live_id_bound_hex: "0x00000096".to_string(), + live_record_count: 0x91, + live_record_count_hex: "0x00000091".to_string(), + header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x91], + header_hex_words: vec![ + "0x00000000".to_string(), + "0x00000006".to_string(), + "0x0000000a".to_string(), + "0x00000014".to_string(), + "0x00000096".to_string(), + "0x00000091".to_string(), + ], + evidence: vec![], + }); + report.save_region_record_triplet_probe = Some(SmpSaveRegionRecordTripletProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-region-record-triplets".to_string(), + semantic_family: "scenario-save-region-record-triplets".to_string(), + records_tag_offset: 0x5100, + close_tag_offset: 0x5200, + record_count: 2, + entries: vec![ + SmpSaveRegionRecordTripletEntryProbe { + record_index: 0, + name: "Marker09".to_string(), + record_payload_relative_offset: 0, + record_payload_relative_offset_hex: "0x0".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0x10, + profile_tag_relative_offset: 0x2e, + pre_name_prefix_len: 0, + pre_name_prefix_hex_bytes: Vec::new(), + pre_name_prefix_dword_candidates: Vec::new(), + policy_chunk_len: 0x1a, + profile_chunk_len: 0x40, + policy_leading_f32_0: 368.0, + policy_leading_f32_1: 0.0, + policy_leading_f32_2: 92.0, + policy_reserved_dwords: vec![0, 0, 0], + policy_reserved_dword_candidates: Vec::new(), + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_collection: Some(SmpSaveRegionProfileCollectionProbe { + direct_collection_flag: 1, + entry_stride: 0x22, + live_id_bound: 18, + live_record_count: 17, + entry_start_relative_offset: 0x4d, + trailing_padding_len: 2, + entries: vec![ + SmpSaveRegionProfileEntryProbe { + entry_index: 0, + row_relative_offset: 0x4d, + name: "House".to_string(), + trailing_weight_f32: 0.2, + }, + SmpSaveRegionProfileEntryProbe { + entry_index: 1, + row_relative_offset: 0x6f, + name: "Farm Corn".to_string(), + trailing_weight_f32: 0.2, + }, + ], + }), + }, + SmpSaveRegionRecordTripletEntryProbe { + record_index: 1, + name: "Marker10".to_string(), + record_payload_relative_offset: 0x6e, + record_payload_relative_offset_hex: "0x6e".to_string(), + name_tag_relative_offset: 0x6e, + policy_tag_relative_offset: 0x7e, + profile_tag_relative_offset: 0x9c, + pre_name_prefix_len: 0, + pre_name_prefix_hex_bytes: Vec::new(), + pre_name_prefix_dword_candidates: Vec::new(), + policy_chunk_len: 0x1a, + profile_chunk_len: 0x20, + policy_leading_f32_0: 552.0, + policy_leading_f32_1: 0.0, + policy_leading_f32_2: 276.0, + policy_reserved_dwords: vec![0, 0, 0], + policy_reserved_dword_candidates: Vec::new(), + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_collection: Some(SmpSaveRegionProfileCollectionProbe { + direct_collection_flag: 1, + entry_stride: 0x22, + live_id_bound: 26, + live_record_count: 24, + entry_start_relative_offset: 0x50, + trailing_padding_len: 0, + entries: vec![SmpSaveRegionProfileEntryProbe { + entry_index: 0, + row_relative_offset: 0x50, + name: "Farm Corn".to_string(), + trailing_weight_f32: 0.2, + }], + }), + }, + ], + evidence: vec![], + }); + report.save_placed_structure_collection_header_probe = + Some(SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-placed-structure-tagged-header-counts".to_string(), + semantic_family: "scenario-save-placed-structure-header-counts".to_string(), + metadata_tag_offset: 0x4000, + records_tag_offset: 0x4100, + close_tag_offset: 0x4200, + direct_collection_flag: 0, + direct_collection_flag_hex: "0x00000000".to_string(), + direct_record_stride: 0x06, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 0x7ee, + live_id_bound_hex: "0x000007ee".to_string(), + live_record_count: 0x7ea, + live_record_count_hex: "0x000007ea".to_string(), + header_words: vec![0, 6, 0x0a, 0x14, 0x7ee, 0x7ea], + header_hex_words: vec![ + "0x00000000".to_string(), + "0x00000006".to_string(), + "0x0000000a".to_string(), + "0x00000014".to_string(), + "0x000007ee".to_string(), + "0x000007ea".to_string(), + ], + evidence: vec![], + }); + + let slice = load_save_slice_from_report(&report).expect("save slice"); + + let company_roster = slice.company_roster.expect("selection-only company roster"); + assert_eq!(company_roster.observed_entry_count, 1); + assert_eq!(company_roster.selected_company_id, Some(1)); + assert!(company_roster.entries.is_empty()); + + let chairman_table = slice + .chairman_profile_table + .expect("selection-only chairman table"); + assert_eq!(chairman_table.observed_entry_count, 2); + assert_eq!(chairman_table.selected_chairman_profile_id, Some(9)); + assert!(chairman_table.entries.is_empty()); + let issue_37_state = slice + .world_issue_37_state + .expect("world issue-0x37 state should load"); + assert_eq!(issue_37_state.issue_value, 3); + assert_eq!(issue_37_state.issue_38_value, 1); + assert_eq!(issue_37_state.issue_39_value, 2); + assert_eq!(issue_37_state.issue_3a_value, 4); + assert_eq!(issue_37_state.multiplier_raw_hex, "0x3d75c28f"); + assert_eq!(issue_37_state.multiplier_value_f32_text, "0.060000"); + let tuning_state = slice + .world_economic_tuning_state + .expect("world economic tuning state should load"); + assert_eq!(tuning_state.mirror_raw_hex, "0x3f46d093"); + assert_eq!(tuning_state.mirror_value_f32_text, "0.776620"); + assert_eq!( + tuning_state.lane_value_f32_text, + vec!["0.750000", "0.007000"] + ); + + assert!( + slice + .notes + .iter() + .any(|note| note.contains("selected_company_id=1")) + ); + assert!( + slice + .notes + .iter() + .any(|note| note.contains("selected_chairman_profile_id=9")) + ); + assert!( + slice + .notes + .iter() + .any(|note| note.contains("grounded issue-0x37 pair: value=3")) + ); + assert!( + slice + .notes + .iter() + .any(|note| note.contains("campaign_override_flag=1")) + ); + assert!( + slice + .notes + .iter() + .any(|note| note.contains("tagged company header reports live_record_count=1")) + ); + assert!(slice.notes.iter().any(|note| { + note.contains("tagged chairman/profile header reports live_record_count=2") + })); + assert!( + slice + .notes + .iter() + .any(|note| note.contains("tagged train header reports live_record_count=20")) + ); + assert!(slice.notes.iter().any(|note| { + note.contains("tagged train metadata also exposes a live-entry directory") + })); + assert!( + slice + .notes + .iter() + .any(|note| { note.contains("tagged region header reports live_record_count=145") }) + ); + assert!(slice.notes.iter().any(|note| { + note.contains("tagged region records also expose 2 repeated 0x55f1/0x55f2/0x55f3 triplets") + })); + assert!(slice.notes.iter().any(|note| { + note.contains("tagged placed-structure header reports live_record_count=2026") + })); +} + +#[test] +fn parses_save_company_roster_probe_from_direct_records() { + let metadata_tag_offset = 0x40usize; + let stride = 0x7684usize; + let count = 2usize; + let start_offset = 0xc6usize; + let total_len = metadata_tag_offset + 4 + start_offset + stride * count + 0x400; + let mut bytes = vec![0u8; total_len]; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x000061a9u32.to_le_bytes()); + let header_words = [ + 1u32, + 0x7684, + 5, + 5, + 5, + count as u32, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + let records_tag_offset = metadata_tag_offset + 4 + 0x200; + let close_tag_offset = records_tag_offset + 4; + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x000061aau32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000061abu32.to_le_bytes()); + + for ( + index, + ( + name, + linked, + merger, + takeover, + bond_count, + _debt, + track_capacity, + mutable_support_scalar_raw_u32, + young_company_support_scalar_raw_u32, + support_progress_word, + recent_per_share_subscore_raw_u32, + cached_share_price_raw_u32, + chairman_salary_baseline, + chairman_salary_current, + chairman_bonus_year, + chairman_bonus_amount, + founding_year, + last_bankruptcy_year, + last_dividend_year, + current_issue_calendar_word, + current_issue_calendar_word_2, + prior_issue_calendar_word, + prior_issue_calendar_word_2, + preferred_locomotive_engine_type_raw_u8, + city_connection_latch, + linked_transit_latch, + linked_transit_route_anchor_entry_id, + linked_transit_route_anchor_fallback_counts, + ), + ) in [ + ( + "Company One", + 1u32, + 1862u32, + 1865u32, + 2u8, + 1_000_000u32, + Some(603i32), + 0x3f800000u32, + 0x42340000u32, + 17u32, + 0x41f00000u32, + 0x426c0000u32, + 24u32, + 31u32, + 1849u32, + 1250i32, + 1842u32, + 1851u32, + 1848u32, + 7u32, + 8u32, + 6u32, + 7u32, + 2u8, + true, + false, + Some(77u32), + [3u32, 5u32, 8u32], + ), + ( + "Company Two", + 2u32, + 0u32, + 1871u32, + 1u8, + 500_000u32, + None, + 0x40000000u32, + 0x42700000u32, + 33u32, + 0x42000000u32, + 0x42780000u32, + 28u32, + 36u32, + 0u32, + 0i32, + 1845u32, + 0u32, + 1850u32, + 3u32, + 4u32, + 2u32, + 3u32, + 1u8, + false, + true, + Some(41u32), + [13u32, 21u32, 34u32], + ), + ] + .into_iter() + .enumerate() + { + let record_offset = metadata_tag_offset + 4 + start_offset + index * stride; + bytes[record_offset..record_offset + 4] + .copy_from_slice(&((index + 1) as u32).to_le_bytes()); + bytes[record_offset + 4..record_offset + 4 + name.len()].copy_from_slice(name.as_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET + 4] + .copy_from_slice(&linked.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET] = 1; + bytes[record_offset + 0x47..record_offset + 0x4b].copy_from_slice(&20000u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET + 4] + .copy_from_slice(&mutable_support_scalar_raw_u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET + 4] + .copy_from_slice(&young_company_support_scalar_raw_u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET] = bond_count; + for slot_index in 0..bond_count as usize { + let slot_offset = record_offset + + SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET + + slot_index * SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE; + let (principal, coupon_rate) = if index == 0 && slot_index == 0 { + (900_000i32, 0.08f32) + } else if index == 0 && slot_index == 1 { + (650_000i32, 0.12f32) + } else { + (500_000i32, 0.10f32) + }; + bytes[slot_offset..slot_offset + 4].copy_from_slice(&principal.to_le_bytes()); + bytes[slot_offset + 4..slot_offset + 8] + .copy_from_slice(&(1894u32 + slot_index as u32).to_le_bytes()); + bytes[slot_offset + 8..slot_offset + 12].copy_from_slice(&coupon_rate.to_le_bytes()); + } + bytes[record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET + 4] + .copy_from_slice(&merger.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET + 4] + .copy_from_slice(&takeover.to_le_bytes()); + let raw_capacity = track_capacity.unwrap_or(-1); + bytes[record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET + 4] + .copy_from_slice(&raw_capacity.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET + 4] + .copy_from_slice(&support_progress_word.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET + 4] + .copy_from_slice(&recent_per_share_subscore_raw_u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET + 4] + .copy_from_slice(&cached_share_price_raw_u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET + 4] + .copy_from_slice(&chairman_salary_baseline.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET + 4] + .copy_from_slice(&chairman_salary_current.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET + 4] + .copy_from_slice(&chairman_bonus_year.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET + 4] + .copy_from_slice(&chairman_bonus_amount.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET + 4] + .copy_from_slice(&founding_year.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET + 4] + .copy_from_slice(&last_bankruptcy_year.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET + 4] + .copy_from_slice(&last_dividend_year.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET + 4] + .copy_from_slice(¤t_issue_calendar_word.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2 + ..record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2 + 4] + .copy_from_slice(¤t_issue_calendar_word_2.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET + 4] + .copy_from_slice(&prior_issue_calendar_word.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2 + ..record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2 + 4] + .copy_from_slice(&prior_issue_calendar_word_2.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET] = + preferred_locomotive_engine_type_raw_u8; + bytes[record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET] = + u8::from(city_connection_latch); + bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET] = + u8::from(linked_transit_latch); + if let Some(anchor_entry_id) = linked_transit_route_anchor_entry_id { + bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET + ..record_offset + + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET + + 4] + .copy_from_slice(&anchor_entry_id.to_le_bytes()); + } + for (fallback_index, relative_offset) in + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_FALLBACK_COUNT_OFFSETS + .into_iter() + .enumerate() + { + bytes[record_offset + relative_offset..record_offset + relative_offset + 4] + .copy_from_slice( + &linked_transit_route_anchor_fallback_counts[fallback_index].to_le_bytes(), + ); + } + let current_cash: f64 = if index == 0 { 125_000.0 } else { -25_000.0 }; + let current_cash_slot_offset = record_offset + + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET + + (RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN as usize + * 8); + bytes[current_cash_slot_offset..current_cash_slot_offset + 8] + .copy_from_slice(¤t_cash.to_bits().to_le_bytes()); + } + + let header_probe = parse_save_company_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("company header probe should parse"); + let roster = parse_save_company_roster_probe( + &bytes, + Some(&header_probe), + Some(&SmpSaveWorldSelectionContextProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-selected-company-and-chairman-context".to_string(), + chunk_tag_offset: 0, + payload_offset: 0, + payload_len: 0, + payload_len_hex: "0x0".to_string(), + selected_company_id_offset: 0, + selected_company_id: 2, + selected_company_id_hex: "0x00000002".to_string(), + selected_chairman_profile_id_offset: 0, + selected_chairman_profile_id: 1, + selected_chairman_profile_id_hex: "0x00000001".to_string(), + chairman_slot_selector_offset: 0, + chairman_slot_selectors: vec![], + campaign_override_flag_offset: 0, + campaign_override_flag: 0, + campaign_override_flag_hex: "0x00".to_string(), + chairman_role_gate_offset: 0, + chairman_role_gate_bytes: vec![], + evidence: vec![], + }), + ) + .expect("company roster should parse"); + + assert_eq!(roster.observed_entry_count, 2); + assert_eq!(roster.selected_company_id, Some(2)); + assert_eq!(roster.entries.len(), 2); + assert_eq!(roster.entries[0].company_id, 1); + assert_eq!(roster.entries[0].current_cash, 125_000); + assert_eq!(roster.entries[0].linked_chairman_profile_id, Some(1)); + assert_eq!(roster.entries[0].debt, 1_550_000); + assert_eq!(roster.entries[0].available_track_laying_capacity, Some(603)); + assert_eq!(roster.entries[0].merger_cooldown_year, Some(1862)); + let market_state = roster.entries[0] + .market_state + .as_ref() + .expect("company market state should load"); + assert_eq!(market_state.outstanding_shares, 20_000); + assert_eq!(market_state.live_bond_slots.len(), 2); + assert_eq!(market_state.live_bond_slots[0].principal, 900_000); + assert_eq!(market_state.live_bond_slots[0].maturity_year, 1894); + assert_eq!( + market_state.live_bond_slots[1].coupon_rate_raw_u32, + 0.12f32.to_bits() + ); + assert_eq!(market_state.largest_live_bond_principal, Some(900_000)); + assert_eq!( + market_state.highest_coupon_live_bond_principal, + Some(650_000) + ); + assert_eq!(market_state.mutable_support_scalar_raw_u32, 0x3f800000); + assert_eq!( + market_state.young_company_support_scalar_raw_u32, + 0x42340000 + ); + assert_eq!(market_state.support_progress_word, 17); + assert_eq!(market_state.recent_per_share_subscore_raw_u32, 0x41f00000); + assert_eq!(market_state.cached_share_price_raw_u32, 0x426c0000); + assert_eq!(market_state.chairman_salary_baseline, 24); + assert_eq!(market_state.chairman_salary_current, 31); + assert_eq!(market_state.chairman_bonus_year, 1849); + assert_eq!(market_state.chairman_bonus_amount, 1250); + assert_eq!(market_state.founding_year, 1842); + assert_eq!(market_state.last_bankruptcy_year, 1851); + assert_eq!(market_state.last_dividend_year, 1848); + assert_eq!(market_state.current_issue_calendar_word, 7); + assert_eq!(market_state.current_issue_calendar_word_2, 8); + assert_eq!(market_state.prior_issue_calendar_word, 6); + assert_eq!(market_state.prior_issue_calendar_word_2, 7); + assert_eq!(market_state.linked_transit_route_anchor_entry_id, Some(77)); + assert_eq!( + market_state.linked_transit_route_anchor_fallback_counts, + vec![3, 5, 8] + ); + assert_eq!( + roster.entries[0].preferred_locomotive_engine_type_raw_u8, + Some(2) + ); + assert!(market_state.city_connection_latch); + assert!(!market_state.linked_transit_latch); + assert_eq!( + market_state.stat_band_root_0cfb_candidates.len(), + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS + ); + assert_eq!( + market_state.stat_band_root_0d7f_candidates.len(), + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS + ); + assert_eq!( + market_state.stat_band_root_1c47_candidates.len(), + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS + ); + assert_eq!( + market_state.stat_band_root_0cfb_candidates[31].label, + "stat_band_0cfb_word_32" + ); + assert_eq!( + market_state.stat_band_root_0cfb_candidates[31].relative_offset_hex, + "0xd77" + ); + assert_eq!(roster.entries[1].company_id, 2); + assert_eq!(roster.entries[1].current_cash, -25_000); + assert_eq!(roster.entries[1].linked_chairman_profile_id, Some(2)); + assert_eq!(roster.entries[1].debt, 500_000); + assert_eq!(roster.entries[1].available_track_laying_capacity, None); + assert_eq!(roster.entries[1].takeover_cooldown_year, Some(1871)); + let second_market_state = roster.entries[1] + .market_state + .as_ref() + .expect("second company market state should load"); + assert_eq!( + second_market_state.largest_live_bond_principal, + Some(500_000) + ); + assert_eq!( + second_market_state.highest_coupon_live_bond_principal, + Some(500_000) + ); + assert_eq!(second_market_state.chairman_bonus_year, 0); + assert_eq!(second_market_state.chairman_bonus_amount, 0); + assert_eq!(second_market_state.last_dividend_year, 1850); + assert_eq!(second_market_state.current_issue_calendar_word, 3); + assert_eq!(second_market_state.current_issue_calendar_word_2, 4); + assert_eq!(second_market_state.prior_issue_calendar_word, 2); + assert_eq!(second_market_state.prior_issue_calendar_word_2, 3); + assert_eq!( + second_market_state.linked_transit_route_anchor_entry_id, + Some(41) + ); + assert_eq!( + second_market_state.linked_transit_route_anchor_fallback_counts, + vec![13, 21, 34] + ); + assert_eq!( + roster.entries[1].preferred_locomotive_engine_type_raw_u8, + Some(1) + ); + assert!(!second_market_state.city_connection_latch); + assert!(second_market_state.linked_transit_latch); +} + +#[test] +fn parses_save_chairman_profile_table_probe_from_direct_records() { + let metadata_tag_offset = 0x40usize; + let stride = 0xcabusize; + let count = 2usize; + let start_offset = 0x4eusize; + let total_len = metadata_tag_offset + 4 + start_offset + stride * count + 0x200; + let mut bytes = vec![0u8; total_len]; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00005209u32.to_le_bytes()); + let header_words = [ + 1u32, + 0xcab, + 8, + 6, + 8, + count as u32, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + let records_tag_offset = metadata_tag_offset + 4 + 0x100; + let close_tag_offset = records_tag_offset + 4; + bytes[records_tag_offset..records_tag_offset + 4].copy_from_slice(&0x0000520au32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); + + for (index, (name, linked, cash, cache0, cache1, cache4, holdings)) in [ + ( + "Collis Huntington", + 1u32, + -107644.0f64, + 252508.0f64, + 0.0f64, + 0.0f64, + vec![(1u32, 6000u32)], + ), + ( + "Thomas Durant", + 2u32, + -382718.0f64, + -283562.0f64, + 822000.0f64, + 1_392_000.0f64, + vec![(2u32, 9000u32)], + ), + ] + .into_iter() + .enumerate() + { + let record_offset = metadata_tag_offset + 4 + start_offset + index * stride; + bytes[record_offset..record_offset + 4] + .copy_from_slice(&((index + 1) as u32).to_le_bytes()); + bytes[record_offset + 4..record_offset + 8].copy_from_slice(&1u32.to_le_bytes()); + bytes[record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET + ..record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET + name.len()] + .copy_from_slice(name.as_bytes()); + bytes[record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET + ..record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET + 8] + .copy_from_slice(&cash.to_le_bytes()); + bytes[record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET + ..record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET + 4] + .copy_from_slice(&linked.to_le_bytes()); + bytes[record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET] = + (index as u8) + 10; + bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET + ..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET + 8] + .copy_from_slice(&cache0.to_le_bytes()); + bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET + ..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET + 8] + .copy_from_slice(&cache1.to_le_bytes()); + bytes[record_offset + 0x211..record_offset + 0x211 + 8] + .copy_from_slice(&cache4.to_le_bytes()); + for (company_id, units) in holdings { + let slot_offset = + record_offset + SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET + company_id as usize * 4; + bytes[slot_offset..slot_offset + 4].copy_from_slice(&units.to_le_bytes()); + } + } + + let header_probe = parse_save_chairman_profile_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("chairman header probe should parse"); + let table = parse_save_chairman_profile_table_probe( + &bytes, + Some(&header_probe), + Some(&SmpSaveWorldSelectionContextProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-selected-company-and-chairman-context".to_string(), + chunk_tag_offset: 0, + payload_offset: 0, + payload_len: 0, + payload_len_hex: "0x0".to_string(), + selected_company_id_offset: 0, + selected_company_id: 2, + selected_company_id_hex: "0x00000002".to_string(), + selected_chairman_profile_id_offset: 0, + selected_chairman_profile_id: 2, + selected_chairman_profile_id_hex: "0x00000002".to_string(), + chairman_slot_selector_offset: 0, + chairman_slot_selectors: vec![], + campaign_override_flag_offset: 0, + campaign_override_flag: 0, + campaign_override_flag_hex: "0x00".to_string(), + chairman_role_gate_offset: 0, + chairman_role_gate_bytes: vec![], + evidence: vec![], + }), + Some(&SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-company-tagged-header-counts".to_string(), + semantic_family: "scenario-save-company-header-counts".to_string(), + metadata_tag_offset: 0, + records_tag_offset: 0, + close_tag_offset: 0, + direct_collection_flag: 1, + direct_collection_flag_hex: "0x00000001".to_string(), + direct_record_stride: 0x7684, + direct_record_stride_hex: "0x00007684".to_string(), + live_id_bound: 5, + live_id_bound_hex: "0x00000005".to_string(), + live_record_count: 2, + live_record_count_hex: "0x00000002".to_string(), + header_words: vec![], + header_hex_words: vec![], + evidence: vec![], + }), + ) + .expect("chairman profile table should parse"); + + assert_eq!(table.observed_entry_count, 2); + assert_eq!(table.selected_chairman_profile_id, Some(2)); + assert_eq!(table.entries.len(), 2); + assert_eq!(table.entries[0].profile_id, 1); + assert_eq!(table.entries[0].name, "Collis Huntington"); + assert_eq!(table.entries[0].linked_company_id, Some(1)); + assert_eq!(table.entries[0].company_holdings.get(&1), Some(&6000)); + assert_eq!(table.entries[0].current_cash, -107644); + assert_eq!(table.entries[0].holdings_value_total, 252508); + assert_eq!(table.entries[0].purchasing_power_total, 144864); + assert_eq!(table.entries[0].personality_byte_0x291, Some(10)); + assert_eq!(table.entries[1].profile_id, 2); + assert_eq!(table.entries[1].company_holdings.get(&2), Some(&9000)); + assert_eq!(table.entries[1].holdings_value_total, 822000); + assert_eq!(table.entries[1].purchasing_power_total, 1_009_282); + assert_eq!(table.entries[1].personality_byte_0x291, Some(11)); +} + +#[test] +fn builds_save_world_selection_role_analysis_from_probe() { + let probe = SmpSaveWorldSelectionContextProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-selected-company-and-chairman-context".to_string(), + chunk_tag_offset: 0, + payload_offset: 0, + payload_len: 0x4f2c, + payload_len_hex: "0x4f2c".to_string(), + selected_company_id_offset: 0x21, + selected_company_id: 3, + selected_company_id_hex: "0x00000003".to_string(), + selected_chairman_profile_id_offset: 0x25, + selected_chairman_profile_id: 7, + selected_chairman_profile_id_hex: "0x00000007".to_string(), + chairman_slot_selector_offset: 0x87, + chairman_slot_selectors: vec![1, 0, 2, 0], + campaign_override_flag_offset: 0xc5, + campaign_override_flag: 1, + campaign_override_flag_hex: "0x01".to_string(), + chairman_role_gate_offset: 0x0bc3, + chairman_role_gate_bytes: vec![2, 0, 1, 0], + evidence: vec![], + }; + + let analysis = build_save_world_selection_role_analysis(&probe); + + assert_eq!(analysis.selected_company_id, 3); + assert_eq!(analysis.selected_chairman_profile_id, 7); + assert_eq!(analysis.campaign_override_flag_hex, "0x01"); + assert_eq!(analysis.chairman_slots.len(), 4); + assert_eq!(analysis.chairman_slots[0].selector_byte_hex, "0x01"); + assert_eq!(analysis.chairman_slots[2].role_gate_byte_hex, "0x01"); +} + +#[test] +fn builds_save_candidate_views_with_raw_bits() { + let mut bytes = vec![0u8; 0x40]; + bytes[0x08..0x0c].copy_from_slice(&0x3f800000u32.to_le_bytes()); + bytes[0x10..0x18].copy_from_slice(&(-2458.0f64).to_le_bytes()); + + let dword = build_save_dword_candidate(&bytes, 0, "unit_float", 0x08) + .expect("dword candidate should build"); + let qword = build_save_qword_candidate(&bytes, 0, 0x10).expect("qword candidate should build"); + + assert_eq!(dword.raw_u32_hex, "0x3f800000"); + assert_eq!(dword.value_i32, 1_065_353_216); + assert_eq!(dword.value_f32, 1.0); + assert_eq!(qword.raw_u64, (-2458.0f64).to_bits()); + assert_eq!(qword.value_i64, (-2458.0f64).to_bits() as i64); + assert_eq!(qword.value_f64, -2458.0); +} + +#[test] +fn derives_chairman_holdings_share_price_total_from_grounded_company_prices() { + let holdings_by_company = + BTreeMap::from([(2u32, 19_000u32), (4u32, 1_000u32), (6u32, 2_000u32)]); + let company_share_prices = BTreeMap::from([(2u32, 66i64), (4u32, 69i64), (6u32, 27i64)]); + + let total = + derive_chairman_holdings_share_price_total(&holdings_by_company, &company_share_prices) + .expect("derived holdings total should compute"); + + assert_eq!(total, 1_377_000); +} + +#[test] +fn derives_chairman_cached_purchasing_power_from_strongest_nonnegative_cache() { + let cached_scalar_candidates = vec![ + SmpSaveScalarCandidate { + relative_offset: 0x1e9, + relative_offset_hex: "0x1e9".to_string(), + raw_u64: (-343_508.0f64).to_bits(), + raw_u64_hex: format!("0x{:016x}", (-343_508.0f64).to_bits()), + value_i64: round_f64_to_i64(-343_508.0).expect("i64"), + value_f64: -343_508.0, + }, + SmpSaveScalarCandidate { + relative_offset: 0x201, + relative_offset_hex: "0x201".to_string(), + raw_u64: 1_386_000.0f64.to_bits(), + raw_u64_hex: format!("0x{:016x}", 1_386_000.0f64.to_bits()), + value_i64: round_f64_to_i64(1_386_000.0).expect("i64"), + value_f64: 1_386_000.0, + }, + SmpSaveScalarCandidate { + relative_offset: 0x211, + relative_offset_hex: "0x211".to_string(), + raw_u64: 1_392_000.0f64.to_bits(), + raw_u64_hex: format!("0x{:016x}", 1_392_000.0f64.to_bits()), + value_i64: round_f64_to_i64(1_392_000.0).expect("i64"), + value_f64: 1_392_000.0, + }, + ]; + + let total = derive_chairman_cached_purchasing_power_total(-463_436, &cached_scalar_candidates) + .expect("derived purchasing power should compute"); + + assert_eq!(total, 928_564); +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/analysis.rs b/crates/rrt-runtime/src/inspect/smp/world/analysis.rs new file mode 100644 index 0000000..a734b14 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/analysis.rs @@ -0,0 +1,137 @@ +use std::fs; +use std::path::Path; + +use crate::inspect::smp::bundle::{SmpInspectionReport, inspect_bundle_bytes}; +use crate::inspect::smp::regions::*; +use crate::inspect::smp::structures::*; +use crate::inspect::smp::world::*; + +pub fn inspect_save_company_and_chairman_analysis_file( + path: &Path, +) -> Result> { + let bytes = fs::read(path)?; + let report = inspect_bundle_bytes( + &bytes, + path.extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.to_ascii_lowercase()), + ); + inspect_save_company_and_chairman_analysis_bytes(&bytes, &report) + .ok_or_else(|| "save inspection did not expose grounded company/chairman analysis".into()) +} + +pub fn inspect_save_company_and_chairman_analysis_bytes( + bytes: &[u8], + report: &SmpInspectionReport, +) -> Option { + let selection_probe = report.save_world_selection_context_probe.as_ref(); + let world_selection_context = selection_probe.map(build_save_world_selection_role_analysis); + let world_issue_37 = report.save_world_issue_37_probe.clone(); + let world_economic_tuning = report.save_world_economic_tuning_probe.clone(); + let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone(); + let train_collection_directory = report.save_train_collection_directory_probe.clone(); + let region_record_triplets = report.save_region_record_triplet_probe.clone(); + let region_queued_notice_records = report + .save_region_queued_notice_record_probe + .clone() + .or_else(|| { + parse_save_region_queued_notice_record_probe( + bytes, + report.file_extension_hint.as_deref(), + report.container_profile.as_ref(), + report.save_region_collection_header_probe.as_ref(), + ) + }); + let region_fixed_row_run_candidates = report + .save_region_fixed_row_run_candidate_probe + .clone() + .or_else(|| { + parse_save_region_fixed_row_run_candidate_probe( + bytes, + report.file_extension_hint.as_deref(), + report.container_profile.as_ref(), + report.save_region_collection_header_probe.as_ref(), + ) + }); + let placed_structure_record_triplets = + report.save_placed_structure_record_triplet_probe.clone(); + let placed_structure_dynamic_side_buffer = report + .save_placed_structure_dynamic_side_buffer_probe + .clone() + .or_else(|| { + parse_save_placed_structure_dynamic_side_buffer_probe( + bytes, + report.file_extension_hint.as_deref(), + report.container_profile.as_ref(), + ) + }); + let placed_structure_dynamic_side_buffer_alignment = placed_structure_dynamic_side_buffer + .as_ref() + .zip(placed_structure_record_triplets.as_ref()) + .map(|(side_buffer, triplets)| { + summarize_placed_structure_dynamic_side_buffer_alignment(side_buffer, triplets) + }); + let unclassified_tagged_collection_headers = report + .save_unclassified_tagged_collection_header_probes + .clone(); + let company_header_probe = report.save_company_collection_header_probe.as_ref(); + let chairman_header_probe = report + .save_chairman_profile_collection_header_probe + .as_ref(); + + let company_entries = build_save_company_record_analysis_entries(&bytes, company_header_probe)?; + let company_share_prices = build_company_share_prices(&company_entries); + let chairman_entries = build_save_chairman_record_analysis_entries( + &bytes, + chairman_header_probe, + company_header_probe, + &company_share_prices, + )?; + let notes = + build_save_company_chairman_analysis_notes(SaveCompanyChairmanAnalysisNotesInputs { + report, + world_selection_context: world_selection_context.as_ref(), + train_collection_directory: train_collection_directory.as_ref(), + region_record_triplets: region_record_triplets.as_ref(), + region_queued_notice_records: region_queued_notice_records.as_ref(), + region_fixed_row_run_candidates: region_fixed_row_run_candidates.as_ref(), + placed_structure_record_triplets: placed_structure_record_triplets.as_ref(), + placed_structure_dynamic_side_buffer: placed_structure_dynamic_side_buffer.as_ref(), + placed_structure_dynamic_side_buffer_alignment: + placed_structure_dynamic_side_buffer_alignment.as_ref(), + unclassified_tagged_collection_headers: &unclassified_tagged_collection_headers, + company_entries: &company_entries, + chairman_entries: &chairman_entries, + }); + + Some(SmpSaveCompanyChairmanAnalysisReport { + profile_family: report + .container_profile + .as_ref() + .map(|profile| profile.profile_family.clone()) + .unwrap_or_else(|| "unknown".to_string()), + selected_company_id: selection_probe.map(|probe| probe.selected_company_id), + selected_chairman_profile_id: selection_probe + .map(|probe| probe.selected_chairman_profile_id), + world_selection_context, + world_issue_37, + world_economic_tuning, + world_finance_neighborhood, + train_collection_header: report.save_train_collection_header_probe.clone(), + train_collection_directory, + region_collection_header: report.save_region_collection_header_probe.clone(), + region_record_triplets, + region_queued_notice_records, + region_fixed_row_run_candidates, + placed_structure_collection_header: report + .save_placed_structure_collection_header_probe + .clone(), + placed_structure_record_triplets, + placed_structure_dynamic_side_buffer, + placed_structure_dynamic_side_buffer_alignment, + unclassified_tagged_collection_headers, + company_entries, + chairman_entries, + notes, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/catalogs.rs b/crates/rrt-runtime/src/inspect/smp/world/catalogs.rs new file mode 100644 index 0000000..1ea19f3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/catalogs.rs @@ -0,0 +1,113 @@ +use crate::inspect::smp::save_load::{ + SmpLoadedCargoCatalog, SmpLoadedCargoCatalogEntry, SmpLoadedLocomotiveCatalog, + SmpLoadedLocomotiveCatalogEntry, SmpLoadedNamedLocomotiveAvailabilityTable, +}; +use crate::inspect::smp::special_conditions::SmpRecipeBookSummaryProbe; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn derive_locomotive_catalog_from_named_availability_table( + table: &SmpLoadedNamedLocomotiveAvailabilityTable, +) -> Option { + if table.entries.is_empty() { + return None; + } + + let entries = table + .entries + .iter() + .enumerate() + .map(|(index, entry)| SmpLoadedLocomotiveCatalogEntry { + locomotive_id: (index + 1) as u32, + name: entry.text.clone(), + }) + .collect::>(); + + Some(SmpLoadedLocomotiveCatalog { + source_kind: format!("{}-ordinal-catalog", table.source_kind), + semantic_family: "scenario-save-derived-locomotive-catalog".to_string(), + entries_offset: table.entries_offset, + observed_entry_count: entries.len(), + entries, + }) +} + +pub(in crate::inspect::smp) fn derive_cargo_catalog_from_recipe_book_probe( + probe: &SmpRecipeBookSummaryProbe, +) -> Option { + if probe.books.is_empty() { + return None; + } + + let entries = probe + .books + .iter() + .filter(|book| book.book_index < 11) + .filter_map(|book| { + let line = book + .lines + .iter() + .find(|line| line.imports_to_runtime_descriptor) + .or_else(|| book.lines.first())?; + let slot_id = (book.book_index + 1) as u32; + let definition = known_cargo_slot_definition(slot_id)?; + Some(SmpLoadedCargoCatalogEntry { + slot_id, + label: definition.label.to_string(), + cargo_class: definition.cargo_class, + book_index: book.book_index, + max_annual_production_word: book.max_annual_production_word, + mode_word: line.mode_word, + runtime_import_branch_kind: line.runtime_import_branch_kind.clone(), + annual_amount_word: line.annual_amount_word, + supplied_cargo_token_word: line.supplied_cargo_token_word, + supplied_cargo_token_probable_high16_ascii_stem: line + .supplied_cargo_token_probable_high16_ascii_stem + .clone(), + demanded_cargo_token_word: line.demanded_cargo_token_word, + demanded_cargo_token_probable_high16_ascii_stem: line + .demanded_cargo_token_probable_high16_ascii_stem + .clone(), + }) + }) + .collect::>(); + + if entries.is_empty() { + return None; + } + + Some(SmpLoadedCargoCatalog { + source_kind: format!("{}-slot-catalog", probe.source_kind), + semantic_family: "scenario-save-derived-cargo-catalog".to_string(), + root_offset: Some(probe.root_offset), + observed_entry_count: entries.len(), + entries, + }) +} + +pub(in crate::inspect::smp) fn known_cargo_slot_definition( + slot_id: u32, +) -> Option { + KNOWN_CARGO_SLOT_DEFINITIONS + .iter() + .copied() + .find(|definition| definition.slot_id == slot_id) +} + +pub(in crate::inspect::smp) fn known_cargo_slot_definition_for_descriptor_id( + descriptor_id: u32, +) -> Option { + KNOWN_CARGO_SLOT_DEFINITIONS + .iter() + .copied() + .find(|definition| definition.descriptor_id == descriptor_id) +} + +pub(in crate::inspect::smp) fn runtime_cargo_class_name( + cargo_class: RuntimeCargoClass, +) -> &'static str { + match cargo_class { + RuntimeCargoClass::Factory => "factory", + RuntimeCargoClass::FarmMine => "farm_mine", + RuntimeCargoClass::Other => "other", + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/chairman_records.rs b/crates/rrt-runtime/src/inspect/smp/world/chairman_records.rs new file mode 100644 index 0000000..acf8926 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/chairman_records.rs @@ -0,0 +1,353 @@ +use std::collections::BTreeMap; + +use crate::inspect::smp::catalog::offsets::world::RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT; +use crate::inspect::smp::common::{ + SmpSaveTaggedCollectionHeaderProbe, parse_nonzero_u32, read_ascii_c_string_at, read_f64_at, + read_u8_at, read_u32_at, read_u64_at, round_f64_to_i64, +}; +use crate::inspect::smp::world::*; + +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN: usize = 0x1f; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_CASH_OFFSET: usize = 0x154; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET: usize = 0x15d; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET: usize = 0x1dd; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET: usize = 0x1e9; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET: usize = 0x1f1; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET: usize = 0x291; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x35b; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT: usize = + RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS: [usize; 7] = + [0x1e9, 0x1f1, 0x1f9, 0x201, 0x209, 0x211, 0x219]; +pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_START_SCAN_LIMIT: usize = 0x80; + +pub(in crate::inspect::smp) fn build_save_qword_candidate( + bytes: &[u8], + record_offset: usize, + relative_offset: usize, +) -> Option { + let raw_u64 = read_u64_at(bytes, record_offset + relative_offset)?; + Some(SmpSaveScalarCandidate { + relative_offset, + relative_offset_hex: format!("0x{relative_offset:x}"), + raw_u64, + raw_u64_hex: format!("0x{raw_u64:016x}"), + value_i64: raw_u64 as i64, + value_f64: f64::from_bits(raw_u64), + }) +} + +pub(in crate::inspect::smp) fn derive_chairman_holdings_share_price_total( + holdings_by_company: &BTreeMap, + company_share_prices: &BTreeMap, +) -> Option { + let mut total = 0i64; + for (company_id, units) in holdings_by_company { + let share_price = *company_share_prices.get(company_id)?; + total = total.checked_add((*units as i64).checked_mul(share_price)?)?; + } + Some(total) +} + +pub(in crate::inspect::smp) fn derive_chairman_cached_purchasing_power_total( + current_cash: i64, + cached_scalar_candidates: &[SmpSaveScalarCandidate], +) -> Option { + let strongest_cached_total = cached_scalar_candidates + .iter() + .filter_map(|candidate| round_f64_to_i64(candidate.value_f64)) + .filter(|value| *value >= 0) + .max()?; + current_cash.checked_add(strongest_cached_total) +} + +pub(in crate::inspect::smp) fn parse_save_chairman_profile_table_probe( + bytes: &[u8], + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, + selection_probe: Option<&SmpSaveWorldSelectionContextProbe>, + company_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + let header_probe = header_probe?; + let observed_entry_count = header_probe.live_record_count as usize; + if observed_entry_count == 0 { + return Some(SmpLoadedChairmanProfileTable { + source_kind: "save-chairman-profile-direct-records".to_string(), + semantic_family: "scenario-save-chairman-profile-direct-records".to_string(), + observed_entry_count, + selected_chairman_profile_id: selection_probe + .map(|probe| probe.selected_chairman_profile_id), + entries: Vec::new(), + }); + } + + let record_start_offset = + detect_save_chairman_profile_record_start_offset(bytes, header_probe)?; + let record_stride = header_probe.direct_record_stride as usize; + let base_offset = header_probe + .metadata_tag_offset + .checked_add(4)? + .checked_add(record_start_offset)?; + let company_id_bound = company_header_probe + .map(|probe| probe.live_id_bound) + .unwrap_or(0); + + let mut entries = Vec::with_capacity(observed_entry_count); + for index in 0usize..observed_entry_count { + let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?; + let profile_id = read_u32_at(bytes, record_offset)?; + let active = read_u32_at(bytes, record_offset + 4)? != 0; + let name = read_ascii_c_string_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET, + SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN, + )?; + let current_cash = round_f64_to_i64(read_f64_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET, + )?)?; + let linked_company_id = parse_nonzero_u32( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, + )?; + let personality_byte_0x291 = read_u8_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET, + )?; + let cache_0 = round_f64_to_i64(read_f64_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET, + )?)?; + let cache_1 = round_f64_to_i64(read_f64_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET, + )?)?; + let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS + .iter() + .map(|relative_offset| { + build_save_qword_candidate(bytes, record_offset, *relative_offset) + }) + .collect::>>()?; + let issue_opinion_terms_raw_i32 = build_save_i32_term_strip( + bytes, + record_offset, + SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET, + SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT, + )?; + let holdings_value_total = cache_0.max(cache_1).max(0); + let net_worth_total = current_cash.saturating_add(holdings_value_total); + let purchasing_power_total = + derive_chairman_cached_purchasing_power_total(current_cash, &cached_scalar_candidates) + .unwrap_or(net_worth_total); + let mut company_holdings = BTreeMap::new(); + for company_id in 1..=company_id_bound { + let slot_offset = record_offset + .checked_add(SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET)? + .checked_add((company_id as usize).checked_mul(4)?)?; + let units = read_u32_at(bytes, slot_offset)?; + if units != 0 { + company_holdings.insert(company_id, units); + } + } + entries.push(SmpLoadedChairmanProfileEntry { + profile_id, + name, + active, + current_cash, + linked_company_id, + company_holdings, + holdings_value_total, + net_worth_total, + purchasing_power_total, + personality_byte_0x291: Some(personality_byte_0x291), + issue_opinion_terms_raw_i32, + }); + } + + Some(SmpLoadedChairmanProfileTable { + source_kind: "save-chairman-profile-direct-records".to_string(), + semantic_family: "scenario-save-chairman-profile-direct-records".to_string(), + observed_entry_count, + selected_chairman_profile_id: selection_probe + .map(|probe| probe.selected_chairman_profile_id), + entries, + }) +} + +pub(in crate::inspect::smp) fn detect_save_chairman_profile_record_start_offset( + bytes: &[u8], + header_probe: &SmpSaveTaggedCollectionHeaderProbe, +) -> Option { + let observed_entry_count = header_probe.live_record_count as usize; + let record_stride = header_probe.direct_record_stride as usize; + let scan_limit = SAVE_CHAIRMAN_RECORD_START_SCAN_LIMIT.min(record_stride); + let base_offset = header_probe.metadata_tag_offset.checked_add(4)?; + let mut best_start = None; + let mut best_score = 0usize; + + for start in 0..scan_limit { + let mut score = 0usize; + let mut seen_ids = std::collections::BTreeSet::new(); + let mut valid = true; + for index in 0usize..observed_entry_count { + let record_offset = match base_offset + .checked_add(start) + .and_then(|offset| offset.checked_add(index.checked_mul(record_stride)?)) + { + Some(offset) => offset, + None => { + valid = false; + break; + } + }; + let profile_id = match read_u32_at(bytes, record_offset) { + Some(value) + if value >= 1 + && value <= header_probe.live_id_bound + && seen_ids.insert(value) => + { + value + } + _ => { + valid = false; + break; + } + }; + match read_u32_at(bytes, record_offset + 4) { + Some(0 | 1) => score += 1, + _ => { + valid = false; + break; + } + } + let name = match read_ascii_c_string_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET, + SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN, + ) { + Some(name) if !name.is_empty() && name.chars().all(is_save_name_char) => name, + _ => { + valid = false; + break; + } + }; + match read_u32_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, + ) { + Some(value) if value <= 0x100 => score += (value != 0) as usize, + _ => { + valid = false; + break; + } + } + match read_f64_at(bytes, record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET) { + Some(value) if value.is_finite() && value.abs() < 1.0e12 => { + score += name.len() + 4; + } + _ => { + valid = false; + break; + } + } + if profile_id == (index + 1) as u32 { + score += 4; + } + } + if valid && score > best_score { + best_score = score; + best_start = Some(start); + } + } + + best_start +} + +pub(in crate::inspect::smp) fn is_save_name_char(ch: char) -> bool { + ch.is_ascii_alphanumeric() + || matches!( + ch, + ' ' | '&' | '\'' | ',' | '.' | '-' | '/' | '(' | ')' | ':' + ) +} + +pub(in crate::inspect::smp) fn build_save_chairman_record_analysis_entries( + bytes: &[u8], + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, + company_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, + company_share_prices: &BTreeMap, +) -> Option> { + let Some(header_probe) = header_probe else { + return Some(Vec::new()); + }; + let record_start_offset = + detect_save_chairman_profile_record_start_offset(bytes, header_probe)?; + let record_stride = header_probe.direct_record_stride as usize; + let base_offset = header_probe + .metadata_tag_offset + .checked_add(4)? + .checked_add(record_start_offset)?; + let company_id_bound = company_header_probe + .map(|probe| probe.live_id_bound) + .unwrap_or(0); + let mut entries = Vec::with_capacity(header_probe.live_record_count as usize); + for index in 0usize..header_probe.live_record_count as usize { + let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?; + let profile_id = read_u32_at(bytes, record_offset)?; + let active = read_u32_at(bytes, record_offset + 4)? != 0; + let name = read_ascii_c_string_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET, + SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN, + )?; + let current_cash = read_f64_at(bytes, record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET)?; + let linked_company_id = parse_nonzero_u32( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, + )?; + let personality_byte_0x291 = read_u8_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET, + )?; + let mut holdings_by_company = BTreeMap::new(); + for company_id in 1..=company_id_bound { + let slot_offset = record_offset + .checked_add(SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET)? + .checked_add((company_id as usize).checked_mul(4)?)?; + let units = read_u32_at(bytes, slot_offset)?; + if units != 0 { + holdings_by_company.insert(company_id, units); + } + } + let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS + .iter() + .map(|relative_offset| { + build_save_qword_candidate(bytes, record_offset, *relative_offset) + }) + .collect::>>()?; + let rounded_current_cash = round_f64_to_i64(current_cash)?; + let derived_holdings_share_price_total = + derive_chairman_holdings_share_price_total(&holdings_by_company, company_share_prices); + let derived_net_worth_share_price_total = derived_holdings_share_price_total + .and_then(|holdings_total| rounded_current_cash.checked_add(holdings_total)); + let derived_cached_purchasing_power_total = derive_chairman_cached_purchasing_power_total( + rounded_current_cash, + &cached_scalar_candidates, + ); + entries.push(SmpSaveChairmanRecordAnalysisEntry { + profile_id, + name, + active, + current_cash, + linked_company_id, + holdings_by_company, + derived_holdings_share_price_total, + derived_net_worth_share_price_total, + derived_cached_purchasing_power_total, + personality_byte_0x291, + personality_byte_0x291_hex: format!("0x{personality_byte_0x291:02x}"), + cached_scalar_candidates, + }); + } + Some(entries) +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/company_records/analysis.rs b/crates/rrt-runtime/src/inspect/smp/world/company_records/analysis.rs new file mode 100644 index 0000000..4434242 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/company_records/analysis.rs @@ -0,0 +1,225 @@ +use std::collections::BTreeMap; + +use super::bonds::{ + parse_save_company_highest_coupon_live_bond_principal, + parse_save_company_largest_live_bond_principal, parse_save_company_live_bond_slots, + parse_save_company_total_debt, +}; +use super::capacity::parse_save_company_available_track_laying_capacity; +use super::offsets::*; +use super::scan::{build_save_dword_candidate, detect_save_company_record_start_offset}; +use super::stat_bands::build_save_company_stat_band_candidates; +use crate::inspect::smp::common::{ + SmpSaveTaggedCollectionHeaderProbe, parse_nonzero_u32, read_ascii_c_string_at, read_f32_at, + read_i32_at, read_u8_at, read_u32_at, +}; +use crate::inspect::smp::world::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn build_save_company_record_analysis_entries( + bytes: &[u8], + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option> { + let Some(header_probe) = header_probe else { + return Some(Vec::new()); + }; + let record_start_offset = detect_save_company_record_start_offset(bytes, header_probe)?; + let record_stride = header_probe.direct_record_stride as usize; + let base_offset = header_probe + .metadata_tag_offset + .checked_add(4)? + .checked_add(record_start_offset)?; + let mut entries = Vec::with_capacity(header_probe.live_record_count as usize); + for index in 0usize..header_probe.live_record_count as usize { + let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?; + let company_id = read_u32_at(bytes, record_offset)?; + let name = read_ascii_c_string_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_NAME_OFFSET, + SAVE_COMPANY_RECORD_NAME_MAX_LEN, + )?; + let active = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET)? != 0; + let linked_chairman_profile_id = parse_nonzero_u32( + bytes, + record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET, + )?; + let outstanding_shares = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET, + )?; + let debt = parse_save_company_total_debt(bytes, record_offset)?; + let bond_count = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)?; + let live_bond_slots = parse_save_company_live_bond_slots(bytes, record_offset)?; + let largest_live_bond_principal = + parse_save_company_largest_live_bond_principal(bytes, record_offset)?; + let highest_coupon_live_bond_principal = + parse_save_company_highest_coupon_live_bond_principal(bytes, record_offset)?; + let available_track_laying_capacity = + parse_save_company_available_track_laying_capacity(bytes, record_offset)?; + let company_value_scalar_f32 = read_f32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET, + )?; + let cached_share_support_scalar_f32 = read_f32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET, + )?; + let cached_share_price_f32 = read_f32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET, + )?; + let chairman_salary_baseline = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET, + )?; + let chairman_salary_current = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET, + )?; + let chairman_bonus_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET, + )?; + let chairman_bonus_amount = read_i32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET, + )?; + let founding_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET, + )?; + let last_bankruptcy_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET, + )?; + let last_dividend_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET, + )?; + let preferred_locomotive_engine_type_raw_u8 = read_u8_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET, + )?; + let city_connection_latch = read_u8_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET, + )? != 0; + let linked_transit_latch = read_u8_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET, + )? != 0; + let linked_transit_autoroute_site_score_cache_refresh_absolute_counter = read_u32_at( + bytes, + record_offset + + SAVE_COMPANY_RECORD_LINKED_TRANSIT_AUTOROUTE_SITE_SCORE_CACHE_REFRESH_ABSOLUTE_COUNTER_OFFSET, + )?; + let linked_transit_site_peer_cache_refresh_absolute_counter = read_u32_at( + bytes, + record_offset + + SAVE_COMPANY_RECORD_LINKED_TRANSIT_SITE_PEER_CACHE_REFRESH_ABSOLUTE_COUNTER_OFFSET, + )?; + let linked_transit_route_anchor_entry_id = parse_nonzero_u32( + bytes, + record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET, + )?; + let linked_transit_route_anchor_fallback_counts = + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_FALLBACK_COUNT_OFFSETS + .iter() + .map(|relative_offset| read_u32_at(bytes, record_offset + *relative_offset)) + .collect::>>()?; + let merger_cooldown_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET, + )?; + let takeover_cooldown_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET, + )?; + let scalar_dword_candidates = SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS + .iter() + .map(|(label, relative_offset)| { + build_save_dword_candidate(bytes, record_offset, label, *relative_offset) + }) + .collect::>>()?; + let post_capacity_dword_candidates = SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS + .iter() + .map(|(label, relative_offset)| { + build_save_dword_candidate(bytes, record_offset, label, *relative_offset) + }) + .collect::>>()?; + let stat_band_root_0cfb_candidates = build_save_company_stat_band_candidates( + bytes, + record_offset, + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET, + "stat_band_0cfb", + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, + )?; + let stat_band_root_0d7f_candidates = build_save_company_stat_band_candidates( + bytes, + record_offset, + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET, + "stat_band_0d7f", + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, + )?; + let stat_band_root_1c47_candidates = build_save_company_stat_band_candidates( + bytes, + record_offset, + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET, + "stat_band_1c47", + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, + )?; + entries.push(SmpSaveCompanyRecordAnalysisEntry { + company_id, + name, + active, + linked_chairman_profile_id, + outstanding_shares, + debt, + bond_count, + live_bond_slots, + largest_live_bond_principal, + highest_coupon_live_bond_principal, + available_track_laying_capacity, + company_value_scalar_f32, + cached_share_support_scalar_f32, + cached_share_price_f32, + chairman_salary_baseline, + chairman_salary_current, + chairman_bonus_year, + chairman_bonus_amount, + founding_year, + last_bankruptcy_year, + last_dividend_year, + preferred_locomotive_engine_type_raw_u8, + preferred_locomotive_engine_type_raw_hex: format!( + "0x{preferred_locomotive_engine_type_raw_u8:02x}" + ), + city_connection_latch, + linked_transit_latch, + linked_transit_autoroute_site_score_cache_refresh_absolute_counter, + linked_transit_site_peer_cache_refresh_absolute_counter, + linked_transit_route_anchor_entry_id, + linked_transit_route_anchor_fallback_counts, + merger_cooldown_year, + takeover_cooldown_year, + scalar_dword_candidates, + post_capacity_dword_candidates, + stat_band_root_0cfb_candidates, + stat_band_root_0d7f_candidates, + stat_band_root_1c47_candidates, + }); + } + Some(entries) +} + +pub(in crate::inspect::smp) fn build_company_share_prices( + company_entries: &[SmpSaveCompanyRecordAnalysisEntry], +) -> BTreeMap { + company_entries + .iter() + .filter_map(|entry| { + round_f64_to_i64(entry.cached_share_price_f32 as f64) + .map(|share_price| (entry.company_id, share_price)) + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/company_records/bonds.rs b/crates/rrt-runtime/src/inspect/smp/world/company_records/bonds.rs new file mode 100644 index 0000000..48e1136 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/company_records/bonds.rs @@ -0,0 +1,92 @@ +use super::offsets::*; +use crate::inspect::smp::common::{read_i32_at, read_u8_at, read_u32_at}; + +pub(in crate::inspect::smp) fn parse_save_company_total_debt( + bytes: &[u8], + record_offset: usize, +) -> Option { + let bond_count = + read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize; + let mut total = 0u64; + for slot_index in 0usize..bond_count { + let slot_offset = record_offset + .checked_add(SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET)? + .checked_add(slot_index.checked_mul(SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE)?)?; + let principal = read_i32_at(bytes, slot_offset)?; + if principal > 0 { + total = total.checked_add(principal as u64)?; + } + } + Some(total) +} + +pub(in crate::inspect::smp) fn parse_save_company_live_bond_slots( + bytes: &[u8], + record_offset: usize, +) -> Option> { + let bond_count = + read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize; + let mut slots = Vec::new(); + for slot_index in 0usize..bond_count { + let slot_offset = record_offset + .checked_add(SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET)? + .checked_add(slot_index.checked_mul(SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE)?)?; + let principal = read_i32_at(bytes, slot_offset)?; + if principal <= 0 { + continue; + } + let maturity_year = read_u32_at(bytes, slot_offset + 4)?; + let coupon_rate_raw_u32 = read_u32_at(bytes, slot_offset + 8)?; + let coupon_rate = f32::from_bits(coupon_rate_raw_u32); + if !coupon_rate.is_finite() { + continue; + } + slots.push(crate::state::RuntimeCompanyBondSlot { + slot_index: slot_index as u32, + principal: principal as u32, + maturity_year, + coupon_rate_raw_u32, + }); + } + Some(slots) +} + +pub(in crate::inspect::smp) fn parse_save_company_largest_live_bond_principal( + bytes: &[u8], + record_offset: usize, +) -> Option> { + let mut largest_live_principal: Option = None; + for slot in parse_save_company_live_bond_slots(bytes, record_offset)? { + largest_live_principal = Some(match largest_live_principal { + Some(current) => current.max(slot.principal), + None => slot.principal, + }); + } + Some(largest_live_principal) +} + +pub(in crate::inspect::smp) fn parse_save_company_highest_coupon_live_bond_principal( + bytes: &[u8], + record_offset: usize, +) -> Option> { + let mut highest_coupon_principal = None; + let mut highest_coupon_rate = None; + for slot in parse_save_company_live_bond_slots(bytes, record_offset)? { + let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32); + match highest_coupon_rate { + Some(current_rate) if coupon_rate < current_rate => {} + Some(current_rate) if coupon_rate == current_rate => { + if let Some(current_principal) = highest_coupon_principal { + if slot.principal > current_principal { + highest_coupon_principal = Some(slot.principal); + } + } + } + _ => { + highest_coupon_rate = Some(coupon_rate); + highest_coupon_principal = Some(slot.principal); + } + } + } + Some(highest_coupon_principal) +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/company_records/capacity.rs b/crates/rrt-runtime/src/inspect/smp/world/company_records/capacity.rs new file mode 100644 index 0000000..366f209 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/company_records/capacity.rs @@ -0,0 +1,17 @@ +use super::offsets::*; +use crate::inspect::smp::common::read_i32_at; + +pub(in crate::inspect::smp) fn parse_save_company_available_track_laying_capacity( + bytes: &[u8], + record_offset: usize, +) -> Option> { + let raw = read_i32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET, + )?; + if raw < 0 { + Some(None) + } else { + Some(Some(raw as u32)) + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/company_records/mod.rs b/crates/rrt-runtime/src/inspect/smp/world/company_records/mod.rs new file mode 100644 index 0000000..b4acbef --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/company_records/mod.rs @@ -0,0 +1,7 @@ +pub(in crate::inspect::smp) mod analysis; +pub(in crate::inspect::smp) mod bonds; +pub(in crate::inspect::smp) mod capacity; +pub(in crate::inspect::smp) mod offsets; +pub(in crate::inspect::smp) mod roster; +pub(in crate::inspect::smp) mod scan; +pub(in crate::inspect::smp) mod stat_bands; diff --git a/crates/rrt-runtime/src/inspect/smp/world/company_records/offsets.rs b/crates/rrt-runtime/src/inspect/smp/world/company_records/offsets.rs new file mode 100644 index 0000000..520294b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/company_records/offsets.rs @@ -0,0 +1,104 @@ +use crate::inspect::smp::catalog::offsets::world::RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT; + +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_NAME_OFFSET: usize = 0x04; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_NAME_MAX_LEN: usize = 0x24; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET: usize = 0x3b; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_ACTIVE_OFFSET: usize = 0x3f; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET: usize = 0x47; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET: usize = 0x4f; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET: usize = 0x57; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET: usize = 0x5b; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET: usize = 0x5f; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE: usize = 12; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET: usize = + 0x14f; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET: usize = 0x34f; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET: usize = 0x353; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET: usize = 0x15f; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET: usize = 0x157; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET: usize = 0x16b; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2: usize = + 0x16f; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET: usize = 0x173; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2: usize = 0x177; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET: usize = 0x289; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET: usize = 0x0d2d; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET: + usize = 0x0d17; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET: + usize = 0x0d35; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_LINKED_TRANSIT_AUTOROUTE_SITE_SCORE_CACHE_REFRESH_ABSOLUTE_COUNTER_OFFSET: usize = + 0x0d3a; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_LINKED_TRANSIT_SITE_PEER_CACHE_REFRESH_ABSOLUTE_COUNTER_OFFSET: usize = + 0x0d3e; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET: usize = 0x0d07; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET: usize = + 0x0d59; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET: usize = + 0x0d19; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET: usize = 0x0d7b; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET: usize = 0x7680; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_FALLBACK_COUNT_OFFSETS: [usize; 3] = + [0x7664, 0x7668, 0x766c]; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET: usize = 0x0cfb; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET: usize = 0x0d7f; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET: usize = 0x1c47; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS: usize = 32; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x2ab; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_ISSUE_OPINION_TERM_COUNT: usize = + RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT: usize = + crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_COUNT as usize + * crate::event::metrics::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN as usize; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT: usize = + crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_COUNT as usize; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_FLOAT_FIELDS: + [usize; 10] = [ + 0x4b, 0x53, 0x323, 0x327, 0x32b, 0x32f, 0x333, 0x337, 0x33b, 0x33f, +]; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_INT_FIELDS: [usize; + 5] = [0x14f, 0x34b, 0x0d0b, 0x0d0f, 0x0d13]; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 9] = [ + ("mutable_support_scalar", 0x4f), + ("young_company_support_scalar", 0x57), + ( + "support_progress_word", + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET, + ), + ( + "recent_per_share_subscore", + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET, + ), + ("cached_share_price", 0x0d7b), + ( + "current_issue_calendar_word", + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET, + ), + ( + "current_issue_calendar_word_2", + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2, + ), + ( + "prior_issue_calendar_word", + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET, + ), + ( + "prior_issue_calendar_word_2", + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2, + ), +]; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [( + &str, + usize, +); 6] = [ + ("post_capacity_word_1", 0x7684), + ("post_capacity_word_2", 0x7688), + ("post_capacity_word_3", 0x768c), + ("post_capacity_word_4", 0x7690), + ("post_capacity_word_5", 0x7694), + ("post_capacity_word_6", 0x7698), +]; +pub(in crate::inspect::smp) const SAVE_COMPANY_RECORD_START_SCAN_LIMIT: usize = 0x120; diff --git a/crates/rrt-runtime/src/inspect/smp/world/company_records/roster.rs b/crates/rrt-runtime/src/inspect/smp/world/company_records/roster.rs new file mode 100644 index 0000000..5454f89 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/company_records/roster.rs @@ -0,0 +1,296 @@ +use super::bonds::{ + parse_save_company_highest_coupon_live_bond_principal, + parse_save_company_largest_live_bond_principal, parse_save_company_live_bond_slots, + parse_save_company_total_debt, +}; +use super::capacity::parse_save_company_available_track_laying_capacity; +use super::offsets::*; +use super::scan::detect_save_company_record_start_offset; +use super::stat_bands::{ + build_save_company_stat_band_candidates, build_save_company_stat_qword_bits, + build_save_i32_term_strip, build_save_u32_field_map, + decode_save_company_current_year_stat_slot, runtime_company_stat_band_candidate_from_save, +}; +use crate::inspect::smp::common::{ + SmpSaveTaggedCollectionHeaderProbe, parse_nonzero_u32, read_i32_at, read_u8_at, read_u32_at, + read_u64_at, +}; +use crate::inspect::smp::world::*; +use crate::inspect::smp::*; + +pub(in crate::inspect::smp) fn parse_save_company_roster_probe( + bytes: &[u8], + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, + selection_probe: Option<&SmpSaveWorldSelectionContextProbe>, +) -> Option { + let header_probe = header_probe?; + let observed_entry_count = header_probe.live_record_count as usize; + if observed_entry_count == 0 { + return Some(SmpLoadedCompanyRoster { + source_kind: "save-company-direct-records".to_string(), + semantic_family: "scenario-save-company-direct-records".to_string(), + observed_entry_count, + selected_company_id: selection_probe.map(|probe| probe.selected_company_id), + entries: Vec::new(), + }); + } + + let record_start_offset = detect_save_company_record_start_offset(bytes, header_probe)?; + let record_stride = header_probe.direct_record_stride as usize; + let base_offset = header_probe + .metadata_tag_offset + .checked_add(4)? + .checked_add(record_start_offset)?; + + let mut entries = Vec::with_capacity(observed_entry_count); + for index in 0usize..observed_entry_count { + let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?; + let company_id = read_u32_at(bytes, record_offset)?; + let active = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET)? != 0; + let linked_chairman_profile_id = parse_nonzero_u32( + bytes, + record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET, + )?; + let outstanding_shares = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET, + )?; + let debt = parse_save_company_total_debt(bytes, record_offset)?; + let bond_count = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)?; + let live_bond_slots = parse_save_company_live_bond_slots(bytes, record_offset)?; + let largest_live_bond_principal = + parse_save_company_largest_live_bond_principal(bytes, record_offset)?; + let highest_coupon_live_bond_principal = + parse_save_company_highest_coupon_live_bond_principal(bytes, record_offset)?; + let available_track_laying_capacity = + parse_save_company_available_track_laying_capacity(bytes, record_offset)?; + let mutable_support_scalar_raw_u32 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET, + )?; + let young_company_support_scalar_raw_u32 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET, + )?; + let support_progress_word = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET, + )?; + let recent_per_share_cache_absolute_counter = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET, + )?; + let recent_per_share_cached_value_bits = read_u64_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 4, + )?; + let recent_per_share_subscore_raw_u32 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET, + )?; + let cached_share_price_raw_u32 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET, + )?; + let chairman_salary_baseline = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET, + )?; + let chairman_salary_current = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET, + )?; + let chairman_bonus_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET, + )?; + let chairman_bonus_amount = read_i32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET, + )?; + let founding_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET, + )?; + let last_bankruptcy_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET, + )?; + let last_dividend_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET, + )?; + let current_issue_calendar_word = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET, + )?; + let current_issue_calendar_word_2 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2, + )?; + let prior_issue_calendar_word = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET, + )?; + let prior_issue_calendar_word_2 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2, + )?; + let preferred_locomotive_engine_type_raw_u8 = read_u8_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET, + )?; + let city_connection_latch = read_u8_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET, + )? != 0; + let linked_transit_latch = read_u8_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET, + )? != 0; + let linked_transit_route_anchor_entry_id = parse_nonzero_u32( + bytes, + record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET, + )?; + let linked_transit_route_anchor_fallback_counts = + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_FALLBACK_COUNT_OFFSETS + .iter() + .map(|relative_offset| read_u32_at(bytes, record_offset + *relative_offset)) + .collect::>>()?; + let merger_cooldown_year = parse_nonzero_u32( + bytes, + record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET, + )?; + let takeover_cooldown_year = parse_nonzero_u32( + bytes, + record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET, + )?; + let stat_band_root_0cfb_candidates = build_save_company_stat_band_candidates( + bytes, + record_offset, + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET, + "stat_band_0cfb", + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, + )?; + let stat_band_root_0d7f_candidates = build_save_company_stat_band_candidates( + bytes, + record_offset, + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET, + "stat_band_0d7f", + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, + )?; + let stat_band_root_1c47_candidates = build_save_company_stat_band_candidates( + bytes, + record_offset, + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET, + "stat_band_1c47", + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, + )?; + let year_stat_family_qword_bits = build_save_company_stat_qword_bits( + bytes, + record_offset, + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET, + SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT, + )?; + let special_stat_family_232a_qword_bits = build_save_company_stat_qword_bits( + bytes, + record_offset, + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET, + SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT, + )?; + let direct_control_transfer_float_fields_raw_u32 = build_save_u32_field_map( + bytes, + record_offset, + &SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_FLOAT_FIELDS, + )?; + let direct_control_transfer_int_fields_raw_u32 = build_save_u32_field_map( + bytes, + record_offset, + &SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_INT_FIELDS, + )?; + let issue_opinion_terms_raw_i32 = build_save_i32_term_strip( + bytes, + record_offset, + SAVE_COMPANY_RECORD_ISSUE_OPINION_TERMS_OFFSET, + SAVE_COMPANY_RECORD_ISSUE_OPINION_TERM_COUNT, + )?; + let current_cash = decode_save_company_current_year_stat_slot( + &year_stat_family_qword_bits, + crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(round_f64_to_i64) + .unwrap_or(0); + entries.push(SmpLoadedCompanyRosterEntry { + company_id, + active, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash, + debt, + credit_rating_score: None, + prime_rate: None, + available_track_laying_capacity, + track_piece_counts: RuntimeTrackPieceCounts::default(), + linked_chairman_profile_id, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year, + merger_cooldown_year, + preferred_locomotive_engine_type_raw_u8: Some(preferred_locomotive_engine_type_raw_u8), + market_state: Some(RuntimeCompanyMarketState { + outstanding_shares, + bond_count, + live_bond_slots, + largest_live_bond_principal, + highest_coupon_live_bond_principal, + mutable_support_scalar_raw_u32, + young_company_support_scalar_raw_u32, + support_progress_word, + recent_per_share_cache_absolute_counter, + recent_per_share_cached_value_bits, + recent_per_share_subscore_raw_u32, + cached_share_price_raw_u32, + chairman_salary_baseline, + chairman_salary_current, + chairman_bonus_year, + chairman_bonus_amount, + founding_year, + last_bankruptcy_year, + last_dividend_year, + current_issue_calendar_word, + current_issue_calendar_word_2, + prior_issue_calendar_word, + prior_issue_calendar_word_2, + city_connection_latch, + linked_transit_latch, + linked_transit_route_anchor_entry_id, + linked_transit_route_anchor_fallback_counts, + stat_band_root_0cfb_candidates: stat_band_root_0cfb_candidates + .iter() + .map(runtime_company_stat_band_candidate_from_save) + .collect(), + stat_band_root_0d7f_candidates: stat_band_root_0d7f_candidates + .iter() + .map(runtime_company_stat_band_candidate_from_save) + .collect(), + stat_band_root_1c47_candidates: stat_band_root_1c47_candidates + .iter() + .map(runtime_company_stat_band_candidate_from_save) + .collect(), + year_stat_family_qword_bits, + special_stat_family_232a_qword_bits, + issue_opinion_terms_raw_i32, + direct_control_transfer_float_fields_raw_u32, + direct_control_transfer_int_fields_raw_u32, + }), + }); + } + + Some(SmpLoadedCompanyRoster { + source_kind: "save-company-direct-records".to_string(), + semantic_family: "scenario-save-company-direct-records".to_string(), + observed_entry_count, + selected_company_id: selection_probe.map(|probe| probe.selected_company_id), + entries, + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/company_records/scan.rs b/crates/rrt-runtime/src/inspect/smp/world/company_records/scan.rs new file mode 100644 index 0000000..bc1db01 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/company_records/scan.rs @@ -0,0 +1,115 @@ +use super::offsets::*; +use crate::inspect::smp::common::{ + SmpSaveDwordCandidate, SmpSaveTaggedCollectionHeaderProbe, read_ascii_c_string_at, read_u8_at, + read_u32_at, +}; +use crate::inspect::smp::world::is_save_name_char; + +pub(in crate::inspect::smp) fn detect_save_company_record_start_offset( + bytes: &[u8], + header_probe: &SmpSaveTaggedCollectionHeaderProbe, +) -> Option { + let observed_entry_count = header_probe.live_record_count as usize; + let record_stride = header_probe.direct_record_stride as usize; + let scan_limit = SAVE_COMPANY_RECORD_START_SCAN_LIMIT.min(record_stride); + let base_offset = header_probe.metadata_tag_offset.checked_add(4)?; + let mut best_start = None; + let mut best_score = 0usize; + + for start in 0..scan_limit { + let mut score = 0usize; + let mut seen_ids = std::collections::BTreeSet::new(); + let mut valid = true; + for index in 0usize..observed_entry_count { + let record_offset = match base_offset + .checked_add(start) + .and_then(|offset| offset.checked_add(index.checked_mul(record_stride)?)) + { + Some(offset) => offset, + None => { + valid = false; + break; + } + }; + let company_id = match read_u32_at(bytes, record_offset) { + Some(value) + if value >= 1 + && value <= header_probe.live_id_bound + && seen_ids.insert(value) => + { + value + } + _ => { + valid = false; + break; + } + }; + let active = match read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET) + { + Some(0 | 1) => { + score += 1; + true + } + _ => { + valid = false; + break; + } + }; + let linked = match read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET, + ) { + Some(value) if value <= 0x100 => value, + _ => { + valid = false; + break; + } + }; + let name = match read_ascii_c_string_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_NAME_OFFSET, + SAVE_COMPANY_RECORD_NAME_MAX_LEN, + ) { + Some(name) if !name.is_empty() && name.chars().all(is_save_name_char) => name, + _ => { + valid = false; + break; + } + }; + score += name.len(); + if active { + score += 8; + } + if linked != 0 { + score += 2; + } + if company_id == (index + 1) as u32 { + score += 4; + } + } + if valid && score > best_score { + best_score = score; + best_start = Some(start); + } + } + + best_start +} + +pub(in crate::inspect::smp) fn build_save_dword_candidate( + bytes: &[u8], + record_offset: usize, + label: &str, + relative_offset: usize, +) -> Option { + let raw_u32 = read_u32_at(bytes, record_offset + relative_offset)?; + Some(SmpSaveDwordCandidate { + label: label.to_string(), + relative_offset, + relative_offset_hex: format!("0x{relative_offset:x}"), + raw_u32, + raw_u32_hex: format!("0x{raw_u32:08x}"), + value_i32: raw_u32 as i32, + value_f32: f32::from_bits(raw_u32), + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/company_records/stat_bands.rs b/crates/rrt-runtime/src/inspect/smp/world/company_records/stat_bands.rs new file mode 100644 index 0000000..5d4e26f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/company_records/stat_bands.rs @@ -0,0 +1,87 @@ +use std::collections::BTreeMap; + +use crate::inspect::smp::common::{read_i32_at, read_u32_at, read_u64_at}; +use crate::inspect::smp::world::SmpSaveDwordCandidate; + +pub(in crate::inspect::smp) fn runtime_company_stat_band_candidate_from_save( + candidate: &SmpSaveDwordCandidate, +) -> crate::state::RuntimeCompanyStatBandCandidate { + crate::state::RuntimeCompanyStatBandCandidate { + label: candidate.label.clone(), + relative_offset: candidate.relative_offset, + relative_offset_hex: candidate.relative_offset_hex.clone(), + raw_u32: candidate.raw_u32, + raw_u32_hex: candidate.raw_u32_hex.clone(), + value_i32: candidate.value_i32, + value_f32_text: format!("{:.6}", candidate.value_f32), + } +} + +pub(in crate::inspect::smp) fn build_save_company_stat_band_candidates( + bytes: &[u8], + record_offset: usize, + root_offset: usize, + label_prefix: &str, + word_count: usize, +) -> Option> { + (0..word_count) + .map(|index| { + let relative_offset = root_offset.checked_add(index.checked_mul(4)?)?; + let label = format!("{label_prefix}_word_{}", index + 1); + super::scan::build_save_dword_candidate(bytes, record_offset, &label, relative_offset) + }) + .collect::>>() +} + +pub(in crate::inspect::smp) fn build_save_company_stat_qword_bits( + bytes: &[u8], + record_offset: usize, + root_offset: usize, + qword_count: usize, +) -> Option> { + (0..qword_count) + .map(|index| { + let relative_offset = root_offset.checked_add(index.checked_mul(8)?)?; + read_u64_at(bytes, record_offset + relative_offset) + }) + .collect::>>() +} + +pub(in crate::inspect::smp) fn build_save_i32_term_strip( + bytes: &[u8], + record_offset: usize, + root_offset: usize, + value_count: usize, +) -> Option> { + (0..value_count) + .map(|index| { + let relative_offset = root_offset.checked_add(index.checked_mul(4)?)?; + read_i32_at(bytes, record_offset + relative_offset) + }) + .collect::>>() +} + +pub(in crate::inspect::smp) fn build_save_u32_field_map( + bytes: &[u8], + record_offset: usize, + offsets: &[usize], +) -> Option> { + let mut fields = BTreeMap::new(); + for relative_offset in offsets { + fields.insert( + u32::try_from(*relative_offset).ok()?, + read_u32_at(bytes, record_offset + *relative_offset)?, + ); + } + Some(fields) +} + +pub(in crate::inspect::smp) fn decode_save_company_current_year_stat_slot( + year_stat_family_qword_bits: &[u64], + slot_id: u32, +) -> Option { + let index = + slot_id.checked_mul(crate::event::metrics::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? as usize; + let value = f64::from_bits(*year_stat_family_qword_bits.get(index)?); + value.is_finite().then_some(value) +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/derived_state.rs b/crates/rrt-runtime/src/inspect/smp/world/derived_state.rs new file mode 100644 index 0000000..26e5e00 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/derived_state.rs @@ -0,0 +1,202 @@ +use crate::inspect::smp::special_conditions::{ + SmpLocomotivePolicyNeighborhoodProbe, SmpPostTextFieldNeighborhoodProbe, +}; +use crate::inspect::smp::world::*; + +pub(in crate::inspect::smp) fn derive_loaded_world_issue_37_state_from_probe( + probe: &SmpSaveWorldIssue37Probe, +) -> SmpLoadedWorldIssue37State { + SmpLoadedWorldIssue37State { + source_kind: probe.source_kind.clone(), + semantic_family: probe.semantic_family.clone(), + issue_value: probe.issue_value_lane.raw_u32, + issue_value_hex: probe.issue_value_lane.raw_u32_hex.clone(), + issue_38_value: u32::from(probe.issue_38_raw_u8), + issue_38_value_hex: probe.issue_38_raw_hex.clone(), + issue_39_value: u32::from(probe.issue_39_raw_u8), + issue_39_value_hex: probe.issue_39_raw_hex.clone(), + issue_3a_value: u32::from(probe.issue_3a_raw_u8), + issue_3a_value_hex: probe.issue_3a_raw_hex.clone(), + multiplier_raw_u32: probe.multiplier_lane.raw_u32, + multiplier_raw_hex: probe.multiplier_lane.raw_u32_hex.clone(), + multiplier_value_f32_text: format!("{:.6}", probe.multiplier_lane.value_f32), + issue_opinion_base_terms_raw_i32: probe.issue_opinion_base_terms_raw_i32.clone(), + } +} + +pub(in crate::inspect::smp) fn derive_loaded_world_economic_tuning_state_from_probe( + probe: &SmpSaveWorldEconomicTuningProbe, +) -> SmpLoadedWorldEconomicTuningState { + SmpLoadedWorldEconomicTuningState { + source_kind: probe.source_kind.clone(), + semantic_family: probe.semantic_family.clone(), + mirror_raw_u32: probe.mirror_lane.raw_u32, + mirror_raw_hex: probe.mirror_lane.raw_u32_hex.clone(), + mirror_value_f32_text: format!("{:.6}", probe.mirror_lane.value_f32), + lane_raw_u32: probe.tuning_lanes.iter().map(|lane| lane.raw_u32).collect(), + lane_raw_hex: probe + .tuning_lanes + .iter() + .map(|lane| lane.raw_u32_hex.clone()) + .collect(), + lane_value_f32_text: probe + .tuning_lanes + .iter() + .map(|lane| format!("{:.6}", lane.value_f32)) + .collect(), + } +} + +pub(in crate::inspect::smp) fn derive_loaded_world_finance_neighborhood_state_from_probe( + probe: &SmpSaveWorldFinanceNeighborhoodProbe, +) -> SmpLoadedWorldFinanceNeighborhoodState { + SmpLoadedWorldFinanceNeighborhoodState { + source_kind: probe.source_kind.clone(), + semantic_family: probe.semantic_family.clone(), + packed_year_word_raw_u16: probe.packed_year_word_raw_u16, + packed_year_word_raw_hex: probe.packed_year_word_raw_hex.clone(), + partial_year_progress_raw_u8: probe.partial_year_progress_raw_u8, + partial_year_progress_raw_hex: probe.partial_year_progress_raw_hex.clone(), + current_calendar_tuple_word_raw_u32: probe.current_calendar_tuple_word_lane.raw_u32, + current_calendar_tuple_word_raw_hex: probe + .current_calendar_tuple_word_lane + .raw_u32_hex + .clone(), + current_calendar_tuple_word_2_raw_u32: probe.current_calendar_tuple_word_2_lane.raw_u32, + current_calendar_tuple_word_2_raw_hex: probe + .current_calendar_tuple_word_2_lane + .raw_u32_hex + .clone(), + absolute_counter_raw_u32: probe.absolute_counter_lane.raw_u32, + absolute_counter_raw_hex: probe.absolute_counter_lane.raw_u32_hex.clone(), + absolute_counter_mirror_raw_u32: probe.absolute_counter_mirror_lane.raw_u32, + absolute_counter_mirror_raw_hex: probe.absolute_counter_mirror_lane.raw_u32_hex.clone(), + stock_policy_raw_u8: probe.stock_policy_raw_u8, + stock_policy_raw_hex: probe.stock_policy_raw_hex.clone(), + bond_policy_raw_u8: probe.bond_policy_raw_u8, + bond_policy_raw_hex: probe.bond_policy_raw_hex.clone(), + bankruptcy_policy_raw_u8: probe.bankruptcy_policy_raw_u8, + bankruptcy_policy_raw_hex: probe.bankruptcy_policy_raw_hex.clone(), + dividend_policy_raw_u8: probe.dividend_policy_raw_u8, + dividend_policy_raw_hex: probe.dividend_policy_raw_hex.clone(), + building_density_growth_setting_raw_u32: probe.building_density_growth_setting_lane.raw_u32, + building_density_growth_setting_raw_hex: probe + .building_density_growth_setting_lane + .raw_u32_hex + .clone(), + labels: probe + .dword_candidates + .iter() + .map(|candidate| candidate.label.clone()) + .collect(), + relative_offsets: probe + .dword_candidates + .iter() + .map(|candidate| candidate.relative_offset) + .collect(), + relative_offset_hex: probe + .dword_candidates + .iter() + .map(|candidate| candidate.relative_offset_hex.clone()) + .collect(), + raw_u32: probe + .dword_candidates + .iter() + .map(|candidate| candidate.raw_u32) + .collect(), + raw_hex: probe + .dword_candidates + .iter() + .map(|candidate| candidate.raw_u32_hex.clone()) + .collect(), + value_i32: probe + .dword_candidates + .iter() + .map(|candidate| candidate.value_i32) + .collect(), + value_f32_text: probe + .dword_candidates + .iter() + .map(|candidate| format!("{:.6}", candidate.value_f32)) + .collect(), + } +} + +pub(in crate::inspect::smp) fn derive_loaded_world_locomotive_policy_state_from_probes( + post_text_probe: Option<&SmpPostTextFieldNeighborhoodProbe>, + locomotive_policy_probe: Option<&SmpLocomotivePolicyNeighborhoodProbe>, +) -> Option { + let field_by_name = |name: &str| { + locomotive_policy_probe? + .grounded_field_observations + .iter() + .find(|field| field.field_name == name) + }; + let post_text_field_by_name = |name: &str| { + post_text_probe? + .grounded_field_observations + .iter() + .find(|field| field.field_name == name) + }; + let selected_year_gap_scalar = field_by_name("selected-year bucket companion scalar"); + let linked_site_gate = field_by_name("linked-site removal follow-on gate"); + let auto_show_grade = post_text_field_by_name("Auto-Show Grade During Track Lay"); + let starting_building_density = post_text_field_by_name("Starting Building Density Level"); + let building_density_growth = post_text_field_by_name("Building Density Growth"); + let leftover_simulation_time = post_text_field_by_name("leftover simulation time accumulator"); + let selected_year_snapshot = post_text_field_by_name("selected-year lane snapshot"); + let all_steam = field_by_name("All Steam Locos Avail."); + let all_diesel = field_by_name("All Diesel Locos Avail."); + let all_electric = field_by_name("All Electric Locos Avail."); + let cached_available_rating = field_by_name("cached available-locomotive rating"); + Some(SmpLoadedWorldLocomotivePolicyState { + source_kind: locomotive_policy_probe + .map(|probe| probe.source_kind.clone()) + .or_else(|| post_text_probe.map(|probe| probe.source_kind.clone()))?, + semantic_family: "world-locomotive-policy".to_string(), + selected_year_gap_scalar_raw_u32: selected_year_gap_scalar + .and_then(|field| field.value_u32), + selected_year_gap_scalar_raw_hex: selected_year_gap_scalar + .and_then(|field| field.value_u32_hex.clone()), + selected_year_gap_scalar_value_f32_text: selected_year_gap_scalar + .and_then(|field| field.probable_f32_le.clone()), + linked_site_removal_follow_on_gate_raw_u8: linked_site_gate + .and_then(|field| field.value_u8), + linked_site_removal_follow_on_gate_raw_hex: linked_site_gate + .and_then(|field| field.value_u8_hex.clone()), + auto_show_grade_during_track_lay_raw_u8: auto_show_grade.and_then(|field| field.value_u8), + auto_show_grade_during_track_lay_raw_hex: auto_show_grade + .and_then(|field| field.value_u8_hex.clone()), + starting_building_density_level_raw_u8: starting_building_density + .and_then(|field| field.value_u8), + starting_building_density_level_raw_hex: starting_building_density + .and_then(|field| field.value_u8_hex.clone()), + building_density_growth_raw_u8: building_density_growth.and_then(|field| field.value_u8), + building_density_growth_raw_hex: building_density_growth + .and_then(|field| field.value_u8_hex.clone()), + leftover_simulation_time_accumulator_raw_u32: leftover_simulation_time + .and_then(|field| field.value_u32), + leftover_simulation_time_accumulator_raw_hex: leftover_simulation_time + .and_then(|field| field.value_u32_hex.clone()), + leftover_simulation_time_accumulator_value_f32_text: leftover_simulation_time + .and_then(|field| field.probable_f32_le.clone()), + selected_year_lane_snapshot_raw_u8: selected_year_snapshot.and_then(|field| field.value_u8), + selected_year_lane_snapshot_raw_hex: selected_year_snapshot + .and_then(|field| field.value_u8_hex.clone()), + all_steam_locomotives_available_raw_u8: all_steam.and_then(|field| field.value_u8), + all_steam_locomotives_available_raw_hex: all_steam + .and_then(|field| field.value_u8_hex.clone()), + all_diesel_locomotives_available_raw_u8: all_diesel.and_then(|field| field.value_u8), + all_diesel_locomotives_available_raw_hex: all_diesel + .and_then(|field| field.value_u8_hex.clone()), + all_electric_locomotives_available_raw_u8: all_electric.and_then(|field| field.value_u8), + all_electric_locomotives_available_raw_hex: all_electric + .and_then(|field| field.value_u8_hex.clone()), + cached_available_locomotive_rating_raw_u32: cached_available_rating + .and_then(|field| field.value_u32), + cached_available_locomotive_rating_raw_hex: cached_available_rating + .and_then(|field| field.value_u32_hex.clone()), + cached_available_locomotive_rating_value_f32_text: cached_available_rating + .and_then(|field| field.probable_f32_le.clone()), + }) +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/entrypoints.rs b/crates/rrt-runtime/src/inspect/smp/world/entrypoints.rs new file mode 100644 index 0000000..5a9f929 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/entrypoints.rs @@ -0,0 +1,19 @@ +use std::path::Path; + +use crate::inspect::smp::bundle::SmpInspectionReport; +use crate::inspect::smp::world::SmpSaveCompanyChairmanAnalysisReport; + +use super::analysis; + +pub fn inspect_save_company_and_chairman_analysis_file( + path: &Path, +) -> Result> { + analysis::inspect_save_company_and_chairman_analysis_file(path) +} + +pub fn inspect_save_company_and_chairman_analysis_bytes( + bytes: &[u8], + report: &SmpInspectionReport, +) -> Option { + analysis::inspect_save_company_and_chairman_analysis_bytes(bytes, report) +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/mod.rs b/crates/rrt-runtime/src/inspect/smp/world/mod.rs new file mode 100644 index 0000000..d373658 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/mod.rs @@ -0,0 +1,33 @@ +mod analysis; +mod catalogs; +mod chairman_records; +mod company_records; +mod derived_state; +pub(in crate::inspect::smp) mod entrypoints; +mod model; +mod notes; +mod selection; +mod world_probes; + +pub use super::common::SmpSaveDwordCandidate; +pub use entrypoints::{ + inspect_save_company_and_chairman_analysis_bytes, + inspect_save_company_and_chairman_analysis_file, +}; +pub use model::*; + +pub(in crate::inspect::smp) use catalogs::*; +pub(in crate::inspect::smp) use chairman_records::*; +pub(in crate::inspect::smp) use company_records::analysis::{ + build_company_share_prices, build_save_company_record_analysis_entries, +}; +pub(in crate::inspect::smp) use company_records::roster::parse_save_company_roster_probe; +pub(in crate::inspect::smp) use company_records::scan::build_save_dword_candidate; +pub(in crate::inspect::smp) use company_records::stat_bands::build_save_i32_term_strip; +pub(in crate::inspect::smp) use derived_state::*; +pub(in crate::inspect::smp) use notes::*; +pub(in crate::inspect::smp) use selection::*; +pub(in crate::inspect::smp) use world_probes::*; + +#[cfg(test)] +pub(super) use company_records::offsets::*; diff --git a/crates/rrt-runtime/src/inspect/smp/world/model/analysis.rs b/crates/rrt-runtime/src/inspect/smp/world/model/analysis.rs new file mode 100644 index 0000000..6cd17d2 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/model/analysis.rs @@ -0,0 +1,150 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::common::{ + SmpSaveDwordCandidate, SmpSaveTaggedCollectionHeaderProbe, + SmpSaveTrainCollectionDirectoryProbe, SmpSaveUnclassifiedTaggedCollectionHeaderProbe, +}; +use crate::inspect::smp::regions::{ + SmpSaveRegionFixedRowRunCandidateProbe, SmpSaveRegionQueuedNoticeRecordProbe, + SmpSaveRegionRecordTripletProbe, +}; +use crate::inspect::smp::structures::{ + SmpSavePlacedStructureDynamicSideBufferAlignmentProbe, + SmpSavePlacedStructureDynamicSideBufferProbe, SmpSavePlacedStructureRecordTripletProbe, +}; +use crate::inspect::smp::world::{ + SmpSaveWorldEconomicTuningProbe, SmpSaveWorldFinanceNeighborhoodProbe, + SmpSaveWorldIssue37Probe, SmpSaveWorldSelectionRoleAnalysis, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveScalarCandidate { + pub relative_offset: usize, + pub relative_offset_hex: String, + pub raw_u64: u64, + pub raw_u64_hex: String, + pub value_i64: i64, + pub value_f64: f64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveCompanyRecordAnalysisEntry { + pub company_id: u32, + pub name: String, + pub active: bool, + #[serde(default)] + pub linked_chairman_profile_id: Option, + pub outstanding_shares: u32, + pub debt: u64, + pub bond_count: u8, + #[serde(default)] + pub live_bond_slots: Vec, + #[serde(default)] + pub largest_live_bond_principal: Option, + #[serde(default)] + pub highest_coupon_live_bond_principal: Option, + #[serde(default)] + pub available_track_laying_capacity: Option, + pub company_value_scalar_f32: f32, + pub cached_share_support_scalar_f32: f32, + pub cached_share_price_f32: f32, + pub chairman_salary_baseline: u32, + pub chairman_salary_current: u32, + pub chairman_bonus_year: u32, + pub chairman_bonus_amount: i32, + pub founding_year: u32, + pub last_bankruptcy_year: u32, + pub last_dividend_year: u32, + pub preferred_locomotive_engine_type_raw_u8: u8, + pub preferred_locomotive_engine_type_raw_hex: String, + pub city_connection_latch: bool, + pub linked_transit_latch: bool, + pub linked_transit_autoroute_site_score_cache_refresh_absolute_counter: u32, + pub linked_transit_site_peer_cache_refresh_absolute_counter: u32, + #[serde(default)] + pub linked_transit_route_anchor_entry_id: Option, + #[serde(default)] + pub linked_transit_route_anchor_fallback_counts: Vec, + pub merger_cooldown_year: u32, + pub takeover_cooldown_year: u32, + #[serde(default)] + pub scalar_dword_candidates: Vec, + #[serde(default)] + pub post_capacity_dword_candidates: Vec, + #[serde(default)] + pub stat_band_root_0cfb_candidates: Vec, + #[serde(default)] + pub stat_band_root_0d7f_candidates: Vec, + #[serde(default)] + pub stat_band_root_1c47_candidates: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveChairmanRecordAnalysisEntry { + pub profile_id: u32, + pub name: String, + pub active: bool, + pub current_cash: f64, + #[serde(default)] + pub linked_company_id: Option, + #[serde(default)] + pub holdings_by_company: BTreeMap, + #[serde(default)] + pub derived_holdings_share_price_total: Option, + #[serde(default)] + pub derived_net_worth_share_price_total: Option, + #[serde(default)] + pub derived_cached_purchasing_power_total: Option, + pub personality_byte_0x291: u8, + pub personality_byte_0x291_hex: String, + #[serde(default)] + pub cached_scalar_candidates: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveCompanyChairmanAnalysisReport { + pub profile_family: String, + #[serde(default)] + pub selected_company_id: Option, + #[serde(default)] + pub selected_chairman_profile_id: Option, + #[serde(default)] + pub world_selection_context: Option, + #[serde(default)] + pub world_issue_37: Option, + #[serde(default)] + pub world_economic_tuning: Option, + #[serde(default)] + pub world_finance_neighborhood: Option, + #[serde(default)] + pub train_collection_header: Option, + #[serde(default)] + pub train_collection_directory: Option, + #[serde(default)] + pub region_collection_header: Option, + #[serde(default)] + pub region_record_triplets: Option, + #[serde(default)] + pub region_queued_notice_records: Option, + #[serde(default)] + pub region_fixed_row_run_candidates: Option, + #[serde(default)] + pub placed_structure_collection_header: Option, + #[serde(default)] + pub placed_structure_record_triplets: Option, + #[serde(default)] + pub placed_structure_dynamic_side_buffer: Option, + #[serde(default)] + pub placed_structure_dynamic_side_buffer_alignment: + Option, + #[serde(default)] + pub unclassified_tagged_collection_headers: Vec, + #[serde(default)] + pub company_entries: Vec, + #[serde(default)] + pub chairman_entries: Vec, + #[serde(default)] + pub notes: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/model/mod.rs b/crates/rrt-runtime/src/inspect/smp/world/model/mod.rs new file mode 100644 index 0000000..8626ab2 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/model/mod.rs @@ -0,0 +1,9 @@ +mod analysis; +mod roster; +mod selection; +mod world_probes; + +pub use analysis::*; +pub use roster::*; +pub use selection::*; +pub use world_probes::*; diff --git a/crates/rrt-runtime/src/inspect/smp/world/model/roster.rs b/crates/rrt-runtime/src/inspect/smp/world/model/roster.rs new file mode 100644 index 0000000..2ba3dc6 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/model/roster.rs @@ -0,0 +1,84 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::state::{ + RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeTrackPieceCounts, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedCompanyRosterEntry { + pub company_id: u32, + pub active: bool, + #[serde(default)] + pub controller_kind: RuntimeCompanyControllerKind, + pub current_cash: i64, + pub debt: u64, + #[serde(default)] + pub credit_rating_score: Option, + #[serde(default)] + pub prime_rate: Option, + #[serde(default)] + pub available_track_laying_capacity: Option, + #[serde(default)] + pub track_piece_counts: RuntimeTrackPieceCounts, + #[serde(default)] + pub linked_chairman_profile_id: Option, + #[serde(default)] + pub book_value_per_share: i64, + #[serde(default)] + pub investor_confidence: i64, + #[serde(default)] + pub management_attitude: i64, + #[serde(default)] + pub takeover_cooldown_year: Option, + #[serde(default)] + pub merger_cooldown_year: Option, + #[serde(default)] + pub preferred_locomotive_engine_type_raw_u8: Option, + #[serde(default)] + pub market_state: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedCompanyRoster { + pub source_kind: String, + pub semantic_family: String, + pub observed_entry_count: usize, + #[serde(default)] + pub selected_company_id: Option, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedChairmanProfileEntry { + pub profile_id: u32, + pub name: String, + pub active: bool, + #[serde(default)] + pub current_cash: i64, + #[serde(default)] + pub linked_company_id: Option, + #[serde(default)] + pub company_holdings: BTreeMap, + #[serde(default)] + pub holdings_value_total: i64, + #[serde(default)] + pub net_worth_total: i64, + #[serde(default)] + pub purchasing_power_total: i64, + #[serde(default)] + pub personality_byte_0x291: Option, + #[serde(default)] + pub issue_opinion_terms_raw_i32: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedChairmanProfileTable { + pub source_kind: String, + pub semantic_family: String, + pub observed_entry_count: usize, + #[serde(default)] + pub selected_chairman_profile_id: Option, + pub entries: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/model/selection.rs b/crates/rrt-runtime/src/inspect/smp/world/model/selection.rs new file mode 100644 index 0000000..cb3bc54 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/model/selection.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveWorldSelectionContextProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub chunk_tag_offset: usize, + pub payload_offset: usize, + pub payload_len: usize, + pub payload_len_hex: String, + pub selected_company_id_offset: usize, + pub selected_company_id: u32, + pub selected_company_id_hex: String, + pub selected_chairman_profile_id_offset: usize, + pub selected_chairman_profile_id: u32, + pub selected_chairman_profile_id_hex: String, + pub chairman_slot_selector_offset: usize, + pub chairman_slot_selectors: Vec, + pub campaign_override_flag_offset: usize, + pub campaign_override_flag: u8, + pub campaign_override_flag_hex: String, + pub chairman_role_gate_offset: usize, + pub chairman_role_gate_bytes: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveWorldSelectionRoleAnalysisEntry { + pub slot_index: usize, + pub selector_byte: u8, + pub selector_byte_hex: String, + pub role_gate_byte: u8, + pub role_gate_byte_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveWorldSelectionRoleAnalysis { + pub selected_company_id: u32, + pub selected_chairman_profile_id: u32, + pub campaign_override_flag: u8, + pub campaign_override_flag_hex: String, + pub chairman_slots: Vec, +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/model/world_probes.rs b/crates/rrt-runtime/src/inspect/smp/world/model/world_probes.rs new file mode 100644 index 0000000..e5a0488 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/model/world_probes.rs @@ -0,0 +1,193 @@ +use serde::{Deserialize, Serialize}; + +use crate::inspect::smp::common::SmpSaveDwordCandidate; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveWorldEconomicTuningProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub chunk_tag_offset: usize, + pub payload_offset: usize, + pub payload_len: usize, + pub payload_len_hex: String, + pub mirror_lane: SmpSaveDwordCandidate, + pub tuning_lanes: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveWorldIssue37Probe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub chunk_tag_offset: usize, + pub payload_offset: usize, + pub payload_len: usize, + pub payload_len_hex: String, + pub issue_37_raw_u8: u8, + pub issue_37_raw_hex: String, + pub issue_38_raw_u8: u8, + pub issue_38_raw_hex: String, + pub issue_39_raw_u8: u8, + pub issue_39_raw_hex: String, + pub issue_3a_raw_u8: u8, + pub issue_3a_raw_hex: String, + pub issue_value_lane: SmpSaveDwordCandidate, + pub multiplier_lane: SmpSaveDwordCandidate, + #[serde(default)] + pub issue_opinion_base_terms_raw_i32: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveWorldFinanceNeighborhoodProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub chunk_tag_offset: usize, + pub payload_offset: usize, + pub payload_len: usize, + pub payload_len_hex: String, + pub packed_year_word_raw_u16: u16, + pub packed_year_word_raw_hex: String, + pub partial_year_progress_raw_u8: u8, + pub partial_year_progress_raw_hex: String, + pub current_calendar_tuple_word_lane: SmpSaveDwordCandidate, + pub current_calendar_tuple_word_2_lane: SmpSaveDwordCandidate, + pub absolute_counter_lane: SmpSaveDwordCandidate, + pub absolute_counter_mirror_lane: SmpSaveDwordCandidate, + pub stock_policy_raw_u8: u8, + pub stock_policy_raw_hex: String, + pub bond_policy_raw_u8: u8, + pub bond_policy_raw_hex: String, + pub bankruptcy_policy_raw_u8: u8, + pub bankruptcy_policy_raw_hex: String, + pub dividend_policy_raw_u8: u8, + pub dividend_policy_raw_hex: String, + pub building_density_growth_setting_lane: SmpSaveDwordCandidate, + pub dword_candidates: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedWorldIssue37State { + pub source_kind: String, + pub semantic_family: String, + pub issue_value: u32, + pub issue_value_hex: String, + pub issue_38_value: u32, + pub issue_38_value_hex: String, + pub issue_39_value: u32, + pub issue_39_value_hex: String, + pub issue_3a_value: u32, + pub issue_3a_value_hex: String, + pub multiplier_raw_u32: u32, + pub multiplier_raw_hex: String, + pub multiplier_value_f32_text: String, + #[serde(default)] + pub issue_opinion_base_terms_raw_i32: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedWorldEconomicTuningState { + pub source_kind: String, + pub semantic_family: String, + pub mirror_raw_u32: u32, + pub mirror_raw_hex: String, + pub mirror_value_f32_text: String, + pub lane_raw_u32: Vec, + pub lane_raw_hex: Vec, + pub lane_value_f32_text: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedWorldFinanceNeighborhoodState { + pub source_kind: String, + pub semantic_family: String, + pub packed_year_word_raw_u16: u16, + pub packed_year_word_raw_hex: String, + pub partial_year_progress_raw_u8: u8, + pub partial_year_progress_raw_hex: String, + pub current_calendar_tuple_word_raw_u32: u32, + pub current_calendar_tuple_word_raw_hex: String, + pub current_calendar_tuple_word_2_raw_u32: u32, + pub current_calendar_tuple_word_2_raw_hex: String, + pub absolute_counter_raw_u32: u32, + pub absolute_counter_raw_hex: String, + pub absolute_counter_mirror_raw_u32: u32, + pub absolute_counter_mirror_raw_hex: String, + pub stock_policy_raw_u8: u8, + pub stock_policy_raw_hex: String, + pub bond_policy_raw_u8: u8, + pub bond_policy_raw_hex: String, + pub bankruptcy_policy_raw_u8: u8, + pub bankruptcy_policy_raw_hex: String, + pub dividend_policy_raw_u8: u8, + pub dividend_policy_raw_hex: String, + pub building_density_growth_setting_raw_u32: u32, + pub building_density_growth_setting_raw_hex: String, + pub labels: Vec, + pub relative_offsets: Vec, + pub relative_offset_hex: Vec, + pub raw_u32: Vec, + pub raw_hex: Vec, + pub value_i32: Vec, + pub value_f32_text: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedWorldLocomotivePolicyState { + pub source_kind: String, + pub semantic_family: String, + #[serde(default)] + pub selected_year_gap_scalar_raw_u32: Option, + #[serde(default)] + pub selected_year_gap_scalar_raw_hex: Option, + #[serde(default)] + pub selected_year_gap_scalar_value_f32_text: Option, + #[serde(default)] + pub linked_site_removal_follow_on_gate_raw_u8: Option, + #[serde(default)] + pub linked_site_removal_follow_on_gate_raw_hex: Option, + #[serde(default)] + pub auto_show_grade_during_track_lay_raw_u8: Option, + #[serde(default)] + pub auto_show_grade_during_track_lay_raw_hex: Option, + #[serde(default)] + pub starting_building_density_level_raw_u8: Option, + #[serde(default)] + pub starting_building_density_level_raw_hex: Option, + #[serde(default)] + pub building_density_growth_raw_u8: Option, + #[serde(default)] + pub building_density_growth_raw_hex: Option, + #[serde(default)] + pub leftover_simulation_time_accumulator_raw_u32: Option, + #[serde(default)] + pub leftover_simulation_time_accumulator_raw_hex: Option, + #[serde(default)] + pub leftover_simulation_time_accumulator_value_f32_text: Option, + #[serde(default)] + pub selected_year_lane_snapshot_raw_u8: Option, + #[serde(default)] + pub selected_year_lane_snapshot_raw_hex: Option, + #[serde(default)] + pub all_steam_locomotives_available_raw_u8: Option, + #[serde(default)] + pub all_steam_locomotives_available_raw_hex: Option, + #[serde(default)] + pub all_diesel_locomotives_available_raw_u8: Option, + #[serde(default)] + pub all_diesel_locomotives_available_raw_hex: Option, + #[serde(default)] + pub all_electric_locomotives_available_raw_u8: Option, + #[serde(default)] + pub all_electric_locomotives_available_raw_hex: Option, + #[serde(default)] + pub cached_available_locomotive_rating_raw_u32: Option, + #[serde(default)] + pub cached_available_locomotive_rating_raw_hex: Option, + #[serde(default)] + pub cached_available_locomotive_rating_value_f32_text: Option, +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/notes.rs b/crates/rrt-runtime/src/inspect/smp/world/notes.rs new file mode 100644 index 0000000..0e0b87f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/notes.rs @@ -0,0 +1,198 @@ +use crate::inspect::smp::bundle::SmpInspectionReport; +use crate::inspect::smp::common::{ + SmpSaveTrainCollectionDirectoryProbe, SmpSaveUnclassifiedTaggedCollectionHeaderProbe, +}; +use crate::inspect::smp::regions::{ + SmpSaveRegionFixedRowRunCandidateProbe, SmpSaveRegionQueuedNoticeRecordProbe, + SmpSaveRegionRecordTripletProbe, +}; +use crate::inspect::smp::structures::{ + SmpSavePlacedStructureDynamicSideBufferAlignmentProbe, + SmpSavePlacedStructureDynamicSideBufferProbe, SmpSavePlacedStructureRecordTripletProbe, +}; +use crate::inspect::smp::world::*; + +pub(in crate::inspect::smp) struct SaveCompanyChairmanAnalysisNotesInputs<'a> { + pub(super) report: &'a SmpInspectionReport, + pub(super) world_selection_context: Option<&'a SmpSaveWorldSelectionRoleAnalysis>, + pub(super) train_collection_directory: Option<&'a SmpSaveTrainCollectionDirectoryProbe>, + pub(super) region_record_triplets: Option<&'a SmpSaveRegionRecordTripletProbe>, + pub(super) region_queued_notice_records: Option<&'a SmpSaveRegionQueuedNoticeRecordProbe>, + pub(super) region_fixed_row_run_candidates: Option<&'a SmpSaveRegionFixedRowRunCandidateProbe>, + pub(super) placed_structure_record_triplets: + Option<&'a SmpSavePlacedStructureRecordTripletProbe>, + pub(super) placed_structure_dynamic_side_buffer: + Option<&'a SmpSavePlacedStructureDynamicSideBufferProbe>, + pub(super) placed_structure_dynamic_side_buffer_alignment: + Option<&'a SmpSavePlacedStructureDynamicSideBufferAlignmentProbe>, + pub(super) unclassified_tagged_collection_headers: + &'a [SmpSaveUnclassifiedTaggedCollectionHeaderProbe], + pub(super) company_entries: &'a [SmpSaveCompanyRecordAnalysisEntry], + pub(super) chairman_entries: &'a [SmpSaveChairmanRecordAnalysisEntry], +} + +pub(in crate::inspect::smp) fn build_save_company_chairman_analysis_notes( + inputs: SaveCompanyChairmanAnalysisNotesInputs<'_>, +) -> Vec { + let mut notes = Vec::new(); + if inputs.world_selection_context.is_some() { + notes.push( + "World selection context now exports the grounded chairman-slot selector bytes and per-slot role-gate bytes from the fixed save-side 0x32c8 world block.".to_string(), + ); + } + if inputs.report.save_world_issue_37_probe.is_some() { + notes.push( + "World analysis now also exports the grounded issue-0x37 pair from the same 0x32c8 world payload: the clamped small issue value at [world+0x2d] and its companion multiplier lane at [world+0x29].".to_string(), + ); + } + if inputs.report.save_world_economic_tuning_probe.is_some() { + notes.push( + "World analysis now also exports the fixed six-lane economic tuning float block from the same 0x32c8 world payload; current atlas evidence still treats that band as distinct from the issue-0x37 investor-confidence family.".to_string(), + ); + } + if inputs + .report + .save_world_finance_neighborhood_probe + .is_some() + { + notes.push( + "World analysis now also exports one fixed dword finance neighborhood around the grounded issue/calendar lanes, so future issue-0x38/0x39 closure can build on rehosted owner-state candidates instead of ad hoc byte guesses.".to_string(), + ); + } + if let Some(header) = inputs.report.save_train_collection_header_probe.as_ref() { + notes.push(format!( + "Train analysis now also exports the tagged train collection header: live_record_count={} live_id_bound={} direct_record_stride=0x{:x}.", + header.live_record_count, header.live_id_bound, header.direct_record_stride + )); + } + if let Some(directory) = inputs.train_collection_directory { + notes.push(format!( + "Train analysis now also exports the tagged live-entry directory rooted at metadata dword {}: {} entries chained from {:?} to {:?}.", + directory.directory_root_dword_index, + directory.entries.len(), + directory.chain_head_live_entry_id, + directory.chain_tail_live_entry_id + )); + } + if let Some(header) = inputs.report.save_region_collection_header_probe.as_ref() { + notes.push(format!( + "Region analysis now also exports the non-direct tagged region collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.", + header.live_record_count, header.live_id_bound, header.direct_record_stride + )); + } + if let Some(triplets) = inputs.region_record_triplets { + notes.push(format!( + "Region analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first serialized region name={:?}, first policy lanes=({:.3}, {:.3}, {:.3}), first profile collection count={:?}, first profile collection trailing_padding_len={:?}.", + triplets.record_count, + triplets.entries.first().map(|entry| entry.name.as_str()), + triplets.entries.first().map(|entry| entry.policy_leading_f32_0).unwrap_or_default(), + triplets.entries.first().map(|entry| entry.policy_leading_f32_1).unwrap_or_default(), + triplets.entries.first().map(|entry| entry.policy_leading_f32_2).unwrap_or_default(), + triplets.entries.first().and_then(|entry| entry.profile_collection.as_ref().map(|collection| collection.live_record_count)), + triplets.entries.first().and_then(|entry| entry.profile_collection.as_ref().map(|collection| collection.trailing_padding_len)) + )); + } + if let Some(queue_probe) = inputs.region_queued_notice_records { + notes.push(format!( + "Region analysis now also exports {} queued kind-7 notice nodes with payload seed {}: first region id={} amount={} promotion={} tails={}/{}.", + queue_probe.entries.len(), + queue_probe.payload_seed_dword_hex, + queue_probe.entries[0].region_id, + queue_probe.entries[0].amount, + queue_probe.entries[0].promotion_latch_dword_hex, + queue_probe.entries[0].trailing_sentinel_i32_0_hex, + queue_probe.entries[0].trailing_sentinel_i32_1_hex + )); + } + if let Some(fixed_row_candidates) = inputs.region_fixed_row_run_candidates { + notes.push(format!( + "Region analysis now also exports {} fixed-row run candidates keyed to live_record_count={} and stride {} before the tagged region metadata; best candidate rows offset is {:?} with shape signature {:?}.", + fixed_row_candidates.candidates.len(), + fixed_row_candidates.target_row_count, + fixed_row_candidates.target_row_stride_hex, + fixed_row_candidates.candidates.first().map(|candidate| candidate.rows_offset_hex.as_str()), + fixed_row_candidates.candidates.first().map(|candidate| candidate.shape_signature.as_str()) + )); + } + if let Some(header) = inputs + .report + .save_placed_structure_collection_header_probe + .as_ref() + { + notes.push(format!( + "Placed-structure analysis now also exports the tagged collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.", + header.live_record_count, header.live_id_bound, header.direct_record_stride + )); + } + if let Some(triplets) = inputs.placed_structure_record_triplets { + notes.push(format!( + "Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}, first footer payload={}, first footer status kind={:?}.", + triplets.record_count, + triplets.entries.first().map(|entry| entry.primary_name.as_str()), + triplets.entries.first().map(|entry| entry.secondary_name.as_str()), + triplets.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000"), + triplets.entries.first().map(|entry| entry.profile_status_kind.as_str()) + )); + } + if let Some(side_buffer) = inputs.placed_structure_dynamic_side_buffer { + let dominant_pattern = side_buffer.compact_prefix_pattern_summaries.first(); + let payload_envelope_summary = side_buffer.payload_envelope_summary.as_ref(); + let short_profile_flag_pair_summary = payload_envelope_summary + .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()); + notes.push(format!( + "Placed-structure analysis now also exports the separate 0x38a5 dynamic side-buffer owner seam with {} embedded name rows, {} decoded rows across {} unique name pairs, {} rows with a tertiary 0x55f1 string, {} unique compact prefix patterns, {} rows whose leading dword matches 0x55f3, {} complete 0x55f1/0x55f2/0x55f3 envelopes, dominant 0x55f2 chunk len=0x{:x} x{}, dominant 0x55f3 span=0x{:x} x{}, dominant short 0x55f3 flag pair={}/{} x{}, and dominant compact pattern={}/{}/{} x{}.", + side_buffer.embedded_name_tag_count, + side_buffer.decoded_embedded_name_row_count, + side_buffer.unique_embedded_name_pair_count, + side_buffer.decoded_embedded_name_row_with_tertiary_name_count, + side_buffer.unique_compact_prefix_pattern_count, + side_buffer.prefix_leading_dword_matching_embedded_profile_tag_count, + payload_envelope_summary.map(|summary| summary.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope).unwrap_or_default(), + payload_envelope_summary.and_then(|summary| summary.dominant_policy_chunk_len).unwrap_or_default(), + payload_envelope_summary.map(|summary| summary.dominant_policy_chunk_len_count).unwrap_or_default(), + payload_envelope_summary.and_then(|summary| summary.dominant_profile_chunk_len).unwrap_or_default(), + payload_envelope_summary.map(|summary| summary.dominant_profile_chunk_len_count).unwrap_or_default(), + short_profile_flag_pair_summary.and_then(|summary| summary.dominant_flag_pair.as_ref()).map(|pair| pair.first_flag_byte_hex.as_str()).unwrap_or("0x00"), + short_profile_flag_pair_summary.and_then(|summary| summary.dominant_flag_pair.as_ref()).map(|pair| pair.second_flag_byte_hex.as_str()).unwrap_or("0x00"), + short_profile_flag_pair_summary.and_then(|summary| summary.dominant_flag_pair.as_ref()).map(|pair| pair.count).unwrap_or_default(), + dominant_pattern.map(|pattern| pattern.prefix_leading_dword_hex.as_str()).unwrap_or("0x00000000"), + dominant_pattern.map(|pattern| pattern.prefix_trailing_word_hex.as_str()).unwrap_or("0x0000"), + dominant_pattern.map(|pattern| pattern.prefix_separator_byte_hex.as_str()).unwrap_or("0x00"), + dominant_pattern.map(|pattern| pattern.count).unwrap_or_default() + )); + } + if let Some(alignment) = inputs.placed_structure_dynamic_side_buffer_alignment { + notes.push(format!( + "Placed-structure analysis now also compares the 0x38a5 side-buffer against the grounded 0x36b1 triplet corpus: {} of {} decoded side-buffer rows reuse {} overlapping placed-structure name pairs, leaving {} unmatched side-buffer rows and {} triplet-only name pairs.", + alignment.side_buffer_rows_with_matching_triplet_name_pair_count, + alignment.side_buffer_row_count, + alignment.overlapping_name_pair_count, + alignment.side_buffer_rows_without_matching_triplet_name_pair_count, + alignment.triplet_name_pairs_without_side_buffer_match_count + )); + } + if let Some(candidate) = inputs.unclassified_tagged_collection_headers.first() { + notes.push(format!( + "Generic save-side tagged collection scan also found {} unclassified candidate families; largest current candidate uses tags {}/{}/{} with live_record_count={} stride=0x{:x} records_span_len=0x{:x}.", + inputs.unclassified_tagged_collection_headers.len(), + candidate.metadata_tag_hex, + candidate.records_tag_hex, + candidate.close_tag_hex, + candidate.live_record_count, + candidate.direct_record_stride, + candidate.records_span_len + )); + } + if !inputs.company_entries.is_empty() { + notes.push("Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string()); + notes.push("Company available_track_laying_capacity is derived from the grounded tail dword [company+0x7680], with negative values treated as the unlimited sentinel.".to_string()); + notes.push("Company scalar_dword_candidates expose the current checked-in raw save windows around support/share-price/calendar lanes, and post_capacity_dword_candidates expose the immediate dwords after [company+0x7680] for deeper track-count and record-tail analysis.".to_string()); + notes.push("Company stat-band root candidates now also expose the first dword windows rooted at [company+0x0cfb], [company+0x0d7f], and [company+0x1c47], the same broader stat bands the grounded cheat reset branch clears before later finance/detail readers rebuild them.".to_string()); + notes.push("Current atlas evidence ties company current_cash and book_value_per_share to stat-family 0x2329 slots 0x0d and 0x1d, so the remaining save-native company finance/governance closure likely needs a structured company-stat family reconstruction instead of more isolated raw offsets.".to_string()); + } + if !inputs.chairman_entries.is_empty() { + notes.push("Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9], now including raw qword hex and signed/f64 views for further purchasing-power analysis.".to_string()); + notes.push("Chairman analysis now also derives one holdings-at-cached-share-price total from the grounded company cached_share_price lane and one strongest-cached purchasing-power total from the nonnegative qword cache band.".to_string()); + } + notes +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/selection.rs b/crates/rrt-runtime/src/inspect/smp/world/selection.rs new file mode 100644 index 0000000..925346b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/selection.rs @@ -0,0 +1,61 @@ +use crate::inspect::smp::common::SmpSaveTaggedCollectionHeaderProbe; +use crate::inspect::smp::world::*; + +pub(in crate::inspect::smp) fn derive_selection_only_company_roster_from_save_world_probe( + probe: &SmpSaveWorldSelectionContextProbe, + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + Some(SmpLoadedCompanyRoster { + source_kind: format!("{}-company-selection-only", probe.source_kind), + semantic_family: "scenario-selected-company-context".to_string(), + observed_entry_count: header_probe + .map(|probe| probe.live_record_count as usize) + .unwrap_or(0), + selected_company_id: Some(probe.selected_company_id), + entries: Vec::new(), + }) +} + +pub(in crate::inspect::smp) fn derive_selection_only_chairman_profile_table_from_save_world_probe( + probe: &SmpSaveWorldSelectionContextProbe, + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + Some(SmpLoadedChairmanProfileTable { + source_kind: format!("{}-chairman-selection-only", probe.source_kind), + semantic_family: "scenario-selected-chairman-context".to_string(), + observed_entry_count: header_probe + .map(|probe| probe.live_record_count as usize) + .unwrap_or(0), + selected_chairman_profile_id: Some(probe.selected_chairman_profile_id), + entries: Vec::new(), + }) +} + +pub(in crate::inspect::smp) fn build_save_world_selection_role_analysis( + probe: &SmpSaveWorldSelectionContextProbe, +) -> SmpSaveWorldSelectionRoleAnalysis { + let chairman_slots = probe + .chairman_slot_selectors + .iter() + .copied() + .zip(probe.chairman_role_gate_bytes.iter().copied()) + .enumerate() + .map(|(slot_index, (selector_byte, role_gate_byte))| { + SmpSaveWorldSelectionRoleAnalysisEntry { + slot_index, + selector_byte, + selector_byte_hex: format!("0x{selector_byte:02x}"), + role_gate_byte, + role_gate_byte_hex: format!("0x{role_gate_byte:02x}"), + } + }) + .collect(); + + SmpSaveWorldSelectionRoleAnalysis { + selected_company_id: probe.selected_company_id, + selected_chairman_profile_id: probe.selected_chairman_profile_id, + campaign_override_flag: probe.campaign_override_flag, + campaign_override_flag_hex: probe.campaign_override_flag_hex.clone(), + chairman_slots, + } +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/world_probes/economic_tuning.rs b/crates/rrt-runtime/src/inspect/smp/world/world_probes/economic_tuning.rs new file mode 100644 index 0000000..c8ee704 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/world_probes/economic_tuning.rs @@ -0,0 +1,90 @@ +use crate::inspect::smp::bundle::SmpContainerProfile; +use crate::inspect::smp::catalog::offsets::world::{ + RT3_SAVE_WORLD_BLOCK_CHUNK_TAG, RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS, RT3_SAVE_WORLD_BLOCK_LEN, + RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG, +}; +use crate::inspect::smp::common::{find_u32_le_offsets, read_u32_at}; +use crate::inspect::smp::world::{SmpSaveWorldEconomicTuningProbe, build_save_dword_candidate}; + +pub(in crate::inspect::smp) fn parse_save_world_economic_tuning_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + if file_extension_hint != Some("gms") { + return None; + } + let profile = container_profile?; + let supported = matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ); + if !supported { + return None; + } + + for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) { + let payload_offset = chunk_tag_offset + 4; + let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?; + if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) { + continue; + } + let mirror_lane = build_save_dword_candidate( + bytes, + payload_offset, + "economic_tuning_mirror_lane_0", + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET, + )?; + let tuning_lanes = RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS + .iter() + .enumerate() + .map(|(lane_index, relative_offset)| { + build_save_dword_candidate( + bytes, + payload_offset, + &format!("economic_tuning_lane_{lane_index}"), + *relative_offset, + ) + }) + .collect::>>()?; + return Some(SmpSaveWorldEconomicTuningProbe { + profile_family: profile.profile_family.clone(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-save-world-economic-tuning".to_string(), + chunk_tag_offset, + payload_offset, + payload_len: RT3_SAVE_WORLD_BLOCK_LEN, + payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), + mirror_lane, + tuning_lanes, + evidence: vec![ + format!( + "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" + ), + format!( + "next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span" + ), + format!( + "mirror lane uses payload +0x{:x} ([world+0x0bde])", + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET + ), + format!( + "primary tuning lanes use payload offsets {} matching the documented [world+0x0be2..+0x0bf6] float block", + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS + .iter() + .map(|offset| format!("0x{offset:x}")) + .collect::>() + .join(", ") + ), + "Current atlas evidence keeps this fixed six-float world tuning band separate from the issue-0x37 investor-confidence lane." + .to_string(), + ], + }); + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/world_probes/finance_neighborhood.rs b/crates/rrt-runtime/src/inspect/smp/world/world_probes/finance_neighborhood.rs new file mode 100644 index 0000000..f33e04f --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/world_probes/finance_neighborhood.rs @@ -0,0 +1,180 @@ +use crate::inspect::smp::bundle::SmpContainerProfile; +use crate::inspect::smp::catalog::offsets::world::{ + RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG, + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_CANDIDATE_FIELDS, + RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS, RT3_SAVE_WORLD_BLOCK_LEN, + RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG, RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET, +}; +use crate::inspect::smp::common::SmpSaveDwordCandidate; +use crate::inspect::smp::common::{find_u32_le_offsets, read_u8_at, read_u16_at, read_u32_at}; +use crate::inspect::smp::world::{ + SmpSaveWorldFinanceNeighborhoodProbe, build_save_dword_candidate, +}; + +pub(in crate::inspect::smp) fn parse_save_world_finance_neighborhood_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + if file_extension_hint != Some("gms") { + return None; + } + let profile = container_profile?; + let supported = matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ); + if !supported { + return None; + } + + for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) { + let payload_offset = chunk_tag_offset + 4; + let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?; + if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) { + continue; + } + + let current_calendar_tuple_word_lane = build_save_dword_candidate( + bytes, + payload_offset, + "current_calendar_tuple_word", + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, + )?; + let packed_year_word_raw_u16 = read_u16_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, + )?; + let partial_year_progress_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET + 2, + )?; + let current_calendar_tuple_word_2_lane = build_save_dword_candidate( + bytes, + payload_offset, + "current_calendar_tuple_word_2", + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET, + )?; + let absolute_counter_lane = build_save_dword_candidate( + bytes, + payload_offset, + "absolute_calendar_counter", + RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET, + )?; + let absolute_counter_mirror_lane = build_save_dword_candidate( + bytes, + payload_offset, + "absolute_calendar_counter_mirror", + RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET, + )?; + let stock_policy_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET, + )?; + let bond_policy_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET, + )?; + let bankruptcy_policy_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, + )?; + let dividend_policy_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET, + )?; + let building_density_growth_setting_lane = build_save_dword_candidate( + bytes, + payload_offset, + "building_density_growth_setting", + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET, + )?; + let dword_candidates = + build_save_world_finance_neighborhood_candidates(bytes, payload_offset)?; + + return Some(SmpSaveWorldFinanceNeighborhoodProbe { + profile_family: profile.profile_family.clone(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-save-world-finance-neighborhood".to_string(), + chunk_tag_offset, + payload_offset, + payload_len: RT3_SAVE_WORLD_BLOCK_LEN, + payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), + packed_year_word_raw_u16, + packed_year_word_raw_hex: format!("0x{packed_year_word_raw_u16:04x}"), + partial_year_progress_raw_u8, + partial_year_progress_raw_hex: format!("0x{partial_year_progress_raw_u8:02x}"), + current_calendar_tuple_word_lane, + current_calendar_tuple_word_2_lane, + absolute_counter_lane, + absolute_counter_mirror_lane, + stock_policy_raw_u8, + stock_policy_raw_hex: format!("0x{stock_policy_raw_u8:02x}"), + bond_policy_raw_u8, + bond_policy_raw_hex: format!("0x{bond_policy_raw_u8:02x}"), + bankruptcy_policy_raw_u8, + bankruptcy_policy_raw_hex: format!("0x{bankruptcy_policy_raw_u8:02x}"), + dividend_policy_raw_u8, + dividend_policy_raw_hex: format!("0x{dividend_policy_raw_u8:02x}"), + building_density_growth_setting_lane, + dword_candidates, + evidence: vec![ + format!( + "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" + ), + format!( + "next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span" + ), + format!( + "payload +0x{:x}/+0x{:x}/+0x{:x} carry the saved world calendar tuple and absolute counter lanes that later company stock-issue cooldown readers compare against", + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET + ), + format!( + "payload +0x{:x}/+0x{:x}/+0x{:x}/+0x{:x} carry the stock, bond, bankruptcy, and dividend finance-policy bytes mirrored from scenario offsets 0x4a87/0x4a8b/0x4a8f/0x4a93", + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET + ), + format!( + "payload +0x{:x} carries the fixed-world building-density growth setting mirrored from `[world+0x4c7c]`, which the annual repurchase and dividend policy helpers both read directly", + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET + ), + "finance-neighborhood candidates cover the fixed dword strip around the grounded world calendar tuple, absolute-counter, selection-context, and issue-0x37 lanes so broader finance reader closure can build on one rehosted owner surface.".to_string(), + ], + }); + } + + None +} + +pub(in crate::inspect::smp) fn build_save_world_finance_neighborhood_candidates( + bytes: &[u8], + payload_offset: usize, +) -> Option> { + (0..RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS) + .map(|index| { + let relative_offset = + RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET + index * 4; + let label = RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_CANDIDATE_FIELDS + .iter() + .find(|(_, named_offset)| *named_offset == relative_offset) + .map(|(name, _)| (*name).to_string()) + .unwrap_or_else(|| format!("finance_neighborhood_word_{:02}", index + 1)); + build_save_dword_candidate(bytes, payload_offset, &label, relative_offset) + }) + .collect() +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/world_probes/issue_37.rs b/crates/rrt-runtime/src/inspect/smp/world/world_probes/issue_37.rs new file mode 100644 index 0000000..9ee390c --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/world_probes/issue_37.rs @@ -0,0 +1,122 @@ +use crate::inspect::smp::bundle::SmpContainerProfile; +use crate::inspect::smp::catalog::offsets::world::{ + RT3_SAVE_WORLD_BLOCK_CHUNK_TAG, RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_BASE_TERMS_OFFSET, + RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT, RT3_SAVE_WORLD_BLOCK_LEN, + RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG, +}; +use crate::inspect::smp::common::{find_u32_le_offsets, read_u8_at, read_u32_at}; +use crate::inspect::smp::world::{ + SmpSaveWorldIssue37Probe, build_save_dword_candidate, build_save_i32_term_strip, +}; + +pub(in crate::inspect::smp) fn parse_save_world_issue_37_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + if file_extension_hint != Some("gms") { + return None; + } + let profile = container_profile?; + let supported = matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ); + if !supported { + return None; + } + + for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) { + let payload_offset = chunk_tag_offset + 4; + let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?; + if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) { + continue; + } + let issue_value_lane = build_save_dword_candidate( + bytes, + payload_offset, + "issue_0x37_value", + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET, + )?; + let issue_37_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET, + )?; + let issue_38_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 1, + )?; + let issue_39_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 2, + )?; + let issue_3a_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 3, + )?; + let multiplier_lane = build_save_dword_candidate( + bytes, + payload_offset, + "issue_0x37_multiplier", + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET, + )?; + let issue_opinion_base_terms_raw_i32 = build_save_i32_term_strip( + bytes, + payload_offset, + RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_BASE_TERMS_OFFSET, + RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT, + )?; + return Some(SmpSaveWorldIssue37Probe { + profile_family: profile.profile_family.clone(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-save-world-issue-0x37".to_string(), + chunk_tag_offset, + payload_offset, + payload_len: RT3_SAVE_WORLD_BLOCK_LEN, + payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), + issue_37_raw_u8, + issue_37_raw_hex: format!("0x{issue_37_raw_u8:02x}"), + issue_38_raw_u8, + issue_38_raw_hex: format!("0x{issue_38_raw_u8:02x}"), + issue_39_raw_u8, + issue_39_raw_hex: format!("0x{issue_39_raw_u8:02x}"), + issue_3a_raw_u8, + issue_3a_raw_hex: format!("0x{issue_3a_raw_u8:02x}"), + issue_value_lane, + multiplier_lane, + issue_opinion_base_terms_raw_i32, + evidence: vec![ + format!( + "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" + ), + format!( + "next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span" + ), + format!( + "issue value lane uses payload +0x{:x} ([world+0x2d]); atlas notes tie 0x004339b0 to the clamped 0..4 issue-0x37 setter there", + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + ), + format!( + "multiplier lane uses payload +0x{:x} ([world+0x29]); atlas notes tie 0x004339b0 to one companion scalar at that lane before company share-price refresh", + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET + ), + format!( + "the adjacent byte strip at payload +0x{:x}..+0x{:x} carries raw issue slots 0x37..0x3a as {:02x} {:02x} {:02x} {:02x}", + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 3, + issue_37_raw_u8, + issue_38_raw_u8, + issue_39_raw_u8, + issue_3a_raw_u8 + ), + ], + }); + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/smp/world/world_probes/mod.rs b/crates/rrt-runtime/src/inspect/smp/world/world_probes/mod.rs new file mode 100644 index 0000000..a8d194b --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/world_probes/mod.rs @@ -0,0 +1,9 @@ +mod economic_tuning; +mod finance_neighborhood; +mod issue_37; +mod selection_context; + +pub(in crate::inspect::smp) use economic_tuning::parse_save_world_economic_tuning_probe; +pub(in crate::inspect::smp) use finance_neighborhood::parse_save_world_finance_neighborhood_probe; +pub(in crate::inspect::smp) use issue_37::parse_save_world_issue_37_probe; +pub(in crate::inspect::smp) use selection_context::parse_save_world_selection_context_probe; diff --git a/crates/rrt-runtime/src/inspect/smp/world/world_probes/selection_context.rs b/crates/rrt-runtime/src/inspect/smp/world/world_probes/selection_context.rs new file mode 100644 index 0000000..1306f14 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/smp/world/world_probes/selection_context.rs @@ -0,0 +1,123 @@ +use crate::inspect::smp::bundle::SmpContainerProfile; +use crate::inspect::smp::catalog::offsets::world::{ + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE, RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT, + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG, + RT3_SAVE_WORLD_BLOCK_LEN, RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG, + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET, +}; +use crate::inspect::smp::common::{find_u32_le_offsets, read_u32_at}; +use crate::inspect::smp::world::SmpSaveWorldSelectionContextProbe; + +pub(in crate::inspect::smp) fn parse_save_world_selection_context_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + if file_extension_hint != Some("gms") { + return None; + } + let profile = container_profile?; + let supported = matches!( + profile.profile_family.as_str(), + "rt3-classic-save-container-v1" + | "rt3-105-save-container-v1" + | "rt3-105-scenario-save-container-v1" + | "rt3-105-alt-save-container-v1" + ); + if !supported { + return None; + } + + for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) { + let payload_offset = chunk_tag_offset + 4; + let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?; + if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) { + continue; + } + let selected_company_id_offset = + payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET; + let selected_chairman_profile_id_offset = + payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET; + let chairman_slot_selector_offset = + payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET; + let campaign_override_flag_offset = + payload_offset + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET; + let chairman_role_gate_offset = + payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET; + let selected_company_id = read_u32_at(bytes, selected_company_id_offset)?; + let selected_chairman_profile_id = read_u32_at(bytes, selected_chairman_profile_id_offset)?; + let chairman_slot_selectors = bytes + .get( + chairman_slot_selector_offset + ..chairman_slot_selector_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT, + )? + .to_vec(); + let campaign_override_flag = *bytes.get(campaign_override_flag_offset)?; + let chairman_role_gate_bytes = (0..RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT) + .map(|slot_index| { + bytes + .get( + chairman_role_gate_offset + + slot_index * RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE, + ) + .copied() + }) + .collect::>>()?; + return Some(SmpSaveWorldSelectionContextProbe { + profile_family: profile.profile_family.clone(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-selected-company-and-chairman-context".to_string(), + chunk_tag_offset, + payload_offset, + payload_len: RT3_SAVE_WORLD_BLOCK_LEN, + payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), + selected_company_id_offset, + selected_company_id, + selected_company_id_hex: format!("0x{selected_company_id:08x}"), + selected_chairman_profile_id_offset, + selected_chairman_profile_id, + selected_chairman_profile_id_hex: format!("0x{selected_chairman_profile_id:08x}"), + chairman_slot_selector_offset, + chairman_slot_selectors, + campaign_override_flag_offset, + campaign_override_flag, + campaign_override_flag_hex: format!("0x{campaign_override_flag:02x}"), + chairman_role_gate_offset, + chairman_role_gate_bytes, + evidence: vec![ + format!( + "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" + ), + format!( + "next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span" + ), + format!( + "selected company id comes from payload +0x{:x} ([world+0x21])", + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET + ), + format!( + "selected chairman profile id comes from payload +0x{:x} ([world+0x25])", + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET + ), + format!( + "16 chairman slot selector bytes come from payload +0x{:x} ([world+0x87])", + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET + ), + format!( + "campaign override flag comes from payload +0x{:x} ([world+0xc5])", + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET + ), + format!( + "chairman role-gate bytes come from payload +0x{:x} + slot*0x{:x} ([world+0x0bc3+slot*9])", + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE + ), + ], + }); + } + + None +} diff --git a/crates/rrt-runtime/src/inspect/win/entrypoints.rs b/crates/rrt-runtime/src/inspect/win/entrypoints.rs new file mode 100644 index 0000000..4a57fa3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/win/entrypoints.rs @@ -0,0 +1,97 @@ +use std::error::Error; +use std::fs; +use std::path::Path; + +use super::header::{WIN_COMMON_HEADER_LEN, analyze_common_header}; +use super::references::{ + build_delta_histogram, build_resource_record_samples, collect_imb_references, + shared_prelude_prefix_hex, +}; +use super::selectors::{build_resource_selector_records, collect_anonymous_selector_records}; +use super::types::WinInspectionReport; + +pub fn inspect_win_file(path: &Path) -> Result> { + let bytes = fs::read(path)?; + inspect_win_bytes(&bytes) +} + +pub fn inspect_win_bytes(bytes: &[u8]) -> Result> { + if bytes.len() < WIN_COMMON_HEADER_LEN { + return Err(format!( + "window resource is too short for the observed common header: {} < 0x{WIN_COMMON_HEADER_LEN:x}", + bytes.len() + ) + .into()); + } + + let header = analyze_common_header(bytes); + let all_imb_references = collect_imb_references(bytes); + let resource_record_samples = build_resource_record_samples(bytes, &all_imb_references); + let resource_selector_records = build_resource_selector_records(&resource_record_samples); + let anonymous_selector_records = collect_anonymous_selector_records(bytes, &all_imb_references); + let common_resource_record_prelude_prefix_words = + shared_prelude_prefix_hex(&resource_record_samples); + let name_len_matches_prelude_word_3_plus_nul_count = resource_record_samples + .iter() + .filter(|sample| { + sample.prelude_words.len() == 4 + && sample.prelude_words[3].value == (sample.name.len() as u32 + 1) + }) + .count(); + let mut unique_imb_references = Vec::new(); + for reference in &all_imb_references { + if !unique_imb_references.contains(&reference.name) { + unique_imb_references.push(reference.name.clone()); + } + } + + let mut notes = Vec::new(); + if header.matches_observed_common_signature { + notes.push( + "Header matches the observed shared .win signature seen in Campaign.win, CompanyDetail.win, and setup.win." + .to_string(), + ); + } else { + notes.push( + "Header diverges from the currently observed shared .win signature; treat field meanings as provisional." + .to_string(), + ); + } + if header.inline_root_resource_name.is_some() { + notes.push( + "The blob carries an inline root .imb resource name immediately after the common 0x50-byte header." + .to_string(), + ); + } else { + notes.push( + "No inline root .imb resource name appears at 0x50; this window likely starts directly with control records." + .to_string(), + ); + } + notes.push( + "Embedded .imb strings are reported as resource references with selector lanes; this inspector still does not decode full control record semantics." + .to_string(), + ); + + Ok(WinInspectionReport { + file_size: bytes.len(), + common_header_len: WIN_COMMON_HEADER_LEN, + common_header_len_hex: format!("0x{WIN_COMMON_HEADER_LEN:02x}"), + shared_header_words: header.shared_header_words, + matches_observed_common_signature: header.matches_observed_common_signature, + common_resource_record_prelude_prefix_words, + name_len_matches_prelude_word_3_plus_nul_count, + inline_root_resource_name: header.inline_root_resource_name, + inline_root_resource_offset: header.inline_root_resource_offset, + inline_root_resource_offset_hex: header.inline_root_resource_offset_hex, + imb_reference_count: all_imb_references.len(), + unique_imb_reference_count: unique_imb_references.len(), + unique_imb_references, + dominant_reference_deltas: build_delta_histogram(&resource_record_samples), + resource_selector_records, + anonymous_selector_records, + first_resource_record_samples: resource_record_samples.into_iter().take(32).collect(), + first_imb_references: all_imb_references.into_iter().take(32).collect(), + notes, + }) +} diff --git a/crates/rrt-runtime/src/inspect/win/header.rs b/crates/rrt-runtime/src/inspect/win/header.rs new file mode 100644 index 0000000..9562639 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/win/header.rs @@ -0,0 +1,59 @@ +use super::names::parse_inline_ascii_name; +use super::read::read_u32_le; +use super::types::WinHeaderWord; + +pub(super) const WIN_COMMON_HEADER_LEN: usize = 0x50; +pub(super) const WIN_INLINE_RESOURCE_OFFSET: usize = 0x50; + +pub(super) struct CommonHeaderAnalysis { + pub shared_header_words: Vec, + pub matches_observed_common_signature: bool, + pub inline_root_resource_name: Option, + pub inline_root_resource_offset: Option, + pub inline_root_resource_offset_hex: Option, +} + +pub(super) fn analyze_common_header(bytes: &[u8]) -> CommonHeaderAnalysis { + let header_offsets = [ + 0x00usize, 0x04, 0x08, 0x0c, 0x10, 0x14, 0x18, 0x1c, 0x20, 0x24, 0x28, 0x2c, 0x30, 0x34, + 0x38, 0x3c, 0x40, 0x44, 0x48, 0x4c, + ]; + let shared_header_words = header_offsets + .iter() + .map(|offset| { + let value = read_u32_le(bytes, *offset).expect("validated common header length"); + WinHeaderWord { + offset: *offset, + offset_hex: format!("0x{offset:02x}"), + value, + value_hex: format!("0x{value:08x}"), + } + }) + .collect::>(); + + let matches_observed_common_signature = read_u32_le(bytes, 0x00) == Some(0x0000_07d0) + && read_u32_le(bytes, 0x04) == Some(0) + && read_u32_le(bytes, 0x08) == Some(0) + && read_u32_le(bytes, 0x0c) == Some(0x8000_0000) + && read_u32_le(bytes, 0x10) == Some(0x8000_003f) + && read_u32_le(bytes, 0x14) == Some(0x0000_003f) + && read_u32_le(bytes, 0x34) == Some(0x0007_d100) + && read_u32_le(bytes, 0x38) == Some(0x0007_d200) + && read_u32_le(bytes, 0x40) == Some(0x000b_b800) + && read_u32_le(bytes, 0x48) == Some(0x000b_b900); + + let inline_root_resource_name = parse_inline_ascii_name(bytes, WIN_INLINE_RESOURCE_OFFSET); + let inline_root_resource_offset = inline_root_resource_name + .as_ref() + .map(|_| WIN_INLINE_RESOURCE_OFFSET + 1); + let inline_root_resource_offset_hex = + inline_root_resource_offset.map(|offset| format!("0x{offset:04x}")); + + CommonHeaderAnalysis { + shared_header_words, + matches_observed_common_signature, + inline_root_resource_name, + inline_root_resource_offset, + inline_root_resource_offset_hex, + } +} diff --git a/crates/rrt-runtime/src/inspect/win/mod.rs b/crates/rrt-runtime/src/inspect/win/mod.rs new file mode 100644 index 0000000..7141c3d --- /dev/null +++ b/crates/rrt-runtime/src/inspect/win/mod.rs @@ -0,0 +1,44 @@ +mod entrypoints; +mod header; +mod names; +mod read; +mod references; +mod selectors; +mod types; + +pub use entrypoints::{inspect_win_bytes, inspect_win_file}; +pub use types::*; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inspects_synthetic_window_blob() { + let mut bytes = vec![0u8; 0x90]; + bytes[0x00..0x04].copy_from_slice(&0x0000_07d0u32.to_le_bytes()); + bytes[0x0c..0x10].copy_from_slice(&0x8000_0000u32.to_le_bytes()); + bytes[0x10..0x14].copy_from_slice(&0x8000_003fu32.to_le_bytes()); + bytes[0x14..0x18].copy_from_slice(&0x0000_003fu32.to_le_bytes()); + bytes[0x34..0x38].copy_from_slice(&0x0007_d100u32.to_le_bytes()); + bytes[0x38..0x3c].copy_from_slice(&0x0007_d200u32.to_le_bytes()); + bytes[0x40..0x44].copy_from_slice(&0x000b_b800u32.to_le_bytes()); + bytes[0x48..0x4c].copy_from_slice(&0x000b_b900u32.to_le_bytes()); + bytes[0x50] = 0; + bytes[0x51..0x51 + "Root.imb".len()].copy_from_slice(b"Root.imb"); + bytes[0x59] = 0; + bytes.extend_from_slice(b"\0Button.imb\0"); + + let report = inspect_win_bytes(&bytes).expect("inspection should succeed"); + assert!(report.matches_observed_common_signature); + assert_eq!( + report.inline_root_resource_name.as_deref(), + Some("Root.imb") + ); + assert_eq!(report.imb_reference_count, 2); + assert_eq!(report.unique_imb_reference_count, 2); + assert_eq!(report.resource_selector_records.len(), 2); + assert_eq!(report.resource_selector_records[0].name, "Root.imb"); + assert!(report.anonymous_selector_records.is_empty()); + } +} diff --git a/crates/rrt-runtime/src/inspect/win/names.rs b/crates/rrt-runtime/src/inspect/win/names.rs new file mode 100644 index 0000000..c061cc3 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/win/names.rs @@ -0,0 +1,33 @@ +pub(super) fn parse_imb_reference_at(bytes: &[u8], offset: usize) -> Option { + if offset > 0 { + let previous = *bytes.get(offset - 1)?; + if previous != 0 { + return None; + } + } + let slice = bytes.get(offset..)?; + let nul = slice.iter().position(|byte| *byte == 0)?; + let candidate = slice.get(..nul)?; + if candidate.len() < 5 { + return None; + } + let value = std::str::from_utf8(candidate).ok()?; + if !value.ends_with(".imb") { + return None; + } + if !value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b' ')) + { + return None; + } + Some(value.to_string()) +} + +pub(super) fn parse_inline_ascii_name(bytes: &[u8], offset: usize) -> Option { + let prefix = *bytes.get(offset)?; + if prefix != 0 { + return None; + } + parse_imb_reference_at(bytes, offset + 1) +} diff --git a/crates/rrt-runtime/src/inspect/win/read.rs b/crates/rrt-runtime/src/inspect/win/read.rs new file mode 100644 index 0000000..cc11187 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/win/read.rs @@ -0,0 +1,4 @@ +pub(super) fn read_u32_le(bytes: &[u8], offset: usize) -> Option { + let slice = bytes.get(offset..offset + 4)?; + Some(u32::from_le_bytes(slice.try_into().ok()?)) +} diff --git a/crates/rrt-runtime/src/inspect/win/references.rs b/crates/rrt-runtime/src/inspect/win/references.rs new file mode 100644 index 0000000..f7977d8 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/win/references.rs @@ -0,0 +1,134 @@ +use super::names::parse_imb_reference_at; +use super::read::read_u32_le; +use super::types::{ + WinHeaderWord, WinReferenceDeltaFrequency, WinResourceRecordSample, WinResourceReference, +}; + +pub(super) fn collect_imb_references(bytes: &[u8]) -> Vec { + let mut references = Vec::new(); + let mut offset = 0usize; + while offset < bytes.len() { + if let Some(name) = parse_imb_reference_at(bytes, offset) { + references.push(WinResourceReference { + offset, + offset_hex: format!("0x{offset:04x}"), + name, + }); + } + offset += 1; + } + references +} + +pub(super) fn build_resource_record_samples( + bytes: &[u8], + references: &[WinResourceReference], +) -> Vec { + let mut samples = Vec::with_capacity(references.len()); + for (index, reference) in references.iter().enumerate() { + let previous_offset = index + .checked_sub(1) + .and_then(|previous| references.get(previous)) + .map(|previous| previous.offset); + let delta_from_previous = previous_offset.map(|previous| reference.offset - previous); + let delta_from_previous_hex = delta_from_previous.map(|delta| format!("0x{delta:x}")); + + let prelude_words = if reference.offset >= 16 { + (0..4) + .map(|index| { + let offset = reference.offset - 16 + index * 4; + let value = read_u32_le(bytes, offset).unwrap_or(0); + WinHeaderWord { + offset, + offset_hex: format!("0x{offset:04x}"), + value, + value_hex: format!("0x{value:08x}"), + } + }) + .collect() + } else { + Vec::new() + }; + + let name_end = reference.offset + reference.name.len(); + let post_name_word_0 = read_u32_le(bytes, name_end + 1).unwrap_or(0); + let post_name_word_1 = read_u32_le(bytes, name_end + 5).unwrap_or(0); + let post_name_word_0_high_u16 = ((post_name_word_0 >> 16) & 0xffff) as u16; + let post_name_word_0_low_u16 = (post_name_word_0 & 0xffff) as u16; + + samples.push(WinResourceRecordSample { + offset: reference.offset, + offset_hex: reference.offset_hex.clone(), + name: reference.name.clone(), + delta_from_previous, + delta_from_previous_hex, + prelude_words, + post_name_word_0, + post_name_word_0_hex: format!("0x{post_name_word_0:08x}"), + post_name_word_0_high_u16, + post_name_word_0_high_u16_hex: format!("0x{post_name_word_0_high_u16:04x}"), + post_name_word_0_low_u16, + post_name_word_0_low_u16_hex: format!("0x{post_name_word_0_low_u16:04x}"), + post_name_word_1, + post_name_word_1_hex: format!("0x{post_name_word_1:08x}"), + }); + } + samples +} + +pub(super) fn build_delta_histogram( + samples: &[WinResourceRecordSample], +) -> Vec { + let mut counts = std::collections::BTreeMap::::new(); + for sample in samples { + if let Some(delta) = sample.delta_from_previous { + *counts.entry(delta).or_default() += 1; + } + } + + let mut frequencies = counts + .into_iter() + .map(|(delta, count)| WinReferenceDeltaFrequency { + delta, + delta_hex: format!("0x{delta:x}"), + count, + }) + .collect::>(); + frequencies.sort_by(|left, right| { + right + .count + .cmp(&left.count) + .then_with(|| left.delta.cmp(&right.delta)) + }); + frequencies.truncate(12); + frequencies +} + +pub(super) fn shared_prelude_prefix_hex( + samples: &[WinResourceRecordSample], +) -> Option> { + let first = samples.first()?; + if first.prelude_words.len() < 3 { + return None; + } + let prefix = first.prelude_words[..3] + .iter() + .map(|word| word.value) + .collect::>(); + if samples.iter().all(|sample| { + sample.prelude_words.len() >= 3 + && sample.prelude_words[..3] + .iter() + .map(|word| word.value) + .collect::>() + == prefix + }) { + return Some( + prefix + .into_iter() + .map(|value| format!("0x{value:08x}")) + .collect(), + ); + } + None +} diff --git a/crates/rrt-runtime/src/inspect/win/selectors.rs b/crates/rrt-runtime/src/inspect/win/selectors.rs new file mode 100644 index 0000000..3a274ca --- /dev/null +++ b/crates/rrt-runtime/src/inspect/win/selectors.rs @@ -0,0 +1,115 @@ +use super::read::read_u32_le; +use super::types::{ + WinAnonymousSelectorRecord, WinResourceRecordSample, WinResourceReference, + WinResourceSelectorRecord, +}; + +pub(super) fn build_resource_selector_records( + samples: &[WinResourceRecordSample], +) -> Vec { + samples + .iter() + .map(|sample| { + let post_name_word_1_high_u16 = ((sample.post_name_word_1 >> 16) & 0xffff) as u16; + let post_name_word_1_middle_u16 = ((sample.post_name_word_1 >> 8) & 0xffff) as u16; + let post_name_word_1_low_u16 = (sample.post_name_word_1 & 0xffff) as u16; + WinResourceSelectorRecord { + offset: sample.offset, + offset_hex: sample.offset_hex.clone(), + name: sample.name.clone(), + post_name_word_0: sample.post_name_word_0, + post_name_word_0_hex: sample.post_name_word_0_hex.clone(), + selector_high_u16: sample.post_name_word_0_high_u16, + selector_high_u16_hex: sample.post_name_word_0_high_u16_hex.clone(), + selector_low_u16: sample.post_name_word_0_low_u16, + selector_low_u16_hex: sample.post_name_word_0_low_u16_hex.clone(), + post_name_word_1: sample.post_name_word_1, + post_name_word_1_hex: sample.post_name_word_1_hex.clone(), + post_name_word_1_high_u16, + post_name_word_1_high_u16_hex: format!("0x{post_name_word_1_high_u16:04x}"), + post_name_word_1_middle_u16, + post_name_word_1_middle_u16_hex: format!("0x{post_name_word_1_middle_u16:04x}"), + post_name_word_1_low_u16, + post_name_word_1_low_u16_hex: format!("0x{post_name_word_1_low_u16:04x}"), + } + }) + .collect() +} + +pub(super) fn collect_anonymous_selector_records( + bytes: &[u8], + references: &[WinResourceReference], +) -> Vec { + const PRELUDE: [u8; 12] = [ + 0xb8, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb9, 0x0b, 0x00, 0x00, + ]; + + let mut records = Vec::new(); + let mut start = 0usize; + while let Some(relative) = bytes.get(start..).and_then(|slice| { + slice + .windows(PRELUDE.len()) + .position(|window| window == PRELUDE) + }) { + let record_offset = start + relative; + let name_len = read_u32_le(bytes, record_offset + PRELUDE.len()).unwrap_or(0); + if name_len == 0 { + let selector_word_0 = read_u32_le(bytes, record_offset + 0x10).unwrap_or(0); + let selector_word_0_low_u16 = (selector_word_0 & 0xffff) as u16; + if (0xc352..=0xc39b).contains(&selector_word_0_low_u16) { + let preceding_named_record = references + .iter() + .rev() + .find(|reference| reference.offset < record_offset); + let following_named_record = references + .iter() + .find(|reference| reference.offset > record_offset); + let selector_word_1 = read_u32_le(bytes, record_offset + 0x14).unwrap_or(0); + let selector_word_0_high_u16 = ((selector_word_0 >> 16) & 0xffff) as u16; + let selector_word_1_middle_u16 = ((selector_word_1 >> 8) & 0xffff) as u16; + let body_word_0 = read_u32_le(bytes, record_offset + 0x18).unwrap_or(0); + let body_word_1 = read_u32_le(bytes, record_offset + 0x1c).unwrap_or(0); + let body_word_2 = read_u32_le(bytes, record_offset + 0x20).unwrap_or(0); + let body_word_3 = read_u32_le(bytes, record_offset + 0x24).unwrap_or(0); + let footer_word_0 = read_u32_le(bytes, record_offset + 0x98).unwrap_or(0); + let footer_word_1 = read_u32_le(bytes, record_offset + 0x9c).unwrap_or(0); + records.push(WinAnonymousSelectorRecord { + record_offset, + record_offset_hex: format!("0x{record_offset:04x}"), + preceding_named_record_name: preceding_named_record + .map(|record| record.name.clone()), + preceding_named_record_offset_hex: preceding_named_record + .map(|record| record.offset_hex.clone()), + following_named_record_name: following_named_record + .map(|record| record.name.clone()), + following_named_record_offset_hex: following_named_record + .map(|record| record.offset_hex.clone()), + selector_word_0, + selector_word_0_hex: format!("0x{selector_word_0:08x}"), + selector_word_0_high_u16, + selector_word_0_high_u16_hex: format!("0x{selector_word_0_high_u16:04x}"), + selector_word_0_low_u16, + selector_word_0_low_u16_hex: format!("0x{selector_word_0_low_u16:04x}"), + selector_word_1, + selector_word_1_hex: format!("0x{selector_word_1:08x}"), + selector_word_1_middle_u16, + selector_word_1_middle_u16_hex: format!("0x{selector_word_1_middle_u16:04x}"), + body_word_0, + body_word_0_hex: format!("0x{body_word_0:08x}"), + body_word_1, + body_word_1_hex: format!("0x{body_word_1:08x}"), + body_word_2, + body_word_2_hex: format!("0x{body_word_2:08x}"), + body_word_3, + body_word_3_hex: format!("0x{body_word_3:08x}"), + footer_word_0, + footer_word_0_hex: format!("0x{footer_word_0:08x}"), + footer_word_1, + footer_word_1_hex: format!("0x{footer_word_1:08x}"), + }); + } + } + start = record_offset + 1; + } + records +} diff --git a/crates/rrt-runtime/src/inspect/win/types.rs b/crates/rrt-runtime/src/inspect/win/types.rs new file mode 100644 index 0000000..9e0da87 --- /dev/null +++ b/crates/rrt-runtime/src/inspect/win/types.rs @@ -0,0 +1,117 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WinHeaderWord { + pub offset: usize, + pub offset_hex: String, + pub value: u32, + pub value_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WinResourceReference { + pub offset: usize, + pub offset_hex: String, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WinReferenceDeltaFrequency { + pub delta: usize, + pub delta_hex: String, + pub count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WinResourceRecordSample { + pub offset: usize, + pub offset_hex: String, + pub name: String, + pub delta_from_previous: Option, + pub delta_from_previous_hex: Option, + pub prelude_words: Vec, + pub post_name_word_0: u32, + pub post_name_word_0_hex: String, + pub post_name_word_0_high_u16: u16, + pub post_name_word_0_high_u16_hex: String, + pub post_name_word_0_low_u16: u16, + pub post_name_word_0_low_u16_hex: String, + pub post_name_word_1: u32, + pub post_name_word_1_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WinResourceSelectorRecord { + pub offset: usize, + pub offset_hex: String, + pub name: String, + pub post_name_word_0: u32, + pub post_name_word_0_hex: String, + pub selector_high_u16: u16, + pub selector_high_u16_hex: String, + pub selector_low_u16: u16, + pub selector_low_u16_hex: String, + pub post_name_word_1: u32, + pub post_name_word_1_hex: String, + pub post_name_word_1_high_u16: u16, + pub post_name_word_1_high_u16_hex: String, + pub post_name_word_1_middle_u16: u16, + pub post_name_word_1_middle_u16_hex: String, + pub post_name_word_1_low_u16: u16, + pub post_name_word_1_low_u16_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WinAnonymousSelectorRecord { + pub record_offset: usize, + pub record_offset_hex: String, + pub preceding_named_record_name: Option, + pub preceding_named_record_offset_hex: Option, + pub following_named_record_name: Option, + pub following_named_record_offset_hex: Option, + pub selector_word_0: u32, + pub selector_word_0_hex: String, + pub selector_word_0_high_u16: u16, + pub selector_word_0_high_u16_hex: String, + pub selector_word_0_low_u16: u16, + pub selector_word_0_low_u16_hex: String, + pub selector_word_1: u32, + pub selector_word_1_hex: String, + pub selector_word_1_middle_u16: u16, + pub selector_word_1_middle_u16_hex: String, + pub body_word_0: u32, + pub body_word_0_hex: String, + pub body_word_1: u32, + pub body_word_1_hex: String, + pub body_word_2: u32, + pub body_word_2_hex: String, + pub body_word_3: u32, + pub body_word_3_hex: String, + pub footer_word_0: u32, + pub footer_word_0_hex: String, + pub footer_word_1: u32, + pub footer_word_1_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WinInspectionReport { + pub file_size: usize, + pub common_header_len: usize, + pub common_header_len_hex: String, + pub shared_header_words: Vec, + pub matches_observed_common_signature: bool, + pub common_resource_record_prelude_prefix_words: Option>, + pub name_len_matches_prelude_word_3_plus_nul_count: usize, + pub inline_root_resource_name: Option, + pub inline_root_resource_offset: Option, + pub inline_root_resource_offset_hex: Option, + pub imb_reference_count: usize, + pub unique_imb_reference_count: usize, + pub unique_imb_references: Vec, + pub dominant_reference_deltas: Vec, + pub resource_selector_records: Vec, + pub anonymous_selector_records: Vec, + pub first_resource_record_samples: Vec, + pub first_imb_references: Vec, + pub notes: Vec, +} diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index a7081a5..b5dd043 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -1,158 +1,18 @@ -pub mod building; pub mod calendar; -pub mod campaign_exe; -pub mod economy; -pub mod import; +pub mod derived; +pub mod documents; +pub mod engine; +pub mod event; +pub mod inspect; pub mod persistence; -pub mod pk4; -pub mod runtime; -pub mod smp; -pub mod step; +pub mod state; pub mod summary; -pub mod win; +pub mod validation; -pub use building::{ - BuildingTypeNamedBindingComparison, BuildingTypeSourceEntry, BuildingTypeSourceFile, - BuildingTypeSourceKind, BuildingTypeSourceReport, inspect_building_types_dir, - inspect_building_types_dir_with_bindings, -}; -pub use calendar::{CalendarPoint, MONTH_SLOTS_PER_YEAR, PHASE_SLOTS_PER_MONTH, TICKS_PER_PHASE}; -pub use campaign_exe::{ - CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, CampaignPageBand, CampaignScenarioEntry, - OBSERVED_CAMPAIGN_SCENARIO_NAMES, inspect_campaign_exe_bytes, inspect_campaign_exe_file, -}; -pub use economy::{ - CARGO_TYPE_MAGIC, CargoEconomySourceReport, CargoLiveRegistryEntry, CargoNameToken, - CargoRegistrySourceKind, CargoSelectorEntry, CargoSelectorReport, CargoSkinDescriptorEntry, - CargoSkinInspectionReport, CargoTypeEntry, CargoTypeInspectionReport, - NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT, - inspect_cargo_economy_sources, inspect_cargo_economy_sources_with_bindings, - inspect_cargo_skin_pk4, inspect_cargo_types_dir, -}; -pub use import::{ - OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument, - RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource, - RuntimeStateDumpDocument, RuntimeStateDumpSource, RuntimeStateImport, - SAVE_SLICE_DOCUMENT_FORMAT_VERSION, STATE_DUMP_FORMAT_VERSION, - load_runtime_overlay_import_document, load_runtime_save_slice_document, - load_runtime_state_import, project_save_slice_overlay_to_runtime_state_import, - project_save_slice_to_runtime_state_import, save_runtime_overlay_import_document, - save_runtime_save_slice_document, validate_runtime_overlay_import_document, - validate_runtime_save_slice_document, validate_runtime_state_dump_document, -}; -pub use persistence::{ - RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION, - load_runtime_snapshot_document, save_runtime_snapshot_document, - validate_runtime_snapshot_document, -}; -pub use pk4::{ - PK4_DIRECTORY_ENTRY_STRIDE, PK4_MAGIC, Pk4Entry, Pk4ExtractionReport, Pk4InspectionReport, - extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file, -}; -pub use runtime::{ - RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A, - RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, RUNTIME_COMPANY_STAT_SLOT_COUNT, - RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN, RUNTIME_WORLD_ISSUE_CREDIT_MARKET, - RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE, RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE, - RUNTIME_WORLD_ISSUE_PRIME_RATE, RuntimeAnnualFinanceNewsEvent, RuntimeCargoCatalogEntry, - RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, - RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, - RuntimeCompanyAnnualBondPolicyState, RuntimeCompanyAnnualCreditorPressureState, - RuntimeCompanyAnnualDeepDistressState, RuntimeCompanyAnnualDividendPolicyState, - RuntimeCompanyAnnualFinancePolicyAction, RuntimeCompanyAnnualFinancePolicyState, - RuntimeCompanyAnnualFinanceState, RuntimeCompanyAnnualStockIssueState, - RuntimeCompanyAnnualStockRepurchaseState, RuntimeCompanyBondSlot, - RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric, - RuntimeCompanyMarketState, RuntimeCompanyMetric, RuntimeCompanyPeriodicServiceState, - RuntimeCompanyPeriodicSideLatchState, RuntimeCompanyStatBandCandidate, - RuntimeCompanyStatSelector, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, - RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator, - RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, - RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, - RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, - RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, - RuntimePackedEventTextBandSummary, RuntimePlayer, RuntimePlayerConditionTestScope, - RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, - RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, - RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate, - RuntimeWorldIssueState, RuntimeWorldRestoreState, RuntimeWorldRoutePreferenceOverrideState, - runtime_annual_bond_principal_flow_relation_label, - runtime_annual_finance_news_family_candidate_label, - runtime_begin_company_periodic_route_preference_override, - runtime_company_annual_bond_policy_state, runtime_company_annual_creditor_pressure_state, - runtime_company_annual_deep_distress_state, runtime_company_annual_dividend_policy_state, - runtime_company_annual_finance_policy_action_label, - runtime_company_annual_finance_policy_state, runtime_company_annual_finance_state, - runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state, - runtime_company_assigned_share_pool, runtime_company_average_live_bond_coupon, - runtime_company_book_value_per_share, runtime_company_credit_rating, - runtime_company_investor_confidence, runtime_company_management_attitude, - runtime_company_market_value, runtime_company_periodic_service_state, - runtime_company_prime_rate, runtime_company_recent_per_share_subscore, - runtime_company_stat_value, runtime_company_stat_value_f64, - runtime_company_unassigned_share_pool, runtime_end_company_periodic_route_preference_override, - runtime_world_annual_finance_mode_active, runtime_world_bankruptcy_allowed, - runtime_world_bond_issue_and_repayment_allowed, runtime_world_building_density_growth_setting, - runtime_world_dividend_adjustment_allowed, runtime_world_issue_opinion_multiplier, - runtime_world_issue_opinion_term_sum_raw, runtime_world_issue_state, - runtime_world_prime_rate_baseline, runtime_world_stock_issue_and_buyback_allowed, -}; -pub use smp::{ - SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane, - SmpAlignedRuntimeRuleBandProbe, SmpAsciiPreview, SmpClassicPackedProfileBlock, - SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe, - SmpHeaderVariantProbe, SmpInfrastructureAssetTraceReport, SmpInspectionReport, SmpKnownTagHit, - SmpLoadedCandidateAvailabilityTable, SmpLoadedCargoCatalog, SmpLoadedCargoCatalogEntry, - SmpLoadedChairmanProfileEntry, SmpLoadedChairmanProfileTable, SmpLoadedCompanyRoster, - SmpLoadedCompanyRosterEntry, SmpLoadedEventRuntimeCollectionSummary, - SmpLoadedNamedLocomotiveAvailabilityTable, SmpLoadedPackedEventCompactControlSummary, - SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary, - SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary, - SmpLoadedPackedEventTextBandSummary, SmpLoadedPlacedStructureCollection, - SmpLoadedPlacedStructureDynamicSideBufferSummary, SmpLoadedPlacedStructureEntry, - SmpLoadedProfile, SmpLoadedRegionCollection, SmpLoadedRegionEntry, - SmpLoadedRegionFixedRowRunSummary, SmpLoadedRegionProfileCollection, - SmpLoadedRegionProfileEntry, SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, - SmpLoadedWorldEconomicTuningState, SmpLoadedWorldFinanceNeighborhoodState, - SmpLoadedWorldIssue37State, SmpLocomotivePolicyFieldObservation, - SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe, - SmpMapTitleHintAdjacentPair, SmpMapTitleHintMapReference, SmpMapTitleHintProbe, - SmpMapTitleHintTitleHit, SmpPackedProfileWordLane, SmpPeriodicCompanyServiceTraceReport, - SmpPostSpecialConditionsScalarLane, SmpPostSpecialConditionsScalarProbe, - SmpPostTextFieldNeighborhoodProbe, SmpPostTextFloatAlignmentCandidate, - SmpPostTextGroundedFieldObservation, SmpPreRecipeScalarPlateauLane, - SmpPreRecipeScalarPlateauProbe, SmpPreamble, SmpPreambleWord, SmpRecipeBookLineSummary, - SmpRecipeBookSummaryBook, SmpRecipeBookSummaryProbe, SmpRegionServiceTraceReport, - SmpRt3105PackedProfileBlock, SmpRt3105PackedProfileProbe, SmpRt3105PostSpanBridgeProbe, - SmpRt3105SaveBridgePayloadProbe, SmpRt3105SaveNameTableEntry, SmpRt3105SaveNameTableProbe, - SmpRuntimeAnchorCycleBlock, SmpRuntimePostSpanHeaderCandidate, SmpRuntimePostSpanProbe, - SmpRuntimeTrailerBlock, SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock, - SmpSaveChairmanRecordAnalysisEntry, SmpSaveCompanyChairmanAnalysisReport, - SmpSaveCompanyRecordAnalysisEntry, SmpSaveDwordCandidate, SmpSaveLoadCandidateTableSummary, - SmpSaveLoadSummary, SmpSavePlacedStructureDynamicSideBufferAlignmentProbe, - SmpSavePlacedStructureDynamicSideBufferNamePairSummary, - SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary, - SmpSavePlacedStructureDynamicSideBufferProbe, SmpSaveRegionFixedRowRunComparisonReport, - SmpSaveRegionQueuedNoticeRecordProbe, SmpSaveScalarCandidate, - SmpSaveTaggedCollectionHeaderProbe, SmpSaveWorldEconomicTuningProbe, - SmpSaveWorldFinanceNeighborhoodProbe, SmpSaveWorldIssue37Probe, - SmpSaveWorldSelectionRoleAnalysis, SmpSaveWorldSelectionRoleAnalysisEntry, - SmpSecondaryVariantProbe, SmpServiceTraceBranchStatus, SmpSharedHeader, - SmpSpecialConditionEntry, SmpSpecialConditionsProbe, - compare_save_region_fixed_row_run_candidates, inspect_map_title_hint_bytes, - inspect_map_title_hint_file, inspect_save_company_and_chairman_analysis_bytes, - inspect_save_company_and_chairman_analysis_file, inspect_save_infrastructure_asset_trace_file, - inspect_save_periodic_company_service_trace_file, - inspect_save_placed_structure_dynamic_side_buffer_file, - inspect_save_region_queued_notice_records_file, inspect_save_region_service_trace_file, - inspect_smp_bytes, inspect_smp_file, inspect_unclassified_save_collection_headers_file, - load_save_slice_file, load_save_slice_from_report, -}; -pub use step::{BoundaryEvent, ServiceEvent, StepCommand, StepResult, execute_step_command}; +#[cfg(test)] +pub(crate) mod test_support; + +pub use calendar::CalendarPoint; +pub use engine::{BoundaryEvent, ServiceEvent, StepCommand, StepResult, execute_step_command}; +pub use state::RuntimeState; pub use summary::RuntimeSummary; -pub use win::{ - WinAnonymousSelectorRecord, WinHeaderWord, WinInspectionReport, WinReferenceDeltaFrequency, - WinResourceRecordSample, WinResourceReference, WinResourceSelectorRecord, inspect_win_bytes, - inspect_win_file, -}; diff --git a/crates/rrt-runtime/src/persistence.rs b/crates/rrt-runtime/src/persistence.rs index 50a82c9..4c5bb7b 100644 --- a/crates/rrt-runtime/src/persistence.rs +++ b/crates/rrt-runtime/src/persistence.rs @@ -68,9 +68,8 @@ pub fn save_runtime_snapshot_document( #[cfg(test)] mod tests { use super::*; - use crate::{ - CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeWorldRestoreState, - }; + use crate::CalendarPoint; + use crate::state::{RuntimeSaveProfileState, RuntimeServiceState, RuntimeWorldRestoreState}; use std::collections::BTreeMap; fn snapshot() -> RuntimeSnapshotDocument { diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs deleted file mode 100644 index 7b269f5..0000000 --- a/crates/rrt-runtime/src/runtime.rs +++ /dev/null @@ -1,10913 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::sync::OnceLock; - -use serde::{Deserialize, Serialize}; - -use crate::CalendarPoint; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeCompanyControllerKind { - #[default] - Unknown, - Human, - Ai, -} - -fn runtime_company_default_active() -> bool { - true -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompany { - pub company_id: u32, - pub current_cash: i64, - pub debt: u64, - #[serde(default)] - pub credit_rating_score: Option, - #[serde(default)] - pub prime_rate: Option, - #[serde(default = "runtime_company_default_active")] - pub active: bool, - #[serde(default)] - pub available_track_laying_capacity: Option, - #[serde(default)] - pub controller_kind: RuntimeCompanyControllerKind, - #[serde(default)] - pub linked_chairman_profile_id: Option, - #[serde(default)] - pub book_value_per_share: i64, - #[serde(default)] - pub investor_confidence: i64, - #[serde(default)] - pub management_attitude: i64, - #[serde(default)] - pub takeover_cooldown_year: Option, - #[serde(default)] - pub merger_cooldown_year: Option, - #[serde(default)] - pub track_piece_counts: RuntimeTrackPieceCounts, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct RuntimeCompanyMarketState { - #[serde(default)] - pub outstanding_shares: u32, - #[serde(default)] - pub bond_count: u8, - #[serde(default)] - pub largest_live_bond_principal: Option, - #[serde(default)] - pub highest_coupon_live_bond_principal: Option, - #[serde(default)] - pub mutable_support_scalar_raw_u32: u32, - #[serde(default)] - pub young_company_support_scalar_raw_u32: u32, - #[serde(default)] - pub support_progress_word: u32, - #[serde(default)] - pub recent_per_share_cache_absolute_counter: u32, - #[serde(default)] - pub recent_per_share_cached_value_bits: u64, - #[serde(default)] - pub recent_per_share_subscore_raw_u32: u32, - #[serde(default)] - pub cached_share_price_raw_u32: u32, - #[serde(default)] - pub chairman_salary_baseline: u32, - #[serde(default)] - pub chairman_salary_current: u32, - #[serde(default)] - pub chairman_bonus_year: u32, - #[serde(default)] - pub chairman_bonus_amount: i32, - #[serde(default)] - pub founding_year: u32, - #[serde(default)] - pub last_bankruptcy_year: u32, - #[serde(default)] - pub last_dividend_year: u32, - #[serde(default)] - pub current_issue_calendar_word: u32, - #[serde(default)] - pub current_issue_calendar_word_2: u32, - #[serde(default)] - pub prior_issue_calendar_word: u32, - #[serde(default)] - pub prior_issue_calendar_word_2: u32, - #[serde(default)] - pub city_connection_latch: bool, - #[serde(default)] - pub linked_transit_latch: bool, - #[serde(default)] - pub linked_transit_route_anchor_entry_id: Option, - #[serde(default)] - pub linked_transit_route_anchor_fallback_counts: Vec, - #[serde(default)] - pub stat_band_root_0cfb_candidates: Vec, - #[serde(default)] - pub stat_band_root_0d7f_candidates: Vec, - #[serde(default)] - pub stat_band_root_1c47_candidates: Vec, - #[serde(default)] - pub year_stat_family_qword_bits: Vec, - #[serde(default)] - pub special_stat_family_232a_qword_bits: Vec, - #[serde(default)] - pub issue_opinion_terms_raw_i32: Vec, - #[serde(default)] - pub live_bond_slots: Vec, - #[serde(default)] - pub direct_control_transfer_float_fields_raw_u32: BTreeMap, - #[serde(default)] - pub direct_control_transfer_int_fields_raw_u32: BTreeMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyBondSlot { - pub slot_index: u32, - pub principal: u32, - #[serde(default)] - pub maturity_year: u32, - pub coupon_rate_raw_u32: u32, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyAnnualFinanceState { - pub company_id: u32, - pub outstanding_shares: u32, - pub bond_count: u8, - #[serde(default)] - pub largest_live_bond_principal: Option, - #[serde(default)] - pub highest_coupon_live_bond_principal: Option, - #[serde(default)] - pub live_bond_coupon_burden_total: Option, - pub assigned_share_pool: u32, - pub unassigned_share_pool: u32, - #[serde(default)] - pub cached_share_price: Option, - pub chairman_salary_baseline: u32, - pub chairman_salary_current: u32, - pub chairman_bonus_year: u32, - pub chairman_bonus_amount: i32, - pub founding_year: u32, - pub last_bankruptcy_year: u32, - pub last_dividend_year: u32, - #[serde(default)] - pub years_since_founding: Option, - #[serde(default)] - pub years_since_last_bankruptcy: Option, - #[serde(default)] - pub years_since_last_dividend: Option, - #[serde(default)] - pub current_partial_year_weight_numerator: Option, - #[serde(default)] - pub trailing_full_year_year_words: Vec, - #[serde(default)] - pub trailing_full_year_net_profits: Vec, - #[serde(default)] - pub trailing_full_year_revenues: Vec, - #[serde(default)] - pub trailing_full_year_fuel_costs: Vec, - #[serde(default)] - pub current_issue_absolute_counter: Option, - #[serde(default)] - pub prior_issue_absolute_counter: Option, - #[serde(default)] - pub current_issue_age_absolute_counter_delta: Option, - pub current_issue_calendar_word: u32, - #[serde(default)] - pub current_issue_calendar_word_2: u32, - pub prior_issue_calendar_word: u32, - #[serde(default)] - pub prior_issue_calendar_word_2: u32, - #[serde(default)] - pub preferred_locomotive_engine_type_raw_u8: Option, - pub city_connection_latch: bool, - pub linked_transit_latch: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct RuntimeCompanyPeriodicSideLatchState { - #[serde(default)] - pub preferred_locomotive_engine_type_raw_u8: Option, - #[serde(default)] - pub city_connection_latch: bool, - #[serde(default)] - pub linked_transit_latch: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyPeriodicServiceState { - pub company_id: u32, - #[serde(default)] - pub preferred_locomotive_engine_type_raw_u8: Option, - pub city_connection_latch: bool, - pub linked_transit_latch: bool, - #[serde(default)] - pub base_route_preference_raw_u8: Option, - #[serde(default)] - pub effective_route_preference_raw_u8: Option, - pub electric_route_preference_override_active: bool, - pub effective_route_quality_multiplier_basis_points: i64, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeWorldRoutePreferenceOverrideState { - pub company_id: u32, - #[serde(default)] - pub base_route_preference_raw_u8: Option, - #[serde(default)] - pub effective_route_preference_raw_u8: Option, - pub electric_route_preference_override_active: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyAnnualCreditorPressureState { - pub company_id: u32, - #[serde(default)] - pub annual_mode_active: Option, - #[serde(default)] - pub bankruptcy_allowed: Option, - #[serde(default)] - pub years_since_last_bankruptcy: Option, - #[serde(default)] - pub years_since_founding: Option, - pub recent_bad_net_profit_year_count: u32, - #[serde(default)] - pub recent_peak_revenue: Option, - #[serde(default)] - pub recent_three_year_net_profit_total: Option, - #[serde(default)] - pub pressure_ladder_cash_floor: Option, - #[serde(default)] - pub current_cash_plus_slot_12_total: Option, - #[serde(default)] - pub support_adjusted_share_price_floor: Option, - #[serde(default)] - pub support_adjusted_share_price_scalar: Option, - #[serde(default)] - pub current_fuel_cost: Option, - #[serde(default)] - pub current_fuel_cost_floor: Option, - pub eligible_for_bankruptcy_branch: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyAnnualDeepDistressState { - pub company_id: u32, - #[serde(default)] - pub bankruptcy_allowed: Option, - #[serde(default)] - pub years_since_founding: Option, - #[serde(default)] - pub years_since_last_bankruptcy: Option, - #[serde(default)] - pub current_cash: Option, - pub recent_first_three_net_profit_years: Vec, - #[serde(default)] - pub deep_distress_cash_floor: Option, - #[serde(default)] - pub deep_distress_net_profit_floor: Option, - pub eligible_for_bankruptcy_fallback: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyAnnualStockRepurchaseState { - pub company_id: u32, - #[serde(default)] - pub annual_mode_active: Option, - #[serde(default)] - pub stock_issue_and_buyback_allowed: Option, - pub city_connection_latch: bool, - #[serde(default)] - pub building_density_growth_setting: Option, - #[serde(default)] - pub linked_chairman_profile_id: Option, - #[serde(default)] - pub linked_chairman_personality_raw_u8: Option, - #[serde(default)] - pub repurchase_batch_size: Option, - #[serde(default)] - pub repurchase_factor_basis_points: Option, - #[serde(default)] - pub current_cash: Option, - #[serde(default)] - pub stock_value_gate_cash_floor: Option, - #[serde(default)] - pub support_adjusted_share_price_scalar: Option, - #[serde(default)] - pub affordability_cash_floor: Option, - #[serde(default)] - pub unassigned_share_pool: Option, - pub eligible_for_single_batch_repurchase: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyAnnualStockIssueState { - pub company_id: u32, - #[serde(default)] - pub annual_mode_active: Option, - #[serde(default)] - pub stock_issue_and_buyback_allowed: Option, - #[serde(default)] - pub bond_issue_and_repayment_allowed: Option, - #[serde(default)] - pub years_since_founding: Option, - #[serde(default)] - pub live_bond_count: Option, - #[serde(default)] - pub initial_issue_batch_size: Option, - #[serde(default)] - pub trimmed_issue_batch_size: Option, - #[serde(default)] - pub share_pressure_basis_points: Option, - #[serde(default)] - pub pressured_support_adjusted_share_price_scalar: Option, - #[serde(default)] - pub pressured_proceeds: Option, - #[serde(default)] - pub book_value_per_share_floor_applied: Option, - #[serde(default)] - pub price_to_book_ratio_basis_points: Option, - #[serde(default)] - pub current_cash: Option, - #[serde(default)] - pub highest_coupon_live_bond_principal: Option, - #[serde(default)] - pub highest_coupon_live_bond_rate_basis_points: Option, - #[serde(default)] - pub current_issue_age_absolute_counter_delta: Option, - #[serde(default)] - pub current_issue_cooldown_floor: Option, - #[serde(default)] - pub minimum_price_to_book_ratio_basis_points: Option, - #[serde(default)] - pub passes_share_price_floor: Option, - #[serde(default)] - pub passes_proceeds_floor: Option, - #[serde(default)] - pub passes_cash_gate: Option, - #[serde(default)] - pub passes_issue_cooldown_gate: Option, - #[serde(default)] - pub passes_coupon_price_to_book_gate: Option, - pub eligible_for_double_tranche_issue: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyAnnualBondPolicyState { - pub company_id: u32, - #[serde(default)] - pub annual_mode_active: Option, - #[serde(default)] - pub bond_issue_and_repayment_allowed: Option, - pub linked_transit_latch: bool, - #[serde(default)] - pub live_bond_count: Option, - #[serde(default)] - pub live_bond_principal_total: Option, - #[serde(default)] - pub matured_live_bond_count: Option, - #[serde(default)] - pub matured_live_bond_principal_total: Option, - #[serde(default)] - pub next_live_bond_maturity_year: Option, - #[serde(default)] - pub live_bond_coupon_burden_total: Option, - #[serde(default)] - pub current_cash: Option, - #[serde(default)] - pub cash_after_full_repayment: Option, - #[serde(default)] - pub issue_cash_floor: Option, - #[serde(default)] - pub issue_principal_step: Option, - #[serde(default)] - pub proposed_issue_bond_count: Option, - #[serde(default)] - pub proposed_issue_total_principal: Option, - #[serde(default)] - pub proposed_issue_years_to_maturity: Option, - pub eligible_for_bond_issue_branch: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyAnnualDividendPolicyState { - pub company_id: u32, - #[serde(default)] - pub annual_mode_active: Option, - #[serde(default)] - pub dividend_adjustment_allowed: Option, - #[serde(default)] - pub years_since_last_dividend: Option, - #[serde(default)] - pub years_since_founding: Option, - #[serde(default)] - pub outstanding_shares: Option, - #[serde(default)] - pub unassigned_share_pool: Option, - #[serde(default)] - pub weighted_recent_net_profit_total: Option, - #[serde(default)] - pub weighted_recent_net_profit_average: Option, - #[serde(default)] - pub current_cash: Option, - pub tiny_unassigned_share_cash_supplement_branch: bool, - #[serde(default)] - pub tentative_target_dividend_per_share_tenths: Option, - #[serde(default)] - pub current_dividend_per_share_tenths: Option, - #[serde(default)] - pub building_density_growth_setting: Option, - #[serde(default)] - pub growth_adjusted_current_dividend_per_share_tenths: Option, - #[serde(default)] - pub board_approved_dividend_rate_ceiling_tenths: Option, - #[serde(default)] - pub proposed_dividend_per_share_tenths: Option, - pub eligible_for_dividend_adjustment_branch: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, PartialOrd, Ord)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeCompanyAnnualFinancePolicyAction { - #[default] - None, - CreditorPressureBankruptcy, - DeepDistressBankruptcyFallback, - BondIssue, - StockRepurchase, - StockIssue, - DividendAdjustment, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyAnnualFinancePolicyState { - pub company_id: u32, - pub action: RuntimeCompanyAnnualFinancePolicyAction, - pub creditor_pressure_bankruptcy_eligible: bool, - pub deep_distress_bankruptcy_fallback_eligible: bool, - pub bond_issue_eligible: bool, - pub stock_repurchase_eligible: bool, - pub stock_issue_eligible: bool, - pub dividend_adjustment_eligible: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct RuntimeTrackPieceCounts { - #[serde(default)] - pub total: u32, - #[serde(default)] - pub single: u32, - #[serde(default)] - pub double: u32, - #[serde(default)] - pub transition: u32, - #[serde(default)] - pub electric: u32, - #[serde(default)] - pub non_electric: u32, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeTerritory { - pub territory_id: u32, - #[serde(default)] - pub name: Option, - #[serde(default)] - pub track_piece_counts: RuntimeTrackPieceCounts, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyTerritoryTrackPieceCount { - pub company_id: u32, - pub territory_id: u32, - #[serde(default)] - pub track_piece_counts: RuntimeTrackPieceCounts, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyTerritoryAccess { - pub company_id: u32, - pub territory_id: u32, -} - -fn runtime_player_default_active() -> bool { - true -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePlayer { - pub player_id: u32, - pub current_cash: i64, - #[serde(default = "runtime_player_default_active")] - pub active: bool, - #[serde(default)] - pub controller_kind: RuntimeCompanyControllerKind, -} - -fn runtime_chairman_profile_default_active() -> bool { - true -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeChairmanProfile { - pub profile_id: u32, - pub name: String, - #[serde(default = "runtime_chairman_profile_default_active")] - pub active: bool, - #[serde(default)] - pub current_cash: i64, - #[serde(default)] - pub linked_company_id: Option, - #[serde(default)] - pub company_holdings: BTreeMap, - #[serde(default)] - pub holdings_value_total: i64, - #[serde(default)] - pub net_worth_total: i64, - #[serde(default)] - pub purchasing_power_total: i64, -} - -fn runtime_train_default_active() -> bool { - true -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeTrain { - pub train_id: u32, - pub owner_company_id: u32, - #[serde(default)] - pub territory_id: Option, - #[serde(default)] - pub locomotive_name: Option, - #[serde(default = "runtime_train_default_active")] - pub active: bool, - #[serde(default)] - pub retired: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeLocomotiveCatalogEntry { - pub locomotive_id: u32, - pub name: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCargoCatalogEntry { - pub slot_id: u32, - pub label: String, - #[serde(default)] - pub cargo_class: RuntimeCargoClass, - #[serde(default)] - pub supplied_token_stem: Option, - #[serde(default)] - pub demanded_token_stem: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeCargoClass { - #[default] - Other, - Factory, - FarmMine, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum RuntimeCompanyTarget { - AllActive, - HumanCompanies, - AiCompanies, - SelectedCompany, - ConditionTrueCompany, - Ids { ids: Vec }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum RuntimePlayerTarget { - AllActive, - HumanPlayers, - AiPlayers, - SelectedPlayer, - ConditionTruePlayer, - Ids { ids: Vec }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum RuntimeChairmanTarget { - AllActive, - HumanChairmen, - AiChairmen, - SelectedChairman, - ConditionTrueChairman, - Ids { ids: Vec }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum RuntimeCargoPriceTarget { - All, - Named { name: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum RuntimeCargoProductionTarget { - All, - Factory, - FarmMine, - Named { name: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum RuntimeTerritoryTarget { - AllTerritories, - Ids { ids: Vec }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeCompanyConditionTestScope { - #[default] - Disabled, - AllCompanies, - SelectedCompanyOnly, - AiCompaniesOnly, - HumanCompaniesOnly, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum RuntimePlayerConditionTestScope { - #[default] - Disabled, - AllPlayers, - SelectedPlayerOnly, - AiPlayersOnly, - HumanPlayersOnly, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeConditionComparator { - Ge, - Le, - Gt, - Lt, - Eq, - Ne, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeCompanyMetric { - CurrentCash, - TotalDebt, - CreditRating, - PrimeRate, - BookValuePerShare, - InvestorConfidence, - ManagementAttitude, - TrackPiecesTotal, - TrackPiecesSingle, - TrackPiecesDouble, - TrackPiecesTransition, - TrackPiecesElectric, - TrackPiecesNonElectric, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeCompanyMarketMetric { - OutstandingShares, - BondCount, - LargestLiveBondPrincipal, - HighestCouponLiveBondPrincipal, - AssignedSharePool, - UnassignedSharePool, - CachedSharePrice, - ChairmanSalaryBaseline, - ChairmanSalaryCurrent, - ChairmanBonusAmount, - CurrentIssueAbsoluteCounter, - PriorIssueAbsoluteCounter, - CurrentIssueAgeAbsoluteCounterDelta, - CurrentIssueCalendarWord, - CurrentIssueCalendarWord2, - PriorIssueCalendarWord, - PriorIssueCalendarWord2, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeChairmanMetric { - CurrentCash, - HoldingsValueTotal, - NetWorthTotal, - PurchasingPowerTotal, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyStatSelector { - pub family_id: u32, - pub slot_id: u32, -} - -pub const RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER: u32 = 0x2329; -pub const RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A: u32 = 0x232a; -pub const RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH: u32 = 0x0d; -pub const RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE: u32 = 0x1d; -pub const RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING: u32 = 0x19; -pub const RUNTIME_COMPANY_STAT_SLOT_COUNT: u32 = 0x2b; -pub const RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN: u32 = 11; -pub const RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE: u32 = 0x37; -pub const RUNTIME_WORLD_ISSUE_CREDIT_MARKET: u32 = 0x38; -pub const RUNTIME_WORLD_ISSUE_PRIME_RATE: u32 = 0x39; -pub const RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE: u32 = 0x3a; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeWorldIssueState { - pub issue_id: u32, - pub raw_value_u32: u32, - #[serde(default)] - pub multiplier_raw_u32: Option, - #[serde(default)] - pub multiplier_value_f32_text: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePackedCalendarTuple { - pub year_word: u16, - pub month_1_based: u8, - pub week_1_based: u8, - pub day_1_based: u8, - pub hour_0_based: u8, - pub quarter_day_1_based: u8, - pub minute_0_based: u8, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeTerritoryMetric { - TrackPiecesTotal, - TrackPiecesSingle, - TrackPiecesDouble, - TrackPiecesTransition, - TrackPiecesElectric, - TrackPiecesNonElectric, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RuntimeTrackMetric { - Total, - Single, - Double, - Transition, - Electric, - NonElectric, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum RuntimeCondition { - WorldVariableThreshold { - index: u32, - comparator: RuntimeConditionComparator, - value: i64, - }, - CompanyNumericThreshold { - target: RuntimeCompanyTarget, - metric: RuntimeCompanyMetric, - comparator: RuntimeConditionComparator, - value: i64, - }, - CompanyVariableThreshold { - target: RuntimeCompanyTarget, - index: u32, - comparator: RuntimeConditionComparator, - value: i64, - }, - ChairmanNumericThreshold { - target: RuntimeChairmanTarget, - metric: RuntimeChairmanMetric, - comparator: RuntimeConditionComparator, - value: i64, - }, - PlayerVariableThreshold { - target: RuntimePlayerTarget, - index: u32, - comparator: RuntimeConditionComparator, - value: i64, - }, - TerritoryNumericThreshold { - target: RuntimeTerritoryTarget, - metric: RuntimeTerritoryMetric, - comparator: RuntimeConditionComparator, - value: i64, - }, - TerritoryVariableThreshold { - target: RuntimeTerritoryTarget, - index: u32, - comparator: RuntimeConditionComparator, - value: i64, - }, - CompanyTerritoryNumericThreshold { - target: RuntimeCompanyTarget, - territory: RuntimeTerritoryTarget, - metric: RuntimeTrackMetric, - comparator: RuntimeConditionComparator, - value: i64, - }, - SpecialConditionThreshold { - label: String, - comparator: RuntimeConditionComparator, - value: i64, - }, - CandidateAvailabilityThreshold { - name: String, - comparator: RuntimeConditionComparator, - value: i64, - }, - NamedLocomotiveAvailabilityThreshold { - name: String, - comparator: RuntimeConditionComparator, - value: i64, - }, - NamedLocomotiveCostThreshold { - name: String, - comparator: RuntimeConditionComparator, - value: i64, - }, - CargoProductionSlotThreshold { - slot: u32, - label: String, - comparator: RuntimeConditionComparator, - value: i64, - }, - CargoProductionTotalThreshold { - comparator: RuntimeConditionComparator, - value: i64, - }, - FactoryProductionTotalThreshold { - comparator: RuntimeConditionComparator, - value: i64, - }, - FarmMineProductionTotalThreshold { - comparator: RuntimeConditionComparator, - value: i64, - }, - OtherCargoProductionTotalThreshold { - comparator: RuntimeConditionComparator, - value: i64, - }, - LimitedTrackBuildingAmountThreshold { - comparator: RuntimeConditionComparator, - value: i64, - }, - TerritoryAccessCostThreshold { - comparator: RuntimeConditionComparator, - value: i64, - }, - EconomicStatusCodeThreshold { - comparator: RuntimeConditionComparator, - value: i64, - }, - WorldFlagEquals { - key: String, - value: bool, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum RuntimeEffect { - SetWorldFlag { - key: String, - value: bool, - }, - SetLimitedTrackBuildingAmount { - value: i32, - }, - SetEconomicStatusCode { - value: i32, - }, - SetCompanyCash { - target: RuntimeCompanyTarget, - value: i64, - }, - SetPlayerCash { - target: RuntimePlayerTarget, - value: i64, - }, - SetChairmanCash { - target: RuntimeChairmanTarget, - value: i64, - }, - SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget, - metric: RuntimeCompanyMetric, - value: i64, - }, - DeactivatePlayer { - target: RuntimePlayerTarget, - }, - DeactivateChairman { - target: RuntimeChairmanTarget, - }, - SetCompanyTerritoryAccess { - target: RuntimeCompanyTarget, - territory: RuntimeTerritoryTarget, - value: bool, - }, - ConfiscateCompanyAssets { - target: RuntimeCompanyTarget, - }, - DeactivateCompany { - target: RuntimeCompanyTarget, - }, - SetCompanyTrackLayingCapacity { - target: RuntimeCompanyTarget, - value: Option, - }, - RetireTrains { - #[serde(default)] - company_target: Option, - #[serde(default)] - territory_target: Option, - #[serde(default)] - locomotive_name: Option, - }, - AdjustCompanyCash { - target: RuntimeCompanyTarget, - delta: i64, - }, - AdjustCompanyDebt { - target: RuntimeCompanyTarget, - delta: i64, - }, - SetCandidateAvailability { - name: String, - value: u32, - }, - SetNamedLocomotiveAvailability { - name: String, - value: bool, - }, - SetNamedLocomotiveAvailabilityValue { - name: String, - value: u32, - }, - SetNamedLocomotiveCost { - name: String, - value: u32, - }, - SetCargoPriceOverride { - target: RuntimeCargoPriceTarget, - value: u32, - }, - SetCargoProductionOverride { - target: RuntimeCargoProductionTarget, - value: u32, - }, - SetCargoProductionSlot { - slot: u32, - value: u32, - }, - SetWorldVariable { - index: u32, - value: i64, - }, - SetCompanyVariable { - target: RuntimeCompanyTarget, - index: u32, - value: i64, - }, - SetPlayerVariable { - target: RuntimePlayerTarget, - index: u32, - value: i64, - }, - SetTerritoryVariable { - target: RuntimeTerritoryTarget, - index: u32, - value: i64, - }, - SetWorldScalarOverride { - key: String, - value: i64, - }, - SetTerritoryAccessCost { - value: u32, - }, - SetSpecialCondition { - label: String, - value: u32, - }, - AppendEventRecord { - record: Box, - }, - ActivateEventRecord { - record_id: u32, - }, - DeactivateEventRecord { - record_id: u32, - }, - RemoveEventRecord { - record_id: u32, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeEventRecordTemplate { - pub record_id: u32, - pub trigger_kind: u8, - pub active: bool, - #[serde(default)] - pub marks_collection_dirty: bool, - #[serde(default)] - pub one_shot: bool, - #[serde(default)] - pub conditions: Vec, - #[serde(default)] - pub effects: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeEventRecord { - pub record_id: u32, - pub trigger_kind: u8, - pub active: bool, - #[serde(default)] - pub service_count: u32, - #[serde(default)] - pub marks_collection_dirty: bool, - #[serde(default)] - pub one_shot: bool, - #[serde(default)] - pub has_fired: bool, - #[serde(default)] - pub conditions: Vec, - #[serde(default)] - pub effects: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePackedEventCollectionSummary { - pub source_kind: String, - pub mechanism_family: String, - pub mechanism_confidence: String, - #[serde(default)] - pub container_profile_family: Option, - pub packed_state_version: u32, - pub packed_state_version_hex: String, - pub live_id_bound: u32, - pub live_record_count: usize, - pub live_entry_ids: Vec, - #[serde(default)] - pub decoded_record_count: usize, - #[serde(default)] - pub imported_runtime_record_count: usize, - #[serde(default)] - pub records: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePackedEventRecordSummary { - pub record_index: usize, - pub live_entry_id: u32, - #[serde(default)] - pub payload_offset: Option, - #[serde(default)] - pub payload_len: Option, - pub decode_status: String, - #[serde(default)] - pub payload_family: String, - #[serde(default)] - pub trigger_kind: Option, - #[serde(default)] - pub active: Option, - #[serde(default)] - pub marks_collection_dirty: Option, - #[serde(default)] - pub one_shot: Option, - #[serde(default)] - pub compact_control: Option, - #[serde(default)] - pub text_bands: Vec, - #[serde(default)] - pub standalone_condition_row_count: usize, - #[serde(default)] - pub standalone_condition_rows: Vec, - #[serde(default)] - pub negative_sentinel_scope: Option, - #[serde(default)] - pub grouped_effect_row_counts: Vec, - #[serde(default)] - pub grouped_effect_rows: Vec, - #[serde(default)] - pub grouped_company_targets: Vec>, - #[serde(default)] - pub decoded_conditions: Vec, - #[serde(default)] - pub decoded_actions: Vec, - #[serde(default)] - pub executable_import_ready: bool, - #[serde(default)] - pub import_outcome: Option, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePackedEventNegativeSentinelScopeSummary { - pub company_test_scope: RuntimeCompanyConditionTestScope, - pub player_test_scope: RuntimePlayerConditionTestScope, - pub territory_scope_selector_is_0x63: bool, - #[serde(default)] - pub source_row_indexes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePackedEventCompactControlSummary { - pub mode_byte_0x7ef: u8, - pub primary_selector_0x7f0: u32, - pub grouped_mode_0x7f4: u8, - pub one_shot_header_0x7f5: u32, - pub modifier_flag_0x7f9: u8, - pub modifier_flag_0x7fa: u8, - #[serde(default)] - pub grouped_target_scope_ordinals_0x7fb: Vec, - #[serde(default)] - pub grouped_scope_checkboxes_0x7ff: Vec, - pub summary_toggle_0x800: u8, - #[serde(default)] - pub grouped_territory_selectors_0x80f: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePackedEventTextBandSummary { - pub label: String, - pub packed_len: usize, - pub present: bool, - pub preview: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePackedEventConditionRowSummary { - pub row_index: usize, - pub raw_condition_id: i32, - pub subtype: u8, - #[serde(default)] - pub flag_bytes: Vec, - #[serde(default)] - pub candidate_name: Option, - #[serde(default)] - pub comparator: Option, - #[serde(default)] - pub metric: Option, - #[serde(default)] - pub semantic_family: Option, - #[serde(default)] - pub semantic_preview: Option, - #[serde(default)] - pub requires_candidate_name_binding: bool, - #[serde(default)] - pub recovered_cargo_slot: Option, - #[serde(default)] - pub recovered_cargo_class: Option, - #[serde(default)] - pub recovered_cargo_label: Option, - #[serde(default)] - pub recovered_cargo_supplied_token_stem: Option, - #[serde(default)] - pub recovered_cargo_demanded_token_stem: Option, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimePackedEventGroupedEffectRowSummary { - pub group_index: usize, - pub row_index: usize, - pub descriptor_id: u32, - #[serde(default)] - pub descriptor_label: Option, - #[serde(default)] - pub target_mask_bits: Option, - #[serde(default)] - pub parameter_family: Option, - #[serde(default)] - pub grouped_target_subject: Option, - #[serde(default)] - pub grouped_target_scope: Option, - pub opcode: u8, - pub raw_scalar_value: i32, - pub value_byte_0x09: u8, - pub value_dword_0x0d: u32, - pub value_byte_0x11: u8, - pub value_byte_0x12: u8, - pub value_word_0x14: u16, - pub value_word_0x16: u16, - pub row_shape: String, - #[serde(default)] - pub semantic_family: Option, - #[serde(default)] - pub semantic_preview: Option, - #[serde(default)] - pub recovered_cargo_slot: Option, - #[serde(default)] - pub recovered_cargo_class: Option, - #[serde(default)] - pub recovered_cargo_label: Option, - #[serde(default)] - pub recovered_cargo_supplied_token_stem: Option, - #[serde(default)] - pub recovered_cargo_demanded_token_stem: Option, - #[serde(default)] - pub recovered_locomotive_id: Option, - #[serde(default)] - pub locomotive_name: Option, - #[serde(default)] - pub notes: Vec, -} - -impl RuntimeEventRecordTemplate { - pub fn into_runtime_record(self) -> RuntimeEventRecord { - RuntimeEventRecord { - record_id: self.record_id, - trigger_kind: self.trigger_kind, - active: self.active, - service_count: 0, - marks_collection_dirty: self.marks_collection_dirty, - one_shot: self.one_shot, - has_fired: false, - conditions: self.conditions, - effects: self.effects, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct RuntimeAnnualFinanceNewsEvent { - pub company_id: u32, - pub selector_label: String, - pub action_label: String, - pub retired_principal_total: u64, - pub issued_principal_total: u64, - pub repurchased_share_count: u64, - pub issued_share_count: u64, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct RuntimeServiceState { - #[serde(default)] - pub periodic_boundary_calls: u64, - #[serde(default)] - pub annual_finance_service_calls: u64, - #[serde(default)] - pub periodic_route_preference_override_apply_count: u64, - #[serde(default)] - pub periodic_route_preference_override_restore_count: u64, - #[serde(default)] - pub trigger_dispatch_counts: BTreeMap, - #[serde(default)] - pub total_event_record_services: u64, - #[serde(default)] - pub dirty_rerun_count: u64, - #[serde(default)] - pub world_issue_opinion_base_terms_raw_i32: Vec, - #[serde(default)] - pub company_market_state: BTreeMap, - #[serde(default)] - pub company_periodic_side_latch_state: BTreeMap, - #[serde(default)] - pub active_periodic_route_preference_override: Option, - #[serde(default)] - pub last_periodic_route_preference_override: Option, - #[serde(default)] - pub annual_finance_last_actions: BTreeMap, - #[serde(default)] - pub annual_finance_action_counts: BTreeMap, - #[serde(default)] - pub annual_dividend_adjustment_commit_count: u64, - #[serde(default)] - pub annual_bond_last_retired_principal_total: u64, - #[serde(default)] - pub annual_bond_last_issued_principal_total: u64, - #[serde(default)] - pub annual_stock_repurchase_last_share_count: u64, - #[serde(default)] - pub annual_stock_issue_last_share_count: u64, - #[serde(default)] - pub annual_finance_last_news_family_candidates: BTreeMap, - #[serde(default)] - pub annual_finance_last_news_events: Vec, - #[serde(default)] - pub chairman_issue_opinion_terms_raw_i32: BTreeMap>, - #[serde(default)] - pub chairman_personality_raw_u8: BTreeMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct RuntimeSaveProfileState { - #[serde(default)] - pub profile_kind: Option, - #[serde(default)] - pub profile_family: Option, - #[serde(default)] - pub map_path: Option, - #[serde(default)] - pub display_name: Option, - #[serde(default)] - pub selected_year_profile_lane: Option, - #[serde(default)] - pub sandbox_enabled: Option, - #[serde(default)] - pub campaign_scenario_enabled: Option, - #[serde(default)] - pub staged_profile_copy_on_restore: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeWorldFinanceNeighborhoodCandidate { - pub label: String, - pub relative_offset: usize, - pub relative_offset_hex: String, - pub raw_u32: u32, - pub raw_u32_hex: String, - pub value_i32: i32, - pub value_f32_text: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeCompanyStatBandCandidate { - pub label: String, - pub relative_offset: usize, - pub relative_offset_hex: String, - pub raw_u32: u32, - pub raw_u32_hex: String, - pub value_i32: i32, - pub value_f32_text: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct RuntimeWorldRestoreState { - #[serde(default)] - pub selected_year_profile_lane: Option, - #[serde(default)] - pub campaign_scenario_enabled: Option, - #[serde(default)] - pub sandbox_enabled: Option, - #[serde(default)] - pub seed_tuple_written_from_raw_lane: Option, - #[serde(default)] - pub absolute_counter_requires_shell_context: Option, - #[serde(default)] - pub absolute_counter_reconstructible_from_save: Option, - #[serde(default)] - pub current_calendar_tuple_word_raw_u32: Option, - #[serde(default)] - pub packed_year_word_raw_u16: Option, - #[serde(default)] - pub partial_year_progress_raw_u8: Option, - #[serde(default)] - pub current_calendar_tuple_word_2_raw_u32: Option, - #[serde(default)] - pub absolute_counter_raw_u32: Option, - #[serde(default)] - pub absolute_counter_mirror_raw_u32: Option, - #[serde(default)] - pub disable_cargo_economy_special_condition_slot: Option, - #[serde(default)] - pub disable_cargo_economy_special_condition_reconstructible_from_save: Option, - #[serde(default)] - pub disable_cargo_economy_special_condition_write_side_grounded: Option, - #[serde(default)] - pub disable_cargo_economy_special_condition_enabled: Option, - #[serde(default)] - pub use_bio_accelerator_cars_enabled: Option, - #[serde(default)] - pub use_wartime_cargos_enabled: Option, - #[serde(default)] - pub disable_train_crashes_enabled: Option, - #[serde(default)] - pub disable_train_crashes_and_breakdowns_enabled: Option, - #[serde(default)] - pub ai_ignore_territories_at_startup_enabled: Option, - #[serde(default)] - pub limited_track_building_amount: Option, - #[serde(default)] - pub economic_status_code: Option, - #[serde(default)] - pub territory_access_cost: Option, - #[serde(default)] - pub linked_site_removal_follow_on_gate_raw_u8: Option, - #[serde(default)] - pub linked_site_removal_follow_on_gate_enabled: Option, - #[serde(default)] - pub auto_show_grade_during_track_lay_raw_u8: Option, - #[serde(default)] - pub starting_building_density_level_raw_u8: Option, - #[serde(default)] - pub post_text_building_density_growth_raw_u8: Option, - #[serde(default)] - pub leftover_simulation_time_accumulator_raw_u32: Option, - #[serde(default)] - pub leftover_simulation_time_accumulator_value_f32_text: Option, - #[serde(default)] - pub selected_year_lane_snapshot_raw_u8: Option, - #[serde(default)] - pub all_steam_locomotives_available_raw_u8: Option, - #[serde(default)] - pub all_steam_locomotives_available_enabled: Option, - #[serde(default)] - pub all_diesel_locomotives_available_raw_u8: Option, - #[serde(default)] - pub all_diesel_locomotives_available_enabled: Option, - #[serde(default)] - pub all_electric_locomotives_available_raw_u8: Option, - #[serde(default)] - pub all_electric_locomotives_available_enabled: Option, - #[serde(default)] - pub issue_37_value: Option, - #[serde(default)] - pub issue_38_value: Option, - #[serde(default)] - pub issue_39_value: Option, - #[serde(default)] - pub issue_3a_value: Option, - #[serde(default)] - pub issue_37_multiplier_raw_u32: Option, - #[serde(default)] - pub issue_37_multiplier_value_f32_text: Option, - #[serde(default)] - pub stock_issue_and_buyback_policy_raw_u8: Option, - #[serde(default)] - pub bond_issue_and_repayment_policy_raw_u8: Option, - #[serde(default)] - pub bankruptcy_policy_raw_u8: Option, - #[serde(default)] - pub dividend_policy_raw_u8: Option, - #[serde(default)] - pub building_density_growth_setting_raw_u32: Option, - #[serde(default)] - pub stock_issue_and_buyback_allowed: Option, - #[serde(default)] - pub bond_issue_and_repayment_allowed: Option, - #[serde(default)] - pub bankruptcy_allowed: Option, - #[serde(default)] - pub dividend_adjustment_allowed: Option, - #[serde(default)] - pub finance_neighborhood_candidates: Vec, - #[serde(default)] - pub economic_tuning_mirror_raw_u32: Option, - #[serde(default)] - pub economic_tuning_mirror_value_f32_text: Option, - #[serde(default)] - pub economic_tuning_lane_raw_u32: Vec, - #[serde(default)] - pub economic_tuning_lane_value_f32_text: Vec, - #[serde(default)] - pub cached_available_locomotive_rating_raw_u32: Option, - #[serde(default)] - pub cached_available_locomotive_rating_value_f32_text: Option, - #[serde(default)] - pub selected_year_bucket_scalar_raw_u32: Option, - #[serde(default)] - pub selected_year_bucket_scalar_value_f32_text: Option, - #[serde(default)] - pub selected_year_bucket_direct_lane_raw_u32: Vec, - #[serde(default)] - pub selected_year_bucket_direct_lane_value_f32_text: Vec, - #[serde(default)] - pub selected_year_bucket_complement_lane_raw_u32: Vec, - #[serde(default)] - pub selected_year_bucket_complement_lane_value_f32_text: Vec, - #[serde(default)] - pub selected_year_bucket_scaled_companion_lane_raw_u32: Vec, - #[serde(default)] - pub selected_year_bucket_scaled_companion_lane_value_f32_text: Vec, - #[serde(default)] - pub selected_year_gap_scalar_raw_u32: Option, - #[serde(default)] - pub selected_year_gap_scalar_value_f32_text: Option, - #[serde(default)] - pub absolute_counter_restore_kind: Option, - #[serde(default)] - pub absolute_counter_adjustment_context: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeState { - pub calendar: CalendarPoint, - #[serde(default)] - pub world_flags: BTreeMap, - #[serde(default)] - pub save_profile: RuntimeSaveProfileState, - #[serde(default)] - pub world_restore: RuntimeWorldRestoreState, - #[serde(default)] - pub metadata: BTreeMap, - #[serde(default)] - pub companies: Vec, - #[serde(default)] - pub selected_company_id: Option, - #[serde(default)] - pub players: Vec, - #[serde(default)] - pub selected_player_id: Option, - #[serde(default)] - pub chairman_profiles: Vec, - #[serde(default)] - pub selected_chairman_profile_id: Option, - #[serde(default)] - pub trains: Vec, - #[serde(default)] - pub locomotive_catalog: Vec, - #[serde(default)] - pub cargo_catalog: Vec, - #[serde(default)] - pub territories: Vec, - #[serde(default)] - pub company_territory_track_piece_counts: Vec, - #[serde(default)] - pub company_territory_access: Vec, - #[serde(default)] - pub packed_event_collection: Option, - #[serde(default)] - pub event_runtime_records: Vec, - #[serde(default)] - pub candidate_availability: BTreeMap, - #[serde(default)] - pub named_locomotive_availability: BTreeMap, - #[serde(default)] - pub named_locomotive_cost: BTreeMap, - #[serde(default)] - pub all_cargo_price_override: Option, - #[serde(default)] - pub named_cargo_price_overrides: BTreeMap, - #[serde(default)] - pub all_cargo_production_override: Option, - #[serde(default)] - pub factory_cargo_production_override: Option, - #[serde(default)] - pub farm_mine_cargo_production_override: Option, - #[serde(default)] - pub named_cargo_production_overrides: BTreeMap, - #[serde(default)] - pub cargo_production_overrides: BTreeMap, - #[serde(default)] - pub world_runtime_variables: BTreeMap, - #[serde(default)] - pub company_runtime_variables: BTreeMap>, - #[serde(default)] - pub player_runtime_variables: BTreeMap>, - #[serde(default)] - pub territory_runtime_variables: BTreeMap>, - #[serde(default)] - pub world_scalar_overrides: BTreeMap, - #[serde(default)] - pub special_conditions: BTreeMap, - #[serde(default)] - pub service_state: RuntimeServiceState, -} - -impl RuntimeState { - pub fn validate(&self) -> Result<(), String> { - self.calendar.validate()?; - - let mut seen_company_ids = BTreeSet::new(); - let mut active_company_ids = BTreeSet::new(); - for company in &self.companies { - if !seen_company_ids.insert(company.company_id) { - return Err(format!("duplicate company_id {}", company.company_id)); - } - if company.active { - active_company_ids.insert(company.company_id); - } - } - if let Some(selected_company_id) = self.selected_company_id { - if !seen_company_ids.contains(&selected_company_id) { - return Err(format!( - "selected_company_id {} does not reference a live company", - selected_company_id - )); - } - if !active_company_ids.contains(&selected_company_id) { - return Err(format!( - "selected_company_id {} must reference an active company", - selected_company_id - )); - } - } - - let mut seen_player_ids = BTreeSet::new(); - let mut active_player_ids = BTreeSet::new(); - for player in &self.players { - if !seen_player_ids.insert(player.player_id) { - return Err(format!("duplicate player_id {}", player.player_id)); - } - if player.active { - active_player_ids.insert(player.player_id); - } - } - - let mut seen_chairman_profile_ids = BTreeSet::new(); - let mut seen_chairman_names = BTreeSet::new(); - let mut active_chairman_profile_ids = BTreeSet::new(); - for chairman in &self.chairman_profiles { - if !seen_chairman_profile_ids.insert(chairman.profile_id) { - return Err(format!( - "duplicate chairman_profile.profile_id {}", - chairman.profile_id - )); - } - if chairman.name.trim().is_empty() { - return Err(format!( - "chairman_profile {} has an empty name", - chairman.profile_id - )); - } - if !seen_chairman_names.insert(chairman.name.clone()) { - return Err(format!( - "duplicate chairman_profile.name {:?}", - chairman.name - )); - } - if chairman.active { - active_chairman_profile_ids.insert(chairman.profile_id); - } - if let Some(linked_company_id) = chairman.linked_company_id { - if !seen_company_ids.contains(&linked_company_id) { - return Err(format!( - "chairman_profile {} references unknown linked_company_id {}", - chairman.profile_id, linked_company_id - )); - } - } - for company_id in chairman.company_holdings.keys() { - if !seen_company_ids.contains(company_id) { - return Err(format!( - "chairman_profile {} references unknown holdings company_id {}", - chairman.profile_id, company_id - )); - } - } - } - if let Some(selected_chairman_profile_id) = self.selected_chairman_profile_id { - if !seen_chairman_profile_ids.contains(&selected_chairman_profile_id) { - return Err(format!( - "selected_chairman_profile_id {} does not reference a live chairman profile", - selected_chairman_profile_id - )); - } - if !active_chairman_profile_ids.contains(&selected_chairman_profile_id) { - return Err(format!( - "selected_chairman_profile_id {} must reference an active chairman profile", - selected_chairman_profile_id - )); - } - } - for company in &self.companies { - if let Some(linked_chairman_profile_id) = company.linked_chairman_profile_id { - let linked_profile = self - .chairman_profiles - .iter() - .find(|profile| profile.profile_id == linked_chairman_profile_id) - .ok_or_else(|| { - format!( - "company {} references unknown linked_chairman_profile_id {}", - company.company_id, linked_chairman_profile_id - ) - })?; - if linked_profile.linked_company_id != Some(company.company_id) { - return Err(format!( - "company {} linked_chairman_profile_id {} must point back through linked_company_id", - company.company_id, linked_chairman_profile_id - )); - } - } - } - for chairman in &self.chairman_profiles { - if let Some(linked_company_id) = chairman.linked_company_id { - let linked_company = self - .companies - .iter() - .find(|company| company.company_id == linked_company_id) - .ok_or_else(|| { - format!( - "chairman_profile {} references unknown linked_company_id {}", - chairman.profile_id, linked_company_id - ) - })?; - if linked_company.linked_chairman_profile_id != Some(chairman.profile_id) { - return Err(format!( - "chairman_profile {} linked_company_id {} must point back through linked_chairman_profile_id", - chairman.profile_id, linked_company_id - )); - } - } - } - if let Some(selected_player_id) = self.selected_player_id { - if !seen_player_ids.contains(&selected_player_id) { - return Err(format!( - "selected_player_id {} does not reference a live player", - selected_player_id - )); - } - if !active_player_ids.contains(&selected_player_id) { - return Err(format!( - "selected_player_id {} must reference an active player", - selected_player_id - )); - } - } - let mut seen_territory_ids = BTreeSet::new(); - let mut seen_territory_names = BTreeSet::new(); - for territory in &self.territories { - if !seen_territory_ids.insert(territory.territory_id) { - return Err(format!("duplicate territory_id {}", territory.territory_id)); - } - if let Some(name) = territory.name.as_deref() { - if name.trim().is_empty() { - return Err(format!( - "territory_id {} has an empty name", - territory.territory_id - )); - } - if !seen_territory_names.insert(name.to_string()) { - return Err(format!("duplicate territory name {name:?}")); - } - } - } - let mut seen_train_ids = BTreeSet::new(); - for train in &self.trains { - if !seen_train_ids.insert(train.train_id) { - return Err(format!("duplicate train_id {}", train.train_id)); - } - if !seen_company_ids.contains(&train.owner_company_id) { - return Err(format!( - "train_id {} references unknown owner_company_id {}", - train.train_id, train.owner_company_id - )); - } - if let Some(territory_id) = train.territory_id { - if !seen_territory_ids.contains(&territory_id) { - return Err(format!( - "train_id {} references unknown territory_id {}", - train.train_id, territory_id - )); - } - } - if train.retired && train.active { - return Err(format!( - "train_id {} cannot be active and retired at the same time", - train.train_id - )); - } - if train - .locomotive_name - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(format!( - "train_id {} has an empty locomotive_name", - train.train_id - )); - } - } - let mut seen_locomotive_ids = BTreeSet::new(); - let mut seen_locomotive_names = BTreeSet::new(); - for entry in &self.locomotive_catalog { - if !seen_locomotive_ids.insert(entry.locomotive_id) { - return Err(format!( - "duplicate locomotive_catalog.locomotive_id {}", - entry.locomotive_id - )); - } - if entry.name.trim().is_empty() { - return Err(format!( - "locomotive_catalog entry {} has an empty name", - entry.locomotive_id - )); - } - if !seen_locomotive_names.insert(entry.name.clone()) { - return Err(format!( - "duplicate locomotive_catalog.name {:?}", - entry.name - )); - } - } - let mut seen_cargo_slots = BTreeSet::new(); - let mut seen_cargo_labels = BTreeSet::new(); - for entry in &self.cargo_catalog { - if !(1..=11).contains(&entry.slot_id) { - return Err(format!( - "cargo_catalog entry has out-of-range slot_id {}", - entry.slot_id - )); - } - if !seen_cargo_slots.insert(entry.slot_id) { - return Err(format!("duplicate cargo_catalog.slot_id {}", entry.slot_id)); - } - if entry.label.trim().is_empty() { - return Err(format!( - "cargo_catalog entry {} has an empty label", - entry.slot_id - )); - } - if !seen_cargo_labels.insert(entry.label.clone()) { - return Err(format!("duplicate cargo_catalog.label {:?}", entry.label)); - } - } - for entry in &self.company_territory_track_piece_counts { - if !seen_company_ids.contains(&entry.company_id) { - return Err(format!( - "company_territory_track_piece_counts references unknown company_id {}", - entry.company_id - )); - } - if !seen_territory_ids.contains(&entry.territory_id) { - return Err(format!( - "company_territory_track_piece_counts references unknown territory_id {}", - entry.territory_id - )); - } - } - let mut seen_company_territory_access = BTreeSet::new(); - for entry in &self.company_territory_access { - if !seen_company_ids.contains(&entry.company_id) { - return Err(format!( - "company_territory_access references unknown company_id {}", - entry.company_id - )); - } - if !seen_territory_ids.contains(&entry.territory_id) { - return Err(format!( - "company_territory_access references unknown territory_id {}", - entry.territory_id - )); - } - if !seen_company_territory_access.insert((entry.company_id, entry.territory_id)) { - return Err(format!( - "duplicate company_territory_access pair ({}, {})", - entry.company_id, entry.territory_id - )); - } - } - - let mut seen_record_ids = BTreeSet::new(); - for record in &self.event_runtime_records { - if !seen_record_ids.insert(record.record_id) { - return Err(format!("duplicate record_id {}", record.record_id)); - } - for (condition_index, condition) in record.conditions.iter().enumerate() { - validate_runtime_condition( - condition, - &seen_company_ids, - &seen_player_ids, - &seen_chairman_profile_ids, - &seen_territory_ids, - ) - .map_err(|err| { - format!( - "event_runtime_records[record_id={}].conditions[{condition_index}] {err}", - record.record_id - ) - })?; - } - for (effect_index, effect) in record.effects.iter().enumerate() { - validate_runtime_effect( - effect, - &seen_company_ids, - &seen_player_ids, - &seen_chairman_profile_ids, - &seen_territory_ids, - ) - .map_err(|err| { - format!( - "event_runtime_records[record_id={}].effects[{effect_index}] {err}", - record.record_id - ) - })?; - } - } - - if let Some(summary) = &self.packed_event_collection { - if summary.source_kind.trim().is_empty() { - return Err("packed_event_collection.source_kind must not be empty".to_string()); - } - if summary.mechanism_family.trim().is_empty() { - return Err( - "packed_event_collection.mechanism_family must not be empty".to_string() - ); - } - if summary.mechanism_confidence.trim().is_empty() { - return Err( - "packed_event_collection.mechanism_confidence must not be empty".to_string(), - ); - } - if summary - .container_profile_family - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err( - "packed_event_collection.container_profile_family must not be empty" - .to_string(), - ); - } - if summary.packed_state_version_hex.trim().is_empty() { - return Err( - "packed_event_collection.packed_state_version_hex must not be empty" - .to_string(), - ); - } - if summary.live_record_count != summary.live_entry_ids.len() { - return Err( - "packed_event_collection.live_record_count must match live_entry_ids length" - .to_string(), - ); - } - if summary.live_record_count != summary.records.len() { - return Err( - "packed_event_collection.live_record_count must match records length" - .to_string(), - ); - } - let decoded_record_count = summary - .records - .iter() - .filter(|record| record.decode_status != "unsupported_framing") - .count(); - if summary.decoded_record_count != decoded_record_count { - return Err( - "packed_event_collection.decoded_record_count must match decoded records" - .to_string(), - ); - } - let importable_or_imported_count = summary - .records - .iter() - .filter(|record| { - record.executable_import_ready - || record.import_outcome.as_deref() == Some("imported") - }) - .count(); - if summary.imported_runtime_record_count > importable_or_imported_count { - return Err( - "packed_event_collection.imported_runtime_record_count must not exceed importable or imported records" - .to_string(), - ); - } - - let mut previous_id = None; - for (record_index, entry_id) in summary.live_entry_ids.iter().enumerate() { - if *entry_id == 0 { - return Err( - "packed_event_collection.live_entry_ids must not contain id 0".to_string(), - ); - } - if *entry_id > summary.live_id_bound { - return Err(format!( - "packed_event_collection.live_entry_id {} exceeds live_id_bound {}", - entry_id, summary.live_id_bound - )); - } - if previous_id.is_some_and(|prior| prior >= *entry_id) { - return Err( - "packed_event_collection.live_entry_ids must be strictly ascending" - .to_string(), - ); - } - previous_id = Some(*entry_id); - - let record = &summary.records[record_index]; - if record.live_entry_id != *entry_id { - return Err(format!( - "packed_event_collection.records[{record_index}].live_entry_id must match live_entry_ids" - )); - } - if record.record_index != record_index { - return Err(format!( - "packed_event_collection.records[{record_index}].record_index must match position" - )); - } - if record.decode_status.trim().is_empty() { - return Err(format!( - "packed_event_collection.records[{record_index}].decode_status must not be empty" - )); - } - if record.payload_family.trim().is_empty() { - return Err(format!( - "packed_event_collection.records[{record_index}].payload_family must not be empty" - )); - } - if record - .import_outcome - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(format!( - "packed_event_collection.records[{record_index}].import_outcome must not be empty" - )); - } - if record.grouped_effect_row_counts.len() != 4 { - return Err(format!( - "packed_event_collection.records[{record_index}].grouped_effect_row_counts must contain exactly 4 entries" - )); - } - if record.payload_family == "real_packed_v1" - && record.standalone_condition_rows.len() - != record.standalone_condition_row_count - { - return Err(format!( - "packed_event_collection.records[{record_index}].standalone_condition_rows must match standalone_condition_row_count" - )); - } - if record.payload_family == "real_packed_v1" - && record.grouped_effect_rows.len() - != record.grouped_effect_row_counts.iter().sum::() - { - return Err(format!( - "packed_event_collection.records[{record_index}].grouped_effect_rows must match grouped_effect_row_counts" - )); - } - for band in &record.text_bands { - if band.label.trim().is_empty() { - return Err(format!( - "packed_event_collection.records[{record_index}].text_bands contains an empty label" - )); - } - } - if let Some(control) = &record.compact_control { - if control.grouped_target_scope_ordinals_0x7fb.len() != 4 { - return Err(format!( - "packed_event_collection.records[{record_index}].compact_control.grouped_target_scope_ordinals_0x7fb must contain exactly 4 entries" - )); - } - if control.grouped_scope_checkboxes_0x7ff.len() != 4 { - return Err(format!( - "packed_event_collection.records[{record_index}].compact_control.grouped_scope_checkboxes_0x7ff must contain exactly 4 entries" - )); - } - if control.grouped_territory_selectors_0x80f.len() != 4 { - return Err(format!( - "packed_event_collection.records[{record_index}].compact_control.grouped_territory_selectors_0x80f must contain exactly 4 entries" - )); - } - } - for row in &record.standalone_condition_rows { - if row - .candidate_name - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(format!( - "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty candidate_name" - )); - } - if row - .comparator - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(format!( - "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty comparator" - )); - } - if row - .metric - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(format!( - "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty metric" - )); - } - if row - .semantic_family - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(format!( - "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty semantic_family" - )); - } - if row - .semantic_preview - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(format!( - "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty semantic_preview" - )); - } - } - for row in &record.grouped_effect_rows { - if row.row_shape.trim().is_empty() { - return Err(format!( - "packed_event_collection.records[{record_index}].grouped_effect_rows contains an empty row_shape" - )); - } - if row - .locomotive_name - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(format!( - "packed_event_collection.records[{record_index}].grouped_effect_rows contains an empty locomotive_name" - )); - } - } - } - } - - for key in self.world_flags.keys() { - if key.trim().is_empty() { - return Err("world_flags contains an empty key".to_string()); - } - } - - for (label, value) in [ - ( - "save_profile.profile_kind", - self.save_profile.profile_kind.as_deref(), - ), - ( - "save_profile.profile_family", - self.save_profile.profile_family.as_deref(), - ), - ( - "save_profile.map_path", - self.save_profile.map_path.as_deref(), - ), - ( - "save_profile.display_name", - self.save_profile.display_name.as_deref(), - ), - ] { - if value.is_some_and(|text| text.trim().is_empty()) { - return Err(format!("{label} must not be empty")); - } - } - - if self.world_restore.selected_year_profile_lane.is_none() - && (self.world_restore.campaign_scenario_enabled.is_some() - || self.world_restore.sandbox_enabled.is_some()) - { - return Err( - "world_restore.selected_year_profile_lane must be present when world restore flags are populated" - .to_string(), - ); - } - - if self - .world_restore - .absolute_counter_restore_kind - .as_deref() - .is_some_and(|text| text.trim().is_empty()) - { - return Err( - "world_restore.absolute_counter_restore_kind must not be empty".to_string(), - ); - } - if self - .world_restore - .absolute_counter_adjustment_context - .as_deref() - .is_some_and(|text| text.trim().is_empty()) - { - return Err( - "world_restore.absolute_counter_adjustment_context must not be empty".to_string(), - ); - } - for (key, value) in &self.metadata { - if key.trim().is_empty() { - return Err("metadata contains an empty key".to_string()); - } - if value.trim().is_empty() { - return Err(format!("metadata[{key}] must not be empty")); - } - } - - for key in self.candidate_availability.keys() { - if key.trim().is_empty() { - return Err("candidate_availability contains an empty key".to_string()); - } - } - for key in self.named_locomotive_availability.keys() { - if key.trim().is_empty() { - return Err("named_locomotive_availability contains an empty key".to_string()); - } - } - for key in self.named_locomotive_cost.keys() { - if key.trim().is_empty() { - return Err("named_locomotive_cost contains an empty key".to_string()); - } - } - for key in self.named_cargo_price_overrides.keys() { - if key.trim().is_empty() { - return Err("named_cargo_price_overrides contains an empty key".to_string()); - } - } - for key in self.named_cargo_production_overrides.keys() { - if key.trim().is_empty() { - return Err("named_cargo_production_overrides contains an empty key".to_string()); - } - } - for slot in self.cargo_production_overrides.keys() { - if !(1..=11).contains(slot) { - return Err(format!( - "cargo_production_overrides contains out-of-range slot {}", - slot - )); - } - } - for index in self.world_runtime_variables.keys() { - if !(1..=4).contains(index) { - return Err(format!( - "world_runtime_variables contains out-of-range index {}", - index - )); - } - } - for (company_id, vars) in &self.company_runtime_variables { - if !seen_company_ids.contains(company_id) { - return Err(format!( - "company_runtime_variables references unknown company_id {}", - company_id - )); - } - for index in vars.keys() { - if !(1..=4).contains(index) { - return Err(format!( - "company_runtime_variables[{company_id}] contains out-of-range index {}", - index - )); - } - } - } - for company_id in self.service_state.company_market_state.keys() { - if !seen_company_ids.contains(company_id) { - return Err(format!( - "service_state.company_market_state references unknown company_id {}", - company_id - )); - } - } - for company_id in self.service_state.company_periodic_side_latch_state.keys() { - if !seen_company_ids.contains(company_id) { - return Err(format!( - "service_state.company_periodic_side_latch_state references unknown company_id {}", - company_id - )); - } - } - if let Some(override_state) = &self.service_state.active_periodic_route_preference_override - { - if !seen_company_ids.contains(&override_state.company_id) { - return Err(format!( - "service_state.active_periodic_route_preference_override references unknown company_id {}", - override_state.company_id - )); - } - } - if let Some(override_state) = &self.service_state.last_periodic_route_preference_override { - if !seen_company_ids.contains(&override_state.company_id) { - return Err(format!( - "service_state.last_periodic_route_preference_override references unknown company_id {}", - override_state.company_id - )); - } - } - for company_id in self.service_state.annual_finance_last_actions.keys() { - if !seen_company_ids.contains(company_id) { - return Err(format!( - "service_state.annual_finance_last_actions references unknown company_id {}", - company_id - )); - } - } - for company_id in self - .service_state - .annual_finance_last_news_family_candidates - .keys() - { - if !seen_company_ids.contains(company_id) { - return Err(format!( - "service_state.annual_finance_last_news_family_candidates references unknown company_id {}", - company_id - )); - } - } - for news_event in &self.service_state.annual_finance_last_news_events { - if !seen_company_ids.contains(&news_event.company_id) { - return Err(format!( - "service_state.annual_finance_last_news_events references unknown company_id {}", - news_event.company_id - )); - } - } - for chairman_profile_id in self - .service_state - .chairman_issue_opinion_terms_raw_i32 - .keys() - { - if !seen_chairman_profile_ids.contains(chairman_profile_id) { - return Err(format!( - "service_state.chairman_issue_opinion_terms_raw_i32 references unknown chairman_profile_id {}", - chairman_profile_id - )); - } - } - for chairman_profile_id in self.service_state.chairman_personality_raw_u8.keys() { - if !seen_chairman_profile_ids.contains(chairman_profile_id) { - return Err(format!( - "service_state.chairman_personality_raw_u8 references unknown chairman_profile_id {}", - chairman_profile_id - )); - } - } - for (player_id, vars) in &self.player_runtime_variables { - if !seen_player_ids.contains(player_id) { - return Err(format!( - "player_runtime_variables references unknown player_id {}", - player_id - )); - } - for index in vars.keys() { - if !(1..=4).contains(index) { - return Err(format!( - "player_runtime_variables[{player_id}] contains out-of-range index {}", - index - )); - } - } - } - for (territory_id, vars) in &self.territory_runtime_variables { - if !seen_territory_ids.contains(territory_id) { - return Err(format!( - "territory_runtime_variables references unknown territory_id {}", - territory_id - )); - } - for index in vars.keys() { - if !(1..=4).contains(index) { - return Err(format!( - "territory_runtime_variables[{territory_id}] contains out-of-range index {}", - index - )); - } - } - } - for key in self.world_scalar_overrides.keys() { - if key.trim().is_empty() { - return Err("world_scalar_overrides contains an empty key".to_string()); - } - } - for key in self.special_conditions.keys() { - if key.trim().is_empty() { - return Err("special_conditions contains an empty key".to_string()); - } - } - - Ok(()) - } - - pub fn refresh_derived_market_state(&mut self) { - let company_share_prices = self - .service_state - .company_market_state - .iter() - .filter_map(|(company_id, market_state)| { - rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32) - .map(|share_price| (*company_id, share_price)) - }) - .collect::>(); - - let company_refresh = self - .companies - .iter() - .map(|company| { - let current_cash = runtime_company_control_transfer_stat_value_f64( - self, - company.company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - ) - .and_then(runtime_round_f64_to_i64); - let book_value_per_share = - runtime_company_direct_float_field_value_f64(self, company.company_id, 0x32f) - .and_then(runtime_round_f64_to_i64); - let prime_rate = runtime_company_prime_rate(self, company.company_id); - let credit_rating_score = runtime_company_credit_rating(self, company.company_id); - let investor_confidence = - runtime_company_investor_confidence(self, company.company_id); - let management_attitude = - runtime_company_management_attitude(self, company.company_id); - ( - company.company_id, - current_cash, - book_value_per_share, - credit_rating_score, - prime_rate, - investor_confidence, - management_attitude, - ) - }) - .collect::>(); - - for company in &mut self.companies { - if let Some(( - _, - current_cash, - book_value_per_share, - credit_rating_score, - prime_rate, - investor_confidence, - management_attitude, - )) = company_refresh - .iter() - .find(|(company_id, _, _, _, _, _, _)| *company_id == company.company_id) - { - if let Some(current_cash) = current_cash { - company.current_cash = *current_cash; - } - if let Some(book_value_per_share) = book_value_per_share { - company.book_value_per_share = *book_value_per_share; - } - if let Some(credit_rating_score) = credit_rating_score { - company.credit_rating_score = Some(*credit_rating_score); - } - if let Some(prime_rate) = prime_rate { - company.prime_rate = Some(*prime_rate); - } - if let Some(investor_confidence) = investor_confidence { - company.investor_confidence = *investor_confidence; - } - if let Some(management_attitude) = management_attitude { - company.management_attitude = *management_attitude; - } - } - } - - let known_company_ids = self - .companies - .iter() - .map(|company| company.company_id) - .collect::>(); - self.service_state - .company_periodic_side_latch_state - .retain(|company_id, _| known_company_ids.contains(company_id)); - for (company_id, market_state) in &self.service_state.company_market_state { - self.service_state - .company_periodic_side_latch_state - .entry(*company_id) - .or_insert_with(|| RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: None, - city_connection_latch: market_state.city_connection_latch, - linked_transit_latch: market_state.linked_transit_latch, - }); - } - - for profile in &mut self.chairman_profiles { - let preserved_threshold_adjusted_holdings_component = profile - .purchasing_power_total - .saturating_sub(profile.current_cash) - .max(0); - if let Some(holdings_value_total) = derive_runtime_chairman_holdings_share_price_total( - &profile.company_holdings, - &company_share_prices, - ) { - profile.holdings_value_total = holdings_value_total; - } - profile.net_worth_total = profile - .current_cash - .saturating_add(profile.holdings_value_total); - profile.purchasing_power_total = profile - .current_cash - .saturating_add(preserved_threshold_adjusted_holdings_component) - .max(profile.net_worth_total); - } - } - - pub fn refresh_derived_world_state(&mut self) { - self.world_restore - .linked_site_removal_follow_on_gate_enabled = self - .world_restore - .linked_site_removal_follow_on_gate_raw_u8 - .map(|raw| raw != 0); - self.world_restore.all_steam_locomotives_available_enabled = self - .world_restore - .all_steam_locomotives_available_raw_u8 - .map(|raw| raw != 0); - self.world_restore.all_diesel_locomotives_available_enabled = self - .world_restore - .all_diesel_locomotives_available_raw_u8 - .map(|raw| raw != 0); - self.world_restore - .all_electric_locomotives_available_enabled = self - .world_restore - .all_electric_locomotives_available_raw_u8 - .map(|raw| raw != 0); - if let Some(&lane_0_raw_u32) = self.world_restore.economic_tuning_lane_raw_u32.first() { - self.world_restore.economic_tuning_mirror_raw_u32 = Some(lane_0_raw_u32); - self.world_restore.economic_tuning_mirror_value_f32_text = - Some(format!("{:.6}", f32::from_bits(lane_0_raw_u32))); - } - let year_word = self - .world_restore - .packed_year_word_raw_u16 - .map(u32::from) - .or_else(|| { - self.world_restore - .current_calendar_tuple_word_raw_u32 - .zip(self.world_restore.current_calendar_tuple_word_2_raw_u32) - .map(|(word_0, word_1)| { - u32::from(runtime_decode_packed_calendar_tuple(word_0, word_1).year_word) - }) - }) - .unwrap_or(self.calendar.year); - if let Some(value) = runtime_world_selected_year_bucket_scalar_from_year_word(year_word) { - self.world_restore.selected_year_bucket_scalar_raw_u32 = Some(value.to_bits()); - self.world_restore - .selected_year_bucket_scalar_value_f32_text = Some(format!("{value:.6}")); - if let Some(bands) = runtime_selected_year_bucket_bands_from_scalar(value) { - self.world_restore.selected_year_bucket_direct_lane_raw_u32 = - bands.direct.iter().map(|lane| lane.to_bits()).collect(); - self.world_restore - .selected_year_bucket_direct_lane_value_f32_text = bands - .direct - .iter() - .map(|lane| format!("{lane:.6}")) - .collect(); - self.world_restore - .selected_year_bucket_complement_lane_raw_u32 = - bands.complement.iter().map(|lane| lane.to_bits()).collect(); - self.world_restore - .selected_year_bucket_complement_lane_value_f32_text = bands - .complement - .iter() - .map(|lane| format!("{lane:.6}")) - .collect(); - self.world_restore - .selected_year_bucket_scaled_companion_lane_raw_u32 = bands - .scaled_companion - .iter() - .map(|lane| lane.to_bits()) - .collect(); - self.world_restore - .selected_year_bucket_scaled_companion_lane_value_f32_text = bands - .scaled_companion - .iter() - .map(|lane| format!("{lane:.6}")) - .collect(); - } - } - if let Some(value) = runtime_world_selected_year_gap_scalar_from_year_word(year_word) { - self.world_restore.selected_year_gap_scalar_raw_u32 = Some(value.to_bits()); - self.world_restore.selected_year_gap_scalar_value_f32_text = - Some(format!("{value:.6}")); - } - for (key, value) in [ - ( - "world.all_steam_locos_available", - self.world_restore.all_steam_locomotives_available_enabled, - ), - ( - "world.all_diesel_locos_available", - self.world_restore.all_diesel_locomotives_available_enabled, - ), - ( - "world.all_electric_locos_available", - self.world_restore - .all_electric_locomotives_available_enabled, - ), - ] { - if let Some(enabled) = value { - self.world_flags.insert(key.to_string(), enabled); - } else { - self.world_flags.remove(key); - } - } - } -} - -#[derive(Debug, Clone, Deserialize)] -struct CheckedInSelectedYearBucketLadderArtifact { - direct_lane_multipliers: Vec, - complement_formula: CheckedInSelectedYearBucketComplementFormula, - scaled_companion_formula: CheckedInSelectedYearBucketScaledCompanionFormula, - entries: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -struct CheckedInSelectedYearBucketLadderEntry { - year: u32, - value: f32, -} - -#[derive(Debug, Clone, Deserialize)] -struct CheckedInSelectedYearBucketComplementFormula { - divisor: f32, - multiplier: f32, - bias: f32, - scale: f32, - floor: f32, - build_106_multiplier: f32, - cap: f32, -} - -#[derive(Debug, Clone, Deserialize)] -struct CheckedInSelectedYearBucketScaledCompanionFormula { - numerator: f32, - multiplier: f32, - bias: f32, - scale: f32, -} - -#[derive(Debug, Clone)] -struct RuntimeSelectedYearBucketBands { - direct: [f32; 3], - complement: [f32; 3], - scaled_companion: [f32; 3], -} - -fn checked_in_selected_year_bucket_ladder() -> &'static CheckedInSelectedYearBucketLadderArtifact { - static LADDER: OnceLock = OnceLock::new(); - LADDER.get_or_init(|| { - serde_json::from_str::(include_str!( - "../../../artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json" - )) - .expect("checked-in selected-year bucket ladder should parse") - }) -} - -pub fn runtime_world_selected_year_bucket_scalar_from_year_word(year_word: u32) -> Option { - let ladder = &checked_in_selected_year_bucket_ladder().entries; - if ladder.is_empty() { - return None; - } - if year_word <= ladder[0].year { - return Some(ladder[0].value); - } - for window in ladder.windows(2) { - let start = &window[0]; - let end = &window[1]; - if year_word <= end.year { - if year_word <= start.year || end.year == start.year { - return Some(start.value); - } - let span = (end.year - start.year) as f32; - let progress = (year_word - start.year) as f32 / span; - return Some(start.value + (end.value - start.value) * progress); - } - } - ladder.last().map(|entry| entry.value) -} - -fn runtime_selected_year_bucket_bands_from_scalar( - scalar: f32, -) -> Option { - let artifact = checked_in_selected_year_bucket_ladder(); - if artifact.direct_lane_multipliers.len() != 3 { - return None; - } - let direct = [ - scalar * artifact.direct_lane_multipliers[0], - scalar * artifact.direct_lane_multipliers[1], - scalar * artifact.direct_lane_multipliers[2], - ]; - let mut complement = [0.0f32; 3]; - let mut scaled_companion = [0.0f32; 3]; - for (index, direct_value) in direct.iter().copied().enumerate() { - let mut x = (((direct_value / artifact.complement_formula.divisor) - * artifact.complement_formula.multiplier) - + artifact.complement_formula.bias) - * artifact.complement_formula.scale; - x = x.max(artifact.complement_formula.floor); - x *= artifact.complement_formula.build_106_multiplier; - x = x.min(artifact.complement_formula.cap); - complement[index] = 1.0 - x; - scaled_companion[index] = (((artifact.scaled_companion_formula.numerator / direct_value) - * artifact.scaled_companion_formula.multiplier) - + artifact.scaled_companion_formula.bias) - * artifact.scaled_companion_formula.scale; - } - Some(RuntimeSelectedYearBucketBands { - direct, - complement, - scaled_companion, - }) -} - -pub fn runtime_world_selected_year_gap_scalar_from_year_word(year_word: u32) -> Option { - let normalized = (year_word as f64 - 1850.0) / 150.0; - if !normalized.is_finite() { - return None; - } - Some(normalized.clamp(1.0 / 3.0, 1.0) as f32) -} - -pub fn runtime_company_stat_value( - state: &RuntimeState, - company_id: u32, - selector: RuntimeCompanyStatSelector, -) -> Option { - runtime_company_stat_value_f64(state, company_id, selector).and_then(runtime_round_f64_to_i64) -} - -pub fn runtime_company_stat_value_f64( - state: &RuntimeState, - company_id: u32, - selector: RuntimeCompanyStatSelector, -) -> Option { - if selector.slot_id >= RUNTIME_COMPANY_STAT_SLOT_COUNT { - return runtime_company_derived_stat_value_f64( - state, - company_id, - selector.family_id, - selector.slot_id, - ); - } - match selector.family_id { - RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER => { - runtime_company_control_transfer_stat_value_f64(state, company_id, selector.slot_id) - } - RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A => { - runtime_company_special_stat_family_232a_value_f64(state, company_id, selector.slot_id) - } - family_id => { - runtime_company_year_stat_value_f64(state, company_id, family_id, selector.slot_id) - } - } -} - -fn runtime_company_current_stat_value_f64( - state: &RuntimeState, - company_id: u32, - slot_id: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - let index = slot_id.checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? as usize; - runtime_decode_saved_f64_bits(*market_state.year_stat_family_qword_bits.get(index)?) -} - -fn runtime_company_direct_float_field_value_f64( - state: &RuntimeState, - company_id: u32, - field_offset: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - let raw_u32 = *market_state - .direct_control_transfer_float_fields_raw_u32 - .get(&field_offset)?; - let value = f32::from_bits(raw_u32) as f64; - if !value.is_finite() { - return None; - } - Some(value) -} - -fn runtime_company_direct_i32_field_value_f64( - state: &RuntimeState, - company_id: u32, - field_offset: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - Some(i64::from( - *market_state - .direct_control_transfer_int_fields_raw_u32 - .get(&field_offset)? as i32, - ) as f64) -} - -pub fn runtime_company_book_value_per_share(state: &RuntimeState, company_id: u32) -> Option { - let company = state - .companies - .iter() - .find(|company| company.company_id == company_id)?; - if company.book_value_per_share != 0 { - return Some(company.book_value_per_share); - } - runtime_company_direct_float_field_value_f64(state, company_id, 0x32f) - .and_then(runtime_round_f64_to_i64) -} - -fn runtime_decode_saved_f32_value_f64(raw_u32: u32) -> Option { - let value = f32::from_bits(raw_u32) as f64; - if !value.is_finite() { - return None; - } - Some(value) -} - -fn runtime_company_highest_live_bond_coupon_rate_f64( - state: &RuntimeState, - company_id: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - market_state - .live_bond_slots - .iter() - .filter_map(|slot| { - let value = f32::from_bits(slot.coupon_rate_raw_u32) as f64; - value.is_finite().then_some(value) - }) - .max_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal)) -} - -fn runtime_company_total_live_bond_principal(state: &RuntimeState, company_id: u32) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - Some( - market_state - .live_bond_slots - .iter() - .map(|slot| slot.principal) - .sum(), - ) -} - -fn runtime_company_matured_live_bond_count( - state: &RuntimeState, - company_id: u32, - current_year_word: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - Some( - market_state - .live_bond_slots - .iter() - .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) - .count() as u32, - ) -} - -fn runtime_company_matured_live_bond_principal_total( - state: &RuntimeState, - company_id: u32, - current_year_word: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - Some( - market_state - .live_bond_slots - .iter() - .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) - .map(|slot| slot.principal) - .sum(), - ) -} - -fn runtime_company_next_live_bond_maturity_year( - state: &RuntimeState, - company_id: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - market_state - .live_bond_slots - .iter() - .filter_map(|slot| (slot.maturity_year != 0).then_some(slot.maturity_year)) - .min() -} - -pub(crate) fn runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( - state: &RuntimeState, - company_id: u32, - share_pressure_shares: i64, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - if let Some(cached_value) = - runtime_decode_saved_f32_value_f64(market_state.cached_share_price_raw_u32) - { - if share_pressure_shares == 0 { - return Some(cached_value.max(0.0001)); - } - } - - if market_state.outstanding_shares == 0 { - return Some(0.0010000000474974513); - } - - let company = state - .companies - .iter() - .find(|company| company.company_id == company_id)?; - let mut recent_per_share = runtime_company_recent_per_share_subscore(state, company_id)?; - let young_company_support = - runtime_decode_saved_f32_value_f64(market_state.young_company_support_scalar_raw_u32)?; - if recent_per_share < young_company_support { - let years_since_founding = - derive_runtime_company_elapsed_years(state.calendar.year, market_state.founding_year) - .unwrap_or(u32::MAX); - if years_since_founding <= 5 { - let elapsed_support_ticks = runtime_world_absolute_counter(state) - .unwrap_or(0) - .saturating_sub(market_state.support_progress_word); - let interpolation = (elapsed_support_ticks / 50).min(50) as f64; - recent_per_share = ((50.0 - interpolation) * young_company_support - + (50.0 + interpolation) * recent_per_share) - / 100.0; - } - } - - let mutable_support = - runtime_decode_saved_f32_value_f64(market_state.mutable_support_scalar_raw_u32)?; - let share_pressure = - (share_pressure_shares as f64 / market_state.outstanding_shares as f64).clamp(-0.2, 0.2); - let effective_mutable_support = mutable_support + share_pressure; - let share_count_growth_ratio = ((market_state.outstanding_shares as f64 - + 1.4 - * effective_mutable_support - * ((market_state.outstanding_shares as f64 / 20_000.0).powf(0.33))) - / market_state.outstanding_shares as f64) - .clamp(0.3, 6.0); - - let investor_multiplier = runtime_world_issue_opinion_multiplier( - state, - RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE, - company.linked_chairman_profile_id, - Some(company_id), - None, - )?; - - Some(((recent_per_share * share_count_growth_ratio * investor_multiplier) + 1.0).max(0.0001)) -} - -pub fn runtime_company_recent_per_share_subscore( - state: &RuntimeState, - company_id: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - if runtime_world_absolute_counter(state) - .is_some_and(|counter| counter == market_state.recent_per_share_cache_absolute_counter) - { - if let Some(cached_value) = - runtime_decode_saved_f64_bits(market_state.recent_per_share_cached_value_bits) - { - return Some(cached_value); - } - } - runtime_decode_saved_f32_value_f64(market_state.recent_per_share_subscore_raw_u32) -} - -fn runtime_company_support_adjusted_share_price_scalar_f64( - state: &RuntimeState, - company_id: u32, -) -> Option { - runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(state, company_id, 0) -} - -pub fn runtime_company_investor_confidence(state: &RuntimeState, company_id: u32) -> Option { - let company = state - .companies - .iter() - .find(|company| company.company_id == company_id)?; - if company.investor_confidence != 0 { - return Some(company.investor_confidence); - } - runtime_company_support_adjusted_share_price_scalar_f64(state, company_id) - .and_then(runtime_round_f64_to_i64) -} - -pub fn runtime_company_management_attitude(state: &RuntimeState, company_id: u32) -> Option { - let company = state - .companies - .iter() - .find(|company| company.company_id == company_id)?; - if company.management_attitude != 0 { - return Some(company.management_attitude); - } - runtime_world_issue_opinion_term_sum_raw( - state, - RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE, - company.linked_chairman_profile_id, - Some(company_id), - None, - ) -} - -fn runtime_company_control_transfer_stat_value_f64( - state: &RuntimeState, - company_id: u32, - slot_id: u32, -) -> Option { - let company = state - .companies - .iter() - .find(|company| company.company_id == company_id)?; - match slot_id { - 0x00..=0x12 if slot_id != RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH => { - runtime_company_current_stat_value_f64(state, company_id, slot_id) - } - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH => { - if company.current_cash != 0 { - Some(company.current_cash as f64) - } else { - runtime_company_current_stat_value_f64(state, company_id, slot_id) - } - } - 0x14 => runtime_company_control_transfer_stat_value_f64(state, company_id, 0x31) - .zip(state.service_state.company_market_state.get(&company_id)) - .map(|(value, market_state)| { - if market_state.outstanding_shares == 0 { - 0.0 - } else { - value / market_state.outstanding_shares as f64 - } - }), - 0x15 => runtime_company_stat_value_f64( - state, - company_id, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: 0x2c, - }, - ) - .zip(state.service_state.company_market_state.get(&company_id)) - .map(|(value, market_state)| { - if market_state.outstanding_shares == 0 { - 0.0 - } else { - value / market_state.outstanding_shares as f64 - } - }), - 0x16 => runtime_company_stat_value_f64( - state, - company_id, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: 0x2b, - }, - ) - .zip(state.service_state.company_market_state.get(&company_id)) - .map(|(value, market_state)| { - if market_state.outstanding_shares == 0 { - 0.0 - } else { - value / market_state.outstanding_shares as f64 - } - }), - 0x13 => runtime_company_support_adjusted_share_price_scalar_f64(state, company_id), - 0x17 => runtime_company_direct_float_field_value_f64(state, company_id, 0x4b), - RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING => { - runtime_company_credit_rating(state, company_id).map(|value| value as f64) - } - 0x1a => runtime_company_direct_float_field_value_f64(state, company_id, 0x53), - 0x1b => runtime_company_direct_float_field_value_f64(state, company_id, 0x323), - 0x1c => runtime_company_direct_float_field_value_f64(state, company_id, 0x327), - RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE => { - runtime_company_book_value_per_share(state, company_id).map(|value| value as f64) - } - 0x1e => runtime_company_direct_float_field_value_f64(state, company_id, 0x333), - 0x1f => runtime_company_direct_float_field_value_f64(state, company_id, 0x33b), - 0x20 => runtime_company_direct_float_field_value_f64(state, company_id, 0x33f), - 0x21 => runtime_company_direct_float_field_value_f64(state, company_id, 0x327).and_then( - |denominator| { - let numerator = - runtime_company_direct_float_field_value_f64(state, company_id, 0x32b)?; - Some(if denominator <= 0.0 { - numerator - } else { - numerator / denominator - }) - }, - ), - 0x22 => runtime_company_direct_float_field_value_f64(state, company_id, 0x333).and_then( - |denominator| { - let numerator = - runtime_company_direct_float_field_value_f64(state, company_id, 0x337)?; - Some(if denominator <= 0.0 { - numerator - } else { - numerator / denominator - }) - }, - ), - 0x23 => runtime_company_direct_float_field_value_f64(state, company_id, 0x33f).and_then( - |denominator| { - let numerator = - runtime_company_direct_float_field_value_f64(state, company_id, 0x343)?; - Some(if denominator <= 0.0 { - numerator - } else { - numerator / denominator - }) - }, - ), - 0x26 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x34b), - 0x27 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x14f), - 0x28 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d0b), - 0x29 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d0f), - 0x2a => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d13), - _ => None, - } -} - -fn runtime_company_special_stat_family_232a_value_f64( - state: &RuntimeState, - company_id: u32, - slot_id: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - let value = runtime_decode_saved_f64_bits( - *market_state - .special_stat_family_232a_qword_bits - .get(slot_id as usize)?, - )?; - if (0x13..=0x1b).contains(&slot_id) { - Some(value + runtime_company_control_transfer_stat_value_f64(state, company_id, slot_id)?) - } else { - Some(value) - } -} - -fn runtime_company_year_stat_value_f64( - state: &RuntimeState, - company_id: u32, - family_id: u32, - slot_id: u32, -) -> Option { - let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); - let year_delta = current_year_word.checked_sub(family_id)?; - if year_delta == 0 || year_delta >= RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN { - return None; - } - let market_state = state.service_state.company_market_state.get(&company_id)?; - let index = slot_id - .checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? - .checked_add(year_delta)? as usize; - runtime_decode_saved_f64_bits(*market_state.year_stat_family_qword_bits.get(index)?) -} - -fn runtime_company_derived_stat_value_f64( - state: &RuntimeState, - company_id: u32, - family_id: u32, - slot_id: u32, -) -> Option { - let stat = |slot_id| { - runtime_company_stat_value_f64( - state, - company_id, - RuntimeCompanyStatSelector { family_id, slot_id }, - ) - }; - let rounded_stat = |slot_id| stat(slot_id).and_then(runtime_round_f64_to_i64); - match slot_id { - 0x2b => Some(stat(0x2d)? + stat(0x2c)?), - 0x2c => Some(stat(0x04)? + stat(0x03)? + stat(0x02)? + stat(0x01)?), - 0x2d => Some(stat(0x2f)? + stat(0x2e)?), - 0x2e => Some( - stat(0x0c)? - + stat(0x0b)? - + stat(0x0a)? - + stat(0x08)? - + stat(0x07)? - + stat(0x06)? - + stat(0x05)?, - ), - 0x2f => stat(0x09), - 0x30 => Some(stat(0x11)? + stat(0x10)? + stat(0x0f)? + stat(0x0e)? + stat(0x0d)?), - 0x31 => Some(stat(0x30)? + stat(0x12)?), - 0x32 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x24)?), - 0x33 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x16)?), - 0x34 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x17)?), - 0x35 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x18)?), - 0x36 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x19)?), - 0x37 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x1a)?), - 0x38 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x1b)?), - _ => None, - } -} - -fn runtime_divide_by_rounded_stat_i64(numerator: f64, denominator: i64) -> Option { - if denominator == 0 { - return Some(0.0); - } - Some(numerator / denominator as f64) -} - -fn runtime_company_trailing_full_year_stat_series( - state: &RuntimeState, - company_id: u32, - slot_id: u32, - full_year_count: u32, -) -> Option<(Vec, Vec)> { - let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); - let mut year_words = Vec::with_capacity(full_year_count as usize); - let mut values = Vec::with_capacity(full_year_count as usize); - for year_offset in 1..=full_year_count { - let family_id = current_year_word.checked_sub(year_offset)?; - let value = runtime_company_stat_value_f64( - state, - company_id, - RuntimeCompanyStatSelector { family_id, slot_id }, - ) - .and_then(runtime_round_f64_to_i64)?; - year_words.push(family_id); - values.push(value); - } - Some((year_words, values)) -} - -fn runtime_company_year_or_control_transfer_metric_value_f64( - state: &RuntimeState, - company_id: u32, - year_word: u32, - slot_id: u32, -) -> Option { - let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); - if year_word == current_year_word { - runtime_company_stat_value_f64( - state, - company_id, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id, - }, - ) - } else { - runtime_company_stat_value_f64( - state, - company_id, - RuntimeCompanyStatSelector { - family_id: year_word, - slot_id, - }, - ) - } -} - -pub fn runtime_world_issue_opinion_term_sum_raw( - state: &RuntimeState, - issue_id: u32, - chairman_profile_id: Option, - company_id: Option, - territory_id: Option, -) -> Option { - let mut total = i64::from( - *state - .service_state - .world_issue_opinion_base_terms_raw_i32 - .get(issue_id as usize)?, - ); - let mut resolved_company_id = company_id; - if let Some(profile_id) = chairman_profile_id { - if let Some(profile_terms) = state - .service_state - .chairman_issue_opinion_terms_raw_i32 - .get(&profile_id) - { - if let Some(value) = profile_terms.get(issue_id as usize) { - total = total.checked_add(i64::from(*value))?; - } - } - if resolved_company_id.is_none() { - resolved_company_id = state - .chairman_profiles - .iter() - .find(|profile| profile.profile_id == profile_id) - .and_then(|profile| profile.linked_company_id); - } - } - if let Some(company_id) = resolved_company_id { - if let Some(company_terms) = state - .service_state - .company_market_state - .get(&company_id) - .map(|market_state| &market_state.issue_opinion_terms_raw_i32) - { - if let Some(value) = company_terms.get(issue_id as usize) { - total = total.checked_add(i64::from(*value))?; - } - } - } - if territory_id.is_some() { - return None; - } - Some(total) -} - -pub fn runtime_world_issue_opinion_multiplier( - state: &RuntimeState, - issue_id: u32, - chairman_profile_id: Option, - company_id: Option, - territory_id: Option, -) -> Option { - let normalize = |raw: i32| (i64::from(raw.max(-99)) as f64) / 100.0 + 1.0; - let base_raw = *state - .service_state - .world_issue_opinion_base_terms_raw_i32 - .get(issue_id as usize)?; - let mut multiplier = normalize(base_raw); - let mut resolved_company_id = company_id; - if let Some(profile_id) = chairman_profile_id { - if let Some(profile_terms) = state - .service_state - .chairman_issue_opinion_terms_raw_i32 - .get(&profile_id) - { - if let Some(value) = profile_terms.get(issue_id as usize) { - multiplier *= normalize(*value); - } - } - if resolved_company_id.is_none() { - resolved_company_id = state - .chairman_profiles - .iter() - .find(|profile| profile.profile_id == profile_id) - .and_then(|profile| profile.linked_company_id); - } - } - if let Some(company_id) = resolved_company_id { - if let Some(company_terms) = state - .service_state - .company_market_state - .get(&company_id) - .map(|market_state| &market_state.issue_opinion_terms_raw_i32) - { - if let Some(value) = company_terms.get(issue_id as usize) { - multiplier *= normalize(*value); - } - } - } - if territory_id.is_some() { - return None; - } - Some(multiplier.max(0.01)) -} - -pub fn runtime_world_issue_state( - state: &RuntimeState, - issue_id: u32, -) -> Option { - match issue_id { - RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE => Some(RuntimeWorldIssueState { - issue_id, - raw_value_u32: state.world_restore.issue_37_value?, - multiplier_raw_u32: state.world_restore.issue_37_multiplier_raw_u32, - multiplier_value_f32_text: state - .world_restore - .issue_37_multiplier_value_f32_text - .clone(), - }), - RUNTIME_WORLD_ISSUE_CREDIT_MARKET => Some(RuntimeWorldIssueState { - issue_id, - raw_value_u32: state.world_restore.issue_38_value?, - multiplier_raw_u32: None, - multiplier_value_f32_text: None, - }), - RUNTIME_WORLD_ISSUE_PRIME_RATE => Some(RuntimeWorldIssueState { - issue_id, - raw_value_u32: state.world_restore.issue_39_value?, - multiplier_raw_u32: None, - multiplier_value_f32_text: None, - }), - RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE => Some(RuntimeWorldIssueState { - issue_id, - raw_value_u32: state.world_restore.issue_3a_value?, - multiplier_raw_u32: None, - multiplier_value_f32_text: None, - }), - _ => None, - } -} - -pub fn runtime_world_prime_rate_baseline(state: &RuntimeState) -> Option { - let raw = state.world_restore.issue_37_value?; - let value = f32::from_bits(raw) as f64; - if !value.is_finite() { - return None; - } - let scaled = (value + 0.001) * 100.0; - if !scaled.is_finite() { - return None; - } - Some(scaled.round() / 100.0) -} - -pub fn runtime_company_prime_rate(state: &RuntimeState, company_id: u32) -> Option { - let company = state - .companies - .iter() - .find(|company| company.company_id == company_id)?; - if let Some(prime_rate) = company.prime_rate { - return Some(prime_rate); - } - let baseline = runtime_world_prime_rate_baseline(state)?; - let raw_issue_sum = runtime_world_issue_opinion_term_sum_raw( - state, - RUNTIME_WORLD_ISSUE_PRIME_RATE, - company.linked_chairman_profile_id, - Some(company_id), - None, - )?; - runtime_round_f64_to_i64(baseline + (raw_issue_sum as f64) * 0.01) -} - -fn runtime_credit_rating_profitability_ladder(ratio: f64) -> f64 { - if !ratio.is_finite() || ratio <= 0.0 { - 0.0 - } else if ratio < 1.0 { - 1.0 + ratio * 4.0 - } else if ratio < 2.0 { - 3.0 + ratio * 2.0 - } else if ratio < 5.0 { - 5.0 + ratio - } else { - 10.0 - } -} - -fn runtime_credit_rating_burden_ladder(ratio: f64) -> f64 { - if !ratio.is_finite() || ratio > 1.0 { - 0.0 - } else if ratio > 0.75 { - 16.0 - ratio * 16.0 - } else if ratio > 0.5 { - 13.0 - ratio * 12.0 - } else if ratio > 0.25 { - 11.0 - ratio * 8.0 - } else if ratio > 0.1 { - 10.0 - ratio * 4.0 - } else { - 10.0 - } -} - -fn runtime_world_credit_market_scale(state: &RuntimeState) -> Option { - const ISSUE_38_SCALE_TABLE: [f64; 8] = [0.8, 0.9, 1.0, 1.1, 1.2, 0.9, 0.95, 1.0]; - let index = state.world_restore.issue_38_value? as usize; - ISSUE_38_SCALE_TABLE.get(index).copied() -} - -pub fn runtime_company_credit_rating(state: &RuntimeState, company_id: u32) -> Option { - let company = state - .companies - .iter() - .find(|company| company.company_id == company_id)?; - if let Some(credit_rating_score) = company.credit_rating_score { - return Some(credit_rating_score); - } - - let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; - if annual_finance_state.outstanding_shares == 0 { - return Some(-512); - } - - let mut weighted_recent_profit_total = 0.0f64; - let mut weighted_recent_profit_weight = 0.0f64; - for (index, (net_profit, fuel_cost)) in annual_finance_state - .trailing_full_year_net_profits - .iter() - .zip(annual_finance_state.trailing_full_year_fuel_costs.iter()) - .take(4) - .enumerate() - { - let weight = (4 - index) as f64; - weighted_recent_profit_total += (*net_profit - *fuel_cost) as f64 * weight; - weighted_recent_profit_weight += weight; - } - let weighted_recent_profit = if weighted_recent_profit_weight > 0.0 { - weighted_recent_profit_total / weighted_recent_profit_weight - } else { - 0.0 - }; - - let current_slot_12 = runtime_company_control_transfer_stat_value_f64(state, company_id, 0x12)?; - let current_slot_30 = runtime_company_derived_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - 0x30, - )?; - let current_slot_31 = runtime_company_derived_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - 0x31, - )?; - let average_live_bond_coupon = runtime_company_average_live_bond_coupon(state, company_id)?; - - let mut finance_pressure = average_live_bond_coupon * current_slot_12; - if company.current_cash > 0 { - let prime_baseline = runtime_world_prime_rate_baseline(state)?; - let raw_issue_39 = runtime_world_issue_opinion_term_sum_raw( - state, - RUNTIME_WORLD_ISSUE_PRIME_RATE, - company.linked_chairman_profile_id, - Some(company_id), - None, - )? as f64; - finance_pressure += - company.current_cash as f64 * (prime_baseline + raw_issue_39 * 0.01 + 0.03); - } - - let profitability_ratio = if finance_pressure < 0.0 { - weighted_recent_profit / (-finance_pressure) - } else { - 10.0 - }; - let mut profitability_score = runtime_credit_rating_profitability_ladder(profitability_ratio); - if let Some(years_since_founding) = annual_finance_state.years_since_founding { - if years_since_founding < 5 { - let missing_years = (5 - years_since_founding) as f64; - profitability_score += (10.0 - profitability_score) * missing_years * 0.1; - } - } - if current_slot_31 > 1_000_000.0 { - profitability_score += current_slot_31 / 100_000.0 - 10.0; - } - - let burden_ratio = if current_slot_30 > 0.0 { - (weighted_recent_profit - current_slot_12) / current_slot_30 - } else { - 1.0 - }; - let burden_score = runtime_credit_rating_burden_ladder(burden_ratio); - - let mut rating = - (profitability_score * burden_score / 10.0 + profitability_score + burden_score) / 3.0; - rating *= runtime_world_credit_market_scale(state)?; - if let Some(years_since_last_bankruptcy) = annual_finance_state.years_since_last_bankruptcy { - if years_since_last_bankruptcy < 15 { - rating *= years_since_last_bankruptcy as f64 * 0.0666; - } - } - - let raw_issue_38 = runtime_world_issue_opinion_term_sum_raw( - state, - RUNTIME_WORLD_ISSUE_CREDIT_MARKET, - company.linked_chairman_profile_id, - Some(company_id), - None, - )? as f64; - runtime_round_f64_to_i64((rating + raw_issue_38 + 0.5).clamp(0.0, 10.0)) -} - -pub fn runtime_company_average_live_bond_coupon( - state: &RuntimeState, - company_id: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - if market_state.live_bond_slots.is_empty() { - return Some(0.0); - } - let mut weighted_coupon_sum = 0.0f64; - let mut total_principal = 0u64; - for slot in &market_state.live_bond_slots { - let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32) as f64; - if !coupon_rate.is_finite() { - continue; - } - weighted_coupon_sum += coupon_rate * (slot.principal as f64); - total_principal = total_principal.checked_add(slot.principal as u64)?; - } - if total_principal == 0 { - return Some(0.0); - } - Some(weighted_coupon_sum / total_principal as f64) -} - -pub fn runtime_company_live_bond_coupon_burden_total( - state: &RuntimeState, - company_id: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - let mut total = 0i64; - for slot in &market_state.live_bond_slots { - let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32) as f64; - if !coupon_rate.is_finite() { - continue; - } - let coupon_burden = - runtime_round_f64_to_i64((slot.principal as f64) * coupon_rate).unwrap_or(0); - total = total.checked_add(coupon_burden)?; - } - Some(total) -} - -pub fn runtime_company_bond_interest_rate_quote_f64( - state: &RuntimeState, - company_id: u32, - _principal: u32, - _years_to_maturity: u32, -) -> Option { - let credit_rating = runtime_company_credit_rating(state, company_id)? as f64; - let prime_rate_percent = runtime_company_prime_rate(state, company_id)? as f64; - let quote = (prime_rate_percent + (10.0 - credit_rating)) / 100.0; - quote.is_finite().then_some(quote) -} - -pub fn runtime_world_annual_finance_mode_active(state: &RuntimeState) -> Option { - Some(state.world_restore.partial_year_progress_raw_u8? == 0x0c) -} - -pub fn runtime_world_bankruptcy_allowed(state: &RuntimeState) -> Option { - Some(state.world_restore.bankruptcy_policy_raw_u8? == 0) -} - -pub fn runtime_world_bond_issue_and_repayment_allowed(state: &RuntimeState) -> Option { - Some(state.world_restore.bond_issue_and_repayment_policy_raw_u8? == 0) -} - -pub fn runtime_world_stock_issue_and_buyback_allowed(state: &RuntimeState) -> Option { - Some(state.world_restore.stock_issue_and_buyback_policy_raw_u8? == 0) -} - -pub fn runtime_world_dividend_adjustment_allowed(state: &RuntimeState) -> Option { - Some(state.world_restore.dividend_policy_raw_u8? == 0) -} - -pub fn runtime_world_building_density_growth_setting(state: &RuntimeState) -> Option { - state.world_restore.building_density_growth_setting_raw_u32 -} - -fn runtime_chairman_stock_repurchase_factor_f64( - state: &RuntimeState, - chairman_profile_id: Option, -) -> Option { - let personality_byte = chairman_profile_id - .and_then(|profile_id| { - state - .service_state - .chairman_personality_raw_u8 - .get(&profile_id) - }) - .copied(); - let mut factor = personality_byte - .map(|byte| (f64::from(byte) * 39.0 + 300.0) / 400.0) - .unwrap_or(1.0); - if runtime_world_building_density_growth_setting(state) == Some(1) { - factor *= 1.6; - } - Some(factor) -} - -pub fn runtime_company_annual_stock_repurchase_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; - let company = state - .companies - .iter() - .find(|company| company.company_id == company_id)?; - let current_cash = runtime_company_control_transfer_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - ) - .and_then(runtime_round_f64_to_i64); - let support_adjusted_share_price_scalar = - runtime_company_support_adjusted_share_price_scalar_f64(state, company_id); - let repurchase_factor = - runtime_chairman_stock_repurchase_factor_f64(state, company.linked_chairman_profile_id)?; - let repurchase_factor_basis_points = runtime_round_f64_to_i64(repurchase_factor * 100.0); - let stock_value_gate_cash_floor = runtime_round_f64_to_i64(repurchase_factor * 800_000.0); - let affordability_cash_floor = support_adjusted_share_price_scalar - .and_then(|value| runtime_round_f64_to_i64(value * repurchase_factor * 1_000.0 * 1.2)); - let support_adjusted_share_price_scalar = - support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64); - let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id); - let eligible_for_single_batch_repurchase = runtime_world_annual_finance_mode_active(state) - == Some(true) - && runtime_world_stock_issue_and_buyback_allowed(state) == Some(true) - && annual_finance_state.city_connection_latch - && current_cash - .zip(stock_value_gate_cash_floor) - .is_some_and(|(value, floor)| value >= floor) - && current_cash - .zip(affordability_cash_floor) - .is_some_and(|(value, floor)| value >= floor) - && unassigned_share_pool.is_some_and(|value| value >= 1_000); - Some(RuntimeCompanyAnnualStockRepurchaseState { - company_id, - annual_mode_active: runtime_world_annual_finance_mode_active(state), - stock_issue_and_buyback_allowed: runtime_world_stock_issue_and_buyback_allowed(state), - city_connection_latch: annual_finance_state.city_connection_latch, - building_density_growth_setting: runtime_world_building_density_growth_setting(state), - linked_chairman_profile_id: company.linked_chairman_profile_id, - linked_chairman_personality_raw_u8: company - .linked_chairman_profile_id - .and_then(|profile_id| { - state - .service_state - .chairman_personality_raw_u8 - .get(&profile_id) - }) - .copied(), - repurchase_batch_size: Some(1_000), - repurchase_factor_basis_points, - current_cash, - stock_value_gate_cash_floor, - support_adjusted_share_price_scalar, - affordability_cash_floor, - unassigned_share_pool, - eligible_for_single_batch_repurchase, - }) -} - -pub fn runtime_company_annual_bond_policy_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - const STANDARD_CASH_FLOOR: i64 = -250_000; - const LINKED_TRANSIT_CASH_FLOOR: i64 = -30_000; - const ISSUE_PRINCIPAL_STEP: u32 = 500_000; - const ISSUE_YEARS_TO_MATURITY: u32 = 30; - - let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; - let current_year_word = state - .world_restore - .packed_year_word_raw_u16 - .map(u32::from) - .unwrap_or(state.calendar.year); - let current_cash = runtime_company_control_transfer_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - ) - .and_then(runtime_round_f64_to_i64); - let live_bond_principal_total = runtime_company_total_live_bond_principal(state, company_id); - let matured_live_bond_count = - runtime_company_matured_live_bond_count(state, company_id, current_year_word); - let matured_live_bond_principal_total = - runtime_company_matured_live_bond_principal_total(state, company_id, current_year_word); - let next_live_bond_maturity_year = - runtime_company_next_live_bond_maturity_year(state, company_id); - let cash_after_full_repayment = current_cash - .zip(live_bond_principal_total) - .map(|(cash, principal)| cash - i64::from(principal)); - let issue_cash_floor = Some(if annual_finance_state.linked_transit_latch { - LINKED_TRANSIT_CASH_FLOOR - } else { - STANDARD_CASH_FLOOR - }); - let proposed_issue_bond_count = cash_after_full_repayment.zip(issue_cash_floor).map( - |(cash_after_repayment, cash_floor)| { - if cash_after_repayment >= cash_floor { - 0 - } else { - let deficit = (cash_floor - cash_after_repayment) as u64; - deficit.div_ceil(u64::from(ISSUE_PRINCIPAL_STEP)) as u32 - } - }, - ); - let proposed_issue_total_principal = - proposed_issue_bond_count.and_then(|count| count.checked_mul(ISSUE_PRINCIPAL_STEP)); - let eligible_for_bond_issue_branch = runtime_world_annual_finance_mode_active(state) - == Some(true) - && runtime_world_bond_issue_and_repayment_allowed(state) == Some(true) - && (matured_live_bond_principal_total.is_some_and(|principal| principal > 0) - || proposed_issue_bond_count.is_some_and(|count| count > 0)); - Some(RuntimeCompanyAnnualBondPolicyState { - company_id, - annual_mode_active: runtime_world_annual_finance_mode_active(state), - bond_issue_and_repayment_allowed: runtime_world_bond_issue_and_repayment_allowed(state), - linked_transit_latch: annual_finance_state.linked_transit_latch, - live_bond_count: Some(annual_finance_state.bond_count), - live_bond_principal_total, - matured_live_bond_count, - matured_live_bond_principal_total, - next_live_bond_maturity_year, - live_bond_coupon_burden_total: annual_finance_state.live_bond_coupon_burden_total, - current_cash, - cash_after_full_repayment, - issue_cash_floor, - issue_principal_step: Some(ISSUE_PRINCIPAL_STEP), - proposed_issue_bond_count, - proposed_issue_total_principal, - proposed_issue_years_to_maturity: Some(ISSUE_YEARS_TO_MATURITY), - eligible_for_bond_issue_branch, - }) -} - -fn runtime_company_board_approved_dividend_rate_ceiling_f64( - state: &RuntimeState, - company_id: u32, -) -> Option { - const REVENUE_GUARD_DIVISOR: f64 = 2.0; - const EARLY_SUPPORT_MULTIPLIER: f64 = 0.05; - const HISTORICAL_GUARD_SCALE: f64 = 1.25; - const ANCHOR_SCALE: f64 = 0.35; - - let market_state = state.service_state.company_market_state.get(&company_id)?; - let current_cash = runtime_company_control_transfer_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - )?; - let shares_plus_one = market_state.outstanding_shares.checked_add(1)?; - let shares_plus_one_f64 = shares_plus_one as f64; - let current_cash_per_share_ceiling = current_cash / shares_plus_one_f64; - let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); - let years_since_founding = current_year_word - .checked_sub(market_state.founding_year) - .unwrap_or(0) - .min(3); - let start_year_offset = if state.world_restore.partial_year_progress_raw_u8 == Some(0x0c) { - 0 - } else { - 1 - }; - - let mut strongest_net_profit_guard = 0.0f64; - let mut strongest_revenue_guard = 0.0f64; - if start_year_offset <= years_since_founding { - for year_offset in start_year_offset..=years_since_founding { - let year_word = current_year_word.checked_sub(year_offset)?; - let net_profit = runtime_company_year_or_control_transfer_metric_value_f64( - state, company_id, year_word, 0x2b, - )?; - strongest_net_profit_guard = strongest_net_profit_guard.max(net_profit); - - let revenue = runtime_company_year_or_control_transfer_metric_value_f64( - state, company_id, year_word, 0x2c, - )?; - strongest_revenue_guard = strongest_revenue_guard.max(revenue); - } - } - - let mut historical_guard_total = - strongest_net_profit_guard.min(strongest_revenue_guard / REVENUE_GUARD_DIVISOR); - if years_since_founding <= 1 { - let early_support_guard = market_state.outstanding_shares as f64 - * runtime_decode_saved_f32_value_f64( - market_state.young_company_support_scalar_raw_u32, - )? - * EARLY_SUPPORT_MULTIPLIER; - historical_guard_total = historical_guard_total.max(early_support_guard); - } - - let historical_guard_per_share_ceiling = - historical_guard_total / shares_plus_one_f64 * HISTORICAL_GUARD_SCALE; - let mut ceiling = current_cash_per_share_ceiling.min(historical_guard_per_share_ceiling); - let anchor_value = if years_since_founding == 0 { - runtime_decode_saved_f32_value_f64(market_state.young_company_support_scalar_raw_u32)? - } else { - runtime_company_year_or_control_transfer_metric_value_f64( - state, - company_id, - current_year_word.checked_sub(1)?, - 0x1c, - )? - }; - ceiling = ceiling.min(anchor_value * ANCHOR_SCALE); - Some(ceiling.max(0.0)) -} - -pub fn runtime_company_annual_dividend_policy_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - const WEIGHTED_NET_PROFIT_DIVISOR: f64 = 6.0; - const CASH_SUPPLEMENT_DIVISOR: f64 = 3.0; - const STANDARD_TARGET_DIVISOR: f64 = 6.0; - const DIVIDEND_DELTA_COLLAPSE_THRESHOLD: f64 = 0.1; - const GROWTH_SETTING_ONE_DIVIDEND_SCALE: f64 = 0.66; - - let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; - let current_cash = runtime_company_control_transfer_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - ) - .and_then(runtime_round_f64_to_i64); - let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); - let current_dividend_per_share = - runtime_company_control_transfer_stat_value_f64(state, company_id, 0x20)?; - let building_density_growth_setting = runtime_world_building_density_growth_setting(state); - let weighted_recent_net_profit_total = Some( - runtime_company_year_or_control_transfer_metric_value_f64( - state, - company_id, - current_year_word, - 0x2b, - ) - .and_then(runtime_round_f64_to_i64)? - .checked_mul(3)? - .checked_add( - runtime_company_year_or_control_transfer_metric_value_f64( - state, - company_id, - current_year_word.checked_sub(1)?, - 0x2b, - ) - .and_then(runtime_round_f64_to_i64)? - .checked_mul(2)?, - )? - .checked_add( - runtime_company_year_or_control_transfer_metric_value_f64( - state, - company_id, - current_year_word.checked_sub(2)?, - 0x2b, - ) - .and_then(runtime_round_f64_to_i64)?, - )?, - ); - let weighted_recent_net_profit_average = weighted_recent_net_profit_total - .and_then(|value| runtime_round_f64_to_i64(value as f64 / WEIGHTED_NET_PROFIT_DIVISOR)); - let tiny_unassigned_share_cash_supplement_branch = - annual_finance_state.unassigned_share_pool <= 1_000; - let tentative_target_dividend_per_share = - weighted_recent_net_profit_average.and_then(|value| { - if annual_finance_state.outstanding_shares == 0 { - return None; - } - let shares = annual_finance_state.outstanding_shares as f64; - if tiny_unassigned_share_cash_supplement_branch { - let cash_component = current_cash.unwrap_or(0).max(0) as f64; - Some( - ((value as f64 / CASH_SUPPLEMENT_DIVISOR) - + cash_component / CASH_SUPPLEMENT_DIVISOR) - / shares, - ) - } else { - Some((value as f64 / STANDARD_TARGET_DIVISOR) / shares) - } - }); - let growth_adjusted_current_dividend_per_share = Some(match building_density_growth_setting { - Some(1) => current_dividend_per_share * GROWTH_SETTING_ONE_DIVIDEND_SCALE, - Some(2) => 0.0, - _ => current_dividend_per_share, - }); - let proposed_dividend_per_share = if tentative_target_dividend_per_share - .is_some_and(|value| value <= DIVIDEND_DELTA_COLLAPSE_THRESHOLD) - { - Some(0.0) - } else { - growth_adjusted_current_dividend_per_share - .zip(tentative_target_dividend_per_share) - .map(|(current_dividend, target)| { - ((current_dividend + target + DIVIDEND_DELTA_COLLAPSE_THRESHOLD) / 2.0 * 10.0) - .round() - / 10.0 - }) - }; - let board_approved_dividend_rate_ceiling = - runtime_company_board_approved_dividend_rate_ceiling_f64(state, company_id); - let proposed_dividend_per_share = proposed_dividend_per_share - .zip(board_approved_dividend_rate_ceiling) - .map(|(proposed, ceiling)| proposed.min(ceiling)); - let current_dividend_per_share_tenths = - runtime_round_f64_to_i64(current_dividend_per_share * 10.0); - let eligible_for_dividend_adjustment_branch = runtime_world_annual_finance_mode_active(state) - == Some(true) - && runtime_world_dividend_adjustment_allowed(state) == Some(true) - && annual_finance_state - .years_since_last_dividend - .is_some_and(|years| years >= 1) - && annual_finance_state - .years_since_founding - .is_some_and(|years| years >= 2) - && !runtime_company_annual_creditor_pressure_state(state, company_id)? - .eligible_for_bankruptcy_branch - && !runtime_company_annual_deep_distress_state(state, company_id)? - .eligible_for_bankruptcy_fallback - && !runtime_company_annual_bond_policy_state(state, company_id)? - .eligible_for_bond_issue_branch - && !runtime_company_annual_stock_repurchase_state(state, company_id)? - .eligible_for_single_batch_repurchase - && !runtime_company_annual_stock_issue_state(state, company_id)? - .eligible_for_double_tranche_issue - && proposed_dividend_per_share.and_then(|value| runtime_round_f64_to_i64(value * 10.0)) - != current_dividend_per_share_tenths; - Some(RuntimeCompanyAnnualDividendPolicyState { - company_id, - annual_mode_active: runtime_world_annual_finance_mode_active(state), - dividend_adjustment_allowed: runtime_world_dividend_adjustment_allowed(state), - years_since_last_dividend: annual_finance_state.years_since_last_dividend, - years_since_founding: annual_finance_state.years_since_founding, - outstanding_shares: Some(annual_finance_state.outstanding_shares), - unassigned_share_pool: Some(annual_finance_state.unassigned_share_pool), - weighted_recent_net_profit_total, - weighted_recent_net_profit_average, - current_cash, - tiny_unassigned_share_cash_supplement_branch, - tentative_target_dividend_per_share_tenths: tentative_target_dividend_per_share - .and_then(|value| runtime_round_f64_to_i64(value * 10.0)), - current_dividend_per_share_tenths, - building_density_growth_setting, - growth_adjusted_current_dividend_per_share_tenths: - growth_adjusted_current_dividend_per_share - .and_then(|value| runtime_round_f64_to_i64(value * 10.0)), - board_approved_dividend_rate_ceiling_tenths: board_approved_dividend_rate_ceiling - .and_then(|value| runtime_round_f64_to_i64(value * 10.0)), - proposed_dividend_per_share_tenths: proposed_dividend_per_share - .and_then(|value| runtime_round_f64_to_i64(value * 10.0)), - eligible_for_dividend_adjustment_branch, - }) -} - -pub fn runtime_company_annual_finance_policy_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - runtime_company_annual_finance_state(state, company_id)?; - let creditor_pressure_bankruptcy_eligible = - runtime_company_annual_creditor_pressure_state(state, company_id) - .map(|state| state.eligible_for_bankruptcy_branch) - .unwrap_or(false); - let deep_distress_bankruptcy_fallback_eligible = - runtime_company_annual_deep_distress_state(state, company_id) - .map(|state| state.eligible_for_bankruptcy_fallback) - .unwrap_or(false); - let bond_issue_eligible = runtime_company_annual_bond_policy_state(state, company_id) - .map(|state| state.eligible_for_bond_issue_branch) - .unwrap_or(false); - let stock_repurchase_eligible = - runtime_company_annual_stock_repurchase_state(state, company_id) - .map(|state| state.eligible_for_single_batch_repurchase) - .unwrap_or(false); - let stock_issue_eligible = runtime_company_annual_stock_issue_state(state, company_id) - .map(|state| state.eligible_for_double_tranche_issue) - .unwrap_or(false); - let dividend_adjustment_eligible = - runtime_company_annual_dividend_policy_state(state, company_id) - .map(|state| state.eligible_for_dividend_adjustment_branch) - .unwrap_or(false); - let action = if creditor_pressure_bankruptcy_eligible { - RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy - } else if deep_distress_bankruptcy_fallback_eligible { - RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback - } else if bond_issue_eligible { - RuntimeCompanyAnnualFinancePolicyAction::BondIssue - } else if stock_repurchase_eligible { - RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase - } else if stock_issue_eligible { - RuntimeCompanyAnnualFinancePolicyAction::StockIssue - } else if dividend_adjustment_eligible { - RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment - } else { - RuntimeCompanyAnnualFinancePolicyAction::None - }; - Some(RuntimeCompanyAnnualFinancePolicyState { - company_id, - action, - creditor_pressure_bankruptcy_eligible, - deep_distress_bankruptcy_fallback_eligible, - bond_issue_eligible, - stock_repurchase_eligible, - stock_issue_eligible, - dividend_adjustment_eligible, - }) -} - -pub fn runtime_company_annual_finance_policy_action_label( - action: RuntimeCompanyAnnualFinancePolicyAction, -) -> &'static str { - match action { - RuntimeCompanyAnnualFinancePolicyAction::None => "none", - RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy => { - "creditor_pressure_bankruptcy" - } - RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => { - "deep_distress_bankruptcy_fallback" - } - RuntimeCompanyAnnualFinancePolicyAction::BondIssue => "bond_issue", - RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => "stock_repurchase", - RuntimeCompanyAnnualFinancePolicyAction::StockIssue => "stock_issue", - RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment => "dividend_adjustment", - } -} - -pub fn runtime_annual_bond_principal_flow_relation_label( - retired_principal_total: u64, - issued_principal_total: u64, -) -> Option<&'static str> { - match retired_principal_total.cmp(&issued_principal_total) { - std::cmp::Ordering::Equal => { - if retired_principal_total == 0 { - None - } else { - Some("retired_equals_issued") - } - } - std::cmp::Ordering::Greater => { - if issued_principal_total == 0 { - Some("retired_only") - } else { - Some("retired_exceeds_issued") - } - } - std::cmp::Ordering::Less => { - if retired_principal_total == 0 { - Some("issued_only") - } else { - Some("issued_exceeds_retired") - } - } - } -} - -pub fn runtime_annual_finance_news_family_candidate_label( - action: RuntimeCompanyAnnualFinancePolicyAction, - retired_principal_total: u64, - issued_principal_total: u64, - repurchased_share_count: u64, - issued_share_count: u64, -) -> Option<&'static str> { - match action { - RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy - | RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => Some("2881"), - RuntimeCompanyAnnualFinancePolicyAction::BondIssue => { - match runtime_annual_bond_principal_flow_relation_label( - retired_principal_total, - issued_principal_total, - ) { - Some("retired_equals_issued") => Some("2882"), - Some("issued_exceeds_retired") => Some("2883"), - Some("retired_exceeds_issued") => Some("2884"), - Some("retired_only") => Some("2885"), - Some("issued_only") => Some("2886"), - _ => None, - } - } - RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => { - (repurchased_share_count > 0).then_some("2887") - } - RuntimeCompanyAnnualFinancePolicyAction::StockIssue => { - (issued_share_count > 0).then_some("4053") - } - _ => None, - } -} - -fn runtime_company_stock_issue_price_to_book_ratio_f64( - pressured_support_adjusted_share_price_scalar: f64, - book_value_per_share: f64, -) -> Option { - let denominator = book_value_per_share.max(1.0); - if !pressured_support_adjusted_share_price_scalar.is_finite() || !denominator.is_finite() { - return None; - } - Some(pressured_support_adjusted_share_price_scalar / denominator) -} - -fn runtime_company_stock_issue_minimum_price_to_book_ratio_f64( - highest_coupon_rate: f64, -) -> Option { - if !highest_coupon_rate.is_finite() || highest_coupon_rate <= 0.0 { - return None; - } - Some(if highest_coupon_rate <= 0.07 { - 1.30 - } else if highest_coupon_rate <= 0.08 { - 1.20 - } else if highest_coupon_rate <= 0.09 { - 1.10 - } else if highest_coupon_rate <= 0.10 { - 0.95 - } else if highest_coupon_rate <= 0.11 { - 0.80 - } else if highest_coupon_rate <= 0.12 { - 0.62 - } else if highest_coupon_rate <= 0.13 { - 0.50 - } else if highest_coupon_rate <= 0.14 { - 0.35 - } else { - return None; - }) -} - -pub fn runtime_company_annual_stock_issue_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - const ISSUE_PROCEEDS_CAP: i64 = 55_000; - const SHARE_PRICE_FLOOR: i64 = 22; - const ONE_YEAR_ABSOLUTE_COUNTER_SPAN: i64 = 12 * 28 * 24 * 60; - - let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; - let current_cash = runtime_company_control_transfer_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - ) - .and_then(runtime_round_f64_to_i64); - let highest_coupon_live_bond_principal = - annual_finance_state.highest_coupon_live_bond_principal; - let highest_coupon_live_bond_rate = - runtime_company_highest_live_bond_coupon_rate_f64(state, company_id); - let highest_coupon_live_bond_rate_basis_points = - highest_coupon_live_bond_rate.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); - let mut initial_issue_batch_size = - (annual_finance_state.outstanding_shares / 10 / 1_000) * 1_000; - if initial_issue_batch_size < 2_000 { - initial_issue_batch_size = 2_000; - } - let initial_issue_batch_size = Some(initial_issue_batch_size); - let mut trimmed_issue_batch_size = initial_issue_batch_size?; - let mut pressured_support_adjusted_share_price_scalar = - runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( - state, - company_id, - -(trimmed_issue_batch_size as i64), - ); - let mut pressured_proceeds = pressured_support_adjusted_share_price_scalar - .and_then(|value| runtime_round_f64_to_i64(value * trimmed_issue_batch_size as f64)); - while trimmed_issue_batch_size > 2_000 - && pressured_proceeds.is_some_and(|value| value > ISSUE_PROCEEDS_CAP) - { - trimmed_issue_batch_size = trimmed_issue_batch_size.saturating_sub(1_000); - pressured_support_adjusted_share_price_scalar = - runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( - state, - company_id, - -(trimmed_issue_batch_size as i64), - ); - pressured_proceeds = pressured_support_adjusted_share_price_scalar - .and_then(|value| runtime_round_f64_to_i64(value * trimmed_issue_batch_size as f64)); - } - let pressured_support_adjusted_share_price_scalar_i64 = - pressured_support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64); - let book_value_per_share_floor_applied = - runtime_company_book_value_per_share(state, company_id).map(|value| value.max(1)); - let price_to_book_ratio = pressured_support_adjusted_share_price_scalar - .zip(book_value_per_share_floor_applied) - .and_then(|(share_price, book_value)| { - runtime_company_stock_issue_price_to_book_ratio_f64(share_price, book_value as f64) - }); - let price_to_book_ratio_basis_points = - price_to_book_ratio.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); - let minimum_price_to_book_ratio = highest_coupon_live_bond_rate - .and_then(runtime_company_stock_issue_minimum_price_to_book_ratio_f64); - let minimum_price_to_book_ratio_basis_points = - minimum_price_to_book_ratio.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); - let passes_share_price_floor = - pressured_support_adjusted_share_price_scalar_i64.map(|value| value >= SHARE_PRICE_FLOOR); - let passes_proceeds_floor = pressured_proceeds.map(|value| value >= ISSUE_PROCEEDS_CAP); - let passes_cash_gate = current_cash - .zip(highest_coupon_live_bond_principal) - .map(|(cash, principal)| cash <= i64::from(principal) + 5_000); - let passes_issue_cooldown_gate = Some( - annual_finance_state - .current_issue_age_absolute_counter_delta - .is_none_or(|delta| delta >= ONE_YEAR_ABSOLUTE_COUNTER_SPAN), - ); - let passes_coupon_price_to_book_gate = price_to_book_ratio_basis_points - .zip(minimum_price_to_book_ratio_basis_points) - .map(|(actual, minimum)| actual >= minimum); - let eligible_for_double_tranche_issue = runtime_world_annual_finance_mode_active(state) - == Some(true) - && runtime_world_stock_issue_and_buyback_allowed(state) == Some(true) - && runtime_world_bond_issue_and_repayment_allowed(state) == Some(true) - && annual_finance_state.bond_count >= 2 - && annual_finance_state - .years_since_founding - .is_some_and(|years| years >= 1) - && !runtime_company_annual_creditor_pressure_state(state, company_id)? - .eligible_for_bankruptcy_branch - && !runtime_company_annual_deep_distress_state(state, company_id)? - .eligible_for_bankruptcy_fallback - && !runtime_company_annual_bond_policy_state(state, company_id)? - .eligible_for_bond_issue_branch - && !runtime_company_annual_stock_repurchase_state(state, company_id)? - .eligible_for_single_batch_repurchase - && passes_share_price_floor == Some(true) - && passes_proceeds_floor == Some(true) - && passes_cash_gate == Some(true) - && passes_issue_cooldown_gate == Some(true) - && passes_coupon_price_to_book_gate == Some(true); - Some(RuntimeCompanyAnnualStockIssueState { - company_id, - annual_mode_active: runtime_world_annual_finance_mode_active(state), - stock_issue_and_buyback_allowed: runtime_world_stock_issue_and_buyback_allowed(state), - bond_issue_and_repayment_allowed: runtime_world_bond_issue_and_repayment_allowed(state), - years_since_founding: annual_finance_state.years_since_founding, - live_bond_count: Some(annual_finance_state.bond_count), - initial_issue_batch_size, - trimmed_issue_batch_size: Some(trimmed_issue_batch_size), - share_pressure_basis_points: runtime_round_f64_to_i64( - -(trimmed_issue_batch_size as f64) / annual_finance_state.outstanding_shares as f64 - * 10_000.0, - ), - pressured_support_adjusted_share_price_scalar: - pressured_support_adjusted_share_price_scalar_i64, - pressured_proceeds, - book_value_per_share_floor_applied, - price_to_book_ratio_basis_points, - current_cash, - highest_coupon_live_bond_principal, - highest_coupon_live_bond_rate_basis_points, - current_issue_age_absolute_counter_delta: annual_finance_state - .current_issue_age_absolute_counter_delta, - current_issue_cooldown_floor: Some(ONE_YEAR_ABSOLUTE_COUNTER_SPAN), - minimum_price_to_book_ratio_basis_points, - passes_share_price_floor, - passes_proceeds_floor, - passes_cash_gate, - passes_issue_cooldown_gate, - passes_coupon_price_to_book_gate, - eligible_for_double_tranche_issue, - }) -} - -pub fn runtime_company_annual_creditor_pressure_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; - let current_cash_plus_slot_12_total = - runtime_company_control_transfer_stat_value_f64(state, company_id, 0x12) - .and_then(runtime_round_f64_to_i64) - .and_then(|slot_12| { - runtime_company_control_transfer_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - ) - .and_then(runtime_round_f64_to_i64) - .map(|current_cash| current_cash + slot_12) - }); - let support_adjusted_share_price_scalar = - runtime_company_support_adjusted_share_price_scalar_f64(state, company_id) - .and_then(runtime_round_f64_to_i64); - let current_fuel_cost = runtime_company_stat_value_f64( - state, - company_id, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: 0x09, - }, - ) - .and_then(runtime_round_f64_to_i64); - let recent_bad_net_profit_year_count = annual_finance_state - .trailing_full_year_net_profits - .iter() - .take(3) - .filter(|value| **value < -10_000) - .count() as u32; - let recent_peak_revenue = annual_finance_state - .trailing_full_year_revenues - .iter() - .take(3) - .copied() - .max(); - let recent_three_year_net_profit_total = - if annual_finance_state.trailing_full_year_net_profits.len() >= 3 { - Some( - annual_finance_state - .trailing_full_year_net_profits - .iter() - .take(3) - .sum::(), - ) - } else { - None - }; - let pressure_ladder_cash_floor = recent_peak_revenue.map(|revenue| { - if revenue < 120_000 { - -600_000 - } else if revenue < 230_000 { - -1_100_000 - } else if revenue < 340_000 { - -1_600_000 - } else { - -2_000_000 - } - }); - let support_adjusted_share_price_floor = Some(if recent_bad_net_profit_year_count == 3 { - 20 - } else { - 15 - }); - let current_fuel_cost_floor = pressure_ladder_cash_floor.map(|floor| floor * 8 / 100); - let eligible_for_bankruptcy_branch = runtime_world_annual_finance_mode_active(state) - == Some(true) - && runtime_world_bankruptcy_allowed(state) == Some(true) - && annual_finance_state - .years_since_last_bankruptcy - .is_some_and(|years| years >= 13) - && annual_finance_state - .years_since_founding - .is_some_and(|years| years >= 4) - && recent_bad_net_profit_year_count >= 2 - && current_cash_plus_slot_12_total - .zip(pressure_ladder_cash_floor) - .is_some_and(|(value, floor)| value <= floor) - && support_adjusted_share_price_scalar - .zip(support_adjusted_share_price_floor) - .is_some_and(|(value, floor)| value >= floor) - && current_fuel_cost - .zip(current_fuel_cost_floor) - .is_some_and(|(value, floor)| value <= floor) - && recent_three_year_net_profit_total.is_some_and(|value| value <= -60_000); - Some(RuntimeCompanyAnnualCreditorPressureState { - company_id, - annual_mode_active: runtime_world_annual_finance_mode_active(state), - bankruptcy_allowed: runtime_world_bankruptcy_allowed(state), - years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy, - years_since_founding: annual_finance_state.years_since_founding, - recent_bad_net_profit_year_count, - recent_peak_revenue, - recent_three_year_net_profit_total, - pressure_ladder_cash_floor, - current_cash_plus_slot_12_total, - support_adjusted_share_price_floor, - support_adjusted_share_price_scalar, - current_fuel_cost, - current_fuel_cost_floor, - eligible_for_bankruptcy_branch, - }) -} - -pub fn runtime_company_annual_deep_distress_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; - let current_cash = runtime_company_control_transfer_stat_value_f64( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - ) - .and_then(runtime_round_f64_to_i64); - let recent_first_three_net_profit_years = annual_finance_state - .trailing_full_year_net_profits - .iter() - .take(3) - .copied() - .collect::>(); - let deep_distress_cash_floor = Some(-300_000); - let deep_distress_net_profit_floor = Some(-20_000); - let eligible_for_bankruptcy_fallback = runtime_world_bankruptcy_allowed(state) == Some(true) - && current_cash - .zip(deep_distress_cash_floor) - .is_some_and(|(value, floor)| value <= floor) - && annual_finance_state - .years_since_founding - .is_some_and(|years| years >= 3) - && recent_first_three_net_profit_years.len() == 3 - && recent_first_three_net_profit_years - .iter() - .all(|value| *value <= deep_distress_net_profit_floor.unwrap()) - && annual_finance_state - .years_since_last_bankruptcy - .is_some_and(|years| years >= 5); - Some(RuntimeCompanyAnnualDeepDistressState { - company_id, - bankruptcy_allowed: runtime_world_bankruptcy_allowed(state), - years_since_founding: annual_finance_state.years_since_founding, - years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy, - current_cash, - recent_first_three_net_profit_years, - deep_distress_cash_floor, - deep_distress_net_profit_floor, - eligible_for_bankruptcy_fallback, - }) -} - -pub fn runtime_world_absolute_counter(state: &RuntimeState) -> Option { - state.world_restore.absolute_counter_raw_u32 -} - -pub fn runtime_world_partial_year_weight_numerator(state: &RuntimeState) -> Option { - Some(i64::from(state.world_restore.partial_year_progress_raw_u8?) * 5 - 5) -} - -pub fn runtime_decode_packed_calendar_tuple( - word_0: u32, - word_1: u32, -) -> RuntimePackedCalendarTuple { - let bytes_0 = word_0.to_le_bytes(); - let bytes_1 = word_1.to_le_bytes(); - RuntimePackedCalendarTuple { - year_word: u16::from_le_bytes([bytes_0[0], bytes_0[1]]), - month_1_based: bytes_0[2], - week_1_based: bytes_0[3], - day_1_based: bytes_1[0], - hour_0_based: bytes_1[1], - quarter_day_1_based: bytes_1[2], - minute_0_based: bytes_1[3], - } -} - -pub fn runtime_encode_packed_calendar_tuple(tuple: RuntimePackedCalendarTuple) -> (u32, u32) { - let year_bytes = tuple.year_word.to_le_bytes(); - let word_0 = u32::from_le_bytes([ - year_bytes[0], - year_bytes[1], - tuple.month_1_based, - tuple.week_1_based, - ]); - let word_1 = u32::from_le_bytes([ - tuple.day_1_based, - tuple.hour_0_based, - tuple.quarter_day_1_based, - tuple.minute_0_based, - ]); - (word_0, word_1) -} - -pub fn runtime_derive_packed_calendar_tuple_from_calendar_point( - calendar: CalendarPoint, -) -> Option { - let year_word = u16::try_from(calendar.year).ok()?; - let month_1_based = u8::try_from(calendar.month_slot.checked_add(1)?).ok()?; - let day_1_based = u8::try_from(calendar.phase_slot.checked_add(1)?).ok()?; - let week_1_based = u8::try_from(calendar.phase_slot / 7 + 1).ok()?; - let total_minutes = calendar.tick_slot.checked_mul(1440)? / crate::TICKS_PER_PHASE; - let hour_0_based = u8::try_from(total_minutes / 60).ok()?; - let minute_0_based = u8::try_from(total_minutes % 60).ok()?; - let quarter_day_1_based = u8::try_from((u32::from(hour_0_based) / 6) + 1).ok()?; - Some(RuntimePackedCalendarTuple { - year_word, - month_1_based, - week_1_based, - day_1_based, - hour_0_based, - quarter_day_1_based, - minute_0_based, - }) -} - -pub fn runtime_pack_packed_calendar_tuple_to_absolute_counter( - tuple: RuntimePackedCalendarTuple, -) -> Option { - if !(1..=12).contains(&tuple.month_1_based) { - return None; - } - if !(1..=28).contains(&tuple.day_1_based) { - return None; - } - if tuple.hour_0_based >= 24 { - return None; - } - if tuple.minute_0_based >= 60 { - return None; - } - - let year = u64::from(tuple.year_word); - let month = u64::from(tuple.month_1_based); - let day = u64::from(tuple.day_1_based); - let hour = u64::from(tuple.hour_0_based); - let minute = u64::from(tuple.minute_0_based); - let absolute_counter = - ((((year * 12 + month) * 28 + day).checked_sub(29)? * 24 + hour) * 60) + minute; - u32::try_from(absolute_counter).ok() -} - -pub fn runtime_company_unassigned_share_pool(state: &RuntimeState, company_id: u32) -> Option { - let outstanding_shares = state - .service_state - .company_market_state - .get(&company_id)? - .outstanding_shares; - let assigned_shares = runtime_company_assigned_share_pool(state, company_id)?; - Some(outstanding_shares.saturating_sub(assigned_shares)) -} - -pub fn runtime_company_assigned_share_pool(state: &RuntimeState, company_id: u32) -> Option { - state.service_state.company_market_state.get(&company_id)?; - Some( - state - .chairman_profiles - .iter() - .filter_map(|profile| profile.company_holdings.get(&company_id).copied()) - .sum::(), - ) -} - -pub fn runtime_company_annual_finance_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - let market_state = state.service_state.company_market_state.get(&company_id)?; - let periodic_service_state = runtime_company_periodic_service_state(state, company_id); - let assigned_share_pool = runtime_company_assigned_share_pool(state, company_id)?; - let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id)?; - let years_since_founding = - derive_runtime_company_elapsed_years(state.calendar.year, market_state.founding_year); - let years_since_last_bankruptcy = derive_runtime_company_elapsed_years( - state.calendar.year, - market_state.last_bankruptcy_year, - ); - let years_since_last_dividend = - derive_runtime_company_elapsed_years(state.calendar.year, market_state.last_dividend_year); - let current_issue_absolute_counter = runtime_pack_packed_calendar_tuple_to_absolute_counter( - runtime_decode_packed_calendar_tuple( - market_state.current_issue_calendar_word, - market_state.current_issue_calendar_word_2, - ), - ); - let prior_issue_absolute_counter = runtime_pack_packed_calendar_tuple_to_absolute_counter( - runtime_decode_packed_calendar_tuple( - market_state.prior_issue_calendar_word, - market_state.prior_issue_calendar_word_2, - ), - ); - let current_issue_age_absolute_counter_delta = match ( - runtime_world_absolute_counter(state), - current_issue_absolute_counter, - ) { - (Some(world_counter), Some(issue_counter)) if world_counter >= issue_counter => { - Some(i64::from(world_counter - issue_counter)) - } - _ => None, - }; - let (trailing_full_year_year_words, trailing_full_year_net_profits) = - runtime_company_trailing_full_year_stat_series(state, company_id, 0x2b, 4) - .unwrap_or_default(); - let (_, trailing_full_year_revenues) = - runtime_company_trailing_full_year_stat_series(state, company_id, 0x2c, 4) - .unwrap_or_default(); - let (_, trailing_full_year_fuel_costs) = - runtime_company_trailing_full_year_stat_series(state, company_id, 0x09, 4) - .unwrap_or_default(); - Some(RuntimeCompanyAnnualFinanceState { - company_id, - outstanding_shares: market_state.outstanding_shares, - bond_count: market_state.bond_count, - largest_live_bond_principal: market_state.largest_live_bond_principal, - highest_coupon_live_bond_principal: market_state.highest_coupon_live_bond_principal, - live_bond_coupon_burden_total: runtime_company_live_bond_coupon_burden_total( - state, company_id, - ), - assigned_share_pool, - unassigned_share_pool, - cached_share_price: rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32), - chairman_salary_baseline: market_state.chairman_salary_baseline, - chairman_salary_current: market_state.chairman_salary_current, - chairman_bonus_year: market_state.chairman_bonus_year, - chairman_bonus_amount: market_state.chairman_bonus_amount, - founding_year: market_state.founding_year, - last_bankruptcy_year: market_state.last_bankruptcy_year, - last_dividend_year: market_state.last_dividend_year, - years_since_founding, - years_since_last_bankruptcy, - years_since_last_dividend, - current_partial_year_weight_numerator: runtime_world_partial_year_weight_numerator(state), - trailing_full_year_year_words, - trailing_full_year_net_profits, - trailing_full_year_revenues, - trailing_full_year_fuel_costs, - current_issue_absolute_counter, - prior_issue_absolute_counter, - current_issue_age_absolute_counter_delta, - current_issue_calendar_word: market_state.current_issue_calendar_word, - current_issue_calendar_word_2: market_state.current_issue_calendar_word_2, - prior_issue_calendar_word: market_state.prior_issue_calendar_word, - prior_issue_calendar_word_2: market_state.prior_issue_calendar_word_2, - preferred_locomotive_engine_type_raw_u8: periodic_service_state - .as_ref() - .and_then(|service_state| service_state.preferred_locomotive_engine_type_raw_u8), - city_connection_latch: periodic_service_state - .as_ref() - .map(|service_state| service_state.city_connection_latch) - .unwrap_or(market_state.city_connection_latch), - linked_transit_latch: periodic_service_state - .as_ref() - .map(|service_state| service_state.linked_transit_latch) - .unwrap_or(market_state.linked_transit_latch), - }) -} - -pub fn runtime_company_periodic_service_state( - state: &RuntimeState, - company_id: u32, -) -> Option { - const DEFAULT_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS: i64 = 140; - const ELECTRIC_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS: i64 = 180; - const ELECTRIC_ENGINE_TYPE_RAW_U8: u8 = 2; - - let market_state = state.service_state.company_market_state.get(&company_id)?; - let periodic_side_latch_state = state - .service_state - .company_periodic_side_latch_state - .get(&company_id); - let preferred_locomotive_engine_type_raw_u8 = periodic_side_latch_state - .and_then(|latch_state| latch_state.preferred_locomotive_engine_type_raw_u8); - let base_route_preference_raw_u8 = state.world_restore.auto_show_grade_during_track_lay_raw_u8; - let electric_route_preference_override_active = - preferred_locomotive_engine_type_raw_u8 == Some(ELECTRIC_ENGINE_TYPE_RAW_U8); - let effective_route_preference_raw_u8 = if electric_route_preference_override_active { - Some(ELECTRIC_ENGINE_TYPE_RAW_U8) - } else { - base_route_preference_raw_u8 - }; - Some(RuntimeCompanyPeriodicServiceState { - company_id, - preferred_locomotive_engine_type_raw_u8, - city_connection_latch: periodic_side_latch_state - .map(|latch_state| latch_state.city_connection_latch) - .unwrap_or(market_state.city_connection_latch), - linked_transit_latch: periodic_side_latch_state - .map(|latch_state| latch_state.linked_transit_latch) - .unwrap_or(market_state.linked_transit_latch), - base_route_preference_raw_u8, - effective_route_preference_raw_u8, - electric_route_preference_override_active, - effective_route_quality_multiplier_basis_points: - if electric_route_preference_override_active { - ELECTRIC_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS - } else { - DEFAULT_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS - }, - }) -} - -pub fn runtime_begin_company_periodic_route_preference_override( - state: &mut RuntimeState, - company_id: u32, -) -> Option { - let periodic_service_state = runtime_company_periodic_service_state(state, company_id)?; - if !periodic_service_state.electric_route_preference_override_active { - return None; - } - let override_state = RuntimeWorldRoutePreferenceOverrideState { - company_id, - base_route_preference_raw_u8: periodic_service_state.base_route_preference_raw_u8, - effective_route_preference_raw_u8: periodic_service_state.effective_route_preference_raw_u8, - electric_route_preference_override_active: true, - }; - state.world_restore.auto_show_grade_during_track_lay_raw_u8 = - override_state.effective_route_preference_raw_u8; - state - .service_state - .active_periodic_route_preference_override = Some(override_state.clone()); - state.service_state.last_periodic_route_preference_override = Some(override_state.clone()); - state - .service_state - .periodic_route_preference_override_apply_count += 1; - Some(override_state) -} - -pub fn runtime_end_company_periodic_route_preference_override( - state: &mut RuntimeState, -) -> Option { - let override_state = state - .service_state - .active_periodic_route_preference_override - .take()?; - state.world_restore.auto_show_grade_during_track_lay_raw_u8 = - override_state.base_route_preference_raw_u8; - state - .service_state - .periodic_route_preference_override_restore_count += 1; - Some(override_state) -} - -pub fn runtime_company_market_value( - state: &RuntimeState, - company_id: u32, - metric: RuntimeCompanyMarketMetric, -) -> Option { - let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; - match metric { - RuntimeCompanyMarketMetric::OutstandingShares => { - Some(annual_finance_state.outstanding_shares as i64) - } - RuntimeCompanyMarketMetric::BondCount => Some(annual_finance_state.bond_count as i64), - RuntimeCompanyMarketMetric::LargestLiveBondPrincipal => annual_finance_state - .largest_live_bond_principal - .map(|value| value as i64), - RuntimeCompanyMarketMetric::HighestCouponLiveBondPrincipal => annual_finance_state - .highest_coupon_live_bond_principal - .map(|value| value as i64), - RuntimeCompanyMarketMetric::AssignedSharePool => { - Some(annual_finance_state.assigned_share_pool as i64) - } - RuntimeCompanyMarketMetric::UnassignedSharePool => { - Some(annual_finance_state.unassigned_share_pool as i64) - } - RuntimeCompanyMarketMetric::CachedSharePrice => annual_finance_state.cached_share_price, - RuntimeCompanyMarketMetric::ChairmanSalaryBaseline => { - Some(annual_finance_state.chairman_salary_baseline as i64) - } - RuntimeCompanyMarketMetric::ChairmanSalaryCurrent => { - Some(annual_finance_state.chairman_salary_current as i64) - } - RuntimeCompanyMarketMetric::ChairmanBonusAmount => { - Some(annual_finance_state.chairman_bonus_amount as i64) - } - RuntimeCompanyMarketMetric::CurrentIssueAbsoluteCounter => annual_finance_state - .current_issue_absolute_counter - .map(i64::from), - RuntimeCompanyMarketMetric::PriorIssueAbsoluteCounter => annual_finance_state - .prior_issue_absolute_counter - .map(i64::from), - RuntimeCompanyMarketMetric::CurrentIssueAgeAbsoluteCounterDelta => { - annual_finance_state.current_issue_age_absolute_counter_delta - } - RuntimeCompanyMarketMetric::CurrentIssueCalendarWord => { - Some(annual_finance_state.current_issue_calendar_word as i64) - } - RuntimeCompanyMarketMetric::CurrentIssueCalendarWord2 => { - Some(annual_finance_state.current_issue_calendar_word_2 as i64) - } - RuntimeCompanyMarketMetric::PriorIssueCalendarWord => { - Some(annual_finance_state.prior_issue_calendar_word as i64) - } - RuntimeCompanyMarketMetric::PriorIssueCalendarWord2 => { - Some(annual_finance_state.prior_issue_calendar_word_2 as i64) - } - } -} - -fn rounded_cached_share_price_i64(raw_u32: u32) -> Option { - let value = f32::from_bits(raw_u32); - if !value.is_finite() { - return None; - } - if value < i64::MIN as f32 || value > i64::MAX as f32 { - return None; - } - Some(value.round() as i64) -} - -fn runtime_decode_saved_f64_bits(bits: u64) -> Option { - let value = f64::from_bits(bits); - if !value.is_finite() { - return None; - } - Some(value) -} - -pub(crate) fn runtime_round_f64_to_i64(value: f64) -> Option { - if !value.is_finite() { - return None; - } - if value < i64::MIN as f64 || value > i64::MAX as f64 { - return None; - } - Some(value.round() as i64) -} - -fn derive_runtime_company_elapsed_years(current_year: u32, prior_year: u32) -> Option { - if prior_year == 0 || prior_year > current_year { - return None; - } - Some(current_year - prior_year) -} - -fn derive_runtime_chairman_holdings_share_price_total( - holdings_by_company: &BTreeMap, - company_share_prices: &BTreeMap, -) -> Option { - let mut total = 0i64; - for (company_id, units) in holdings_by_company { - let share_price = *company_share_prices.get(company_id)?; - total = total.checked_add((*units as i64).checked_mul(share_price)?)?; - } - Some(total) -} - -fn validate_runtime_effect( - effect: &RuntimeEffect, - valid_company_ids: &BTreeSet, - valid_player_ids: &BTreeSet, - valid_chairman_profile_ids: &BTreeSet, - valid_territory_ids: &BTreeSet, -) -> Result<(), String> { - match effect { - RuntimeEffect::SetWorldFlag { key, .. } => { - if key.trim().is_empty() { - return Err("key must not be empty".to_string()); - } - } - RuntimeEffect::SetWorldVariable { index, .. } => { - if !(1..=4).contains(index) { - return Err(format!( - "world runtime variable index {index} must be in 1..=4" - )); - } - } - RuntimeEffect::SetWorldScalarOverride { key, .. } => { - if key.trim().is_empty() { - return Err("key must not be empty".to_string()); - } - } - RuntimeEffect::SetLimitedTrackBuildingAmount { .. } - | RuntimeEffect::SetEconomicStatusCode { .. } => {} - RuntimeEffect::SetCompanyVariable { target, index, .. } => { - validate_company_target(target, valid_company_ids)?; - if !(1..=4).contains(index) { - return Err(format!("runtime variable index {index} must be in 1..=4")); - } - } - RuntimeEffect::SetCompanyCash { target, .. } - | RuntimeEffect::ConfiscateCompanyAssets { target } - | RuntimeEffect::DeactivateCompany { target } - | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } - | RuntimeEffect::AdjustCompanyCash { target, .. } - | RuntimeEffect::AdjustCompanyDebt { target, .. } => { - validate_company_target(target, valid_company_ids)?; - } - RuntimeEffect::SetCompanyGovernanceScalar { target, metric, .. } => { - validate_company_target(target, valid_company_ids)?; - validate_company_governance_scalar_metric(*metric)?; - } - RuntimeEffect::SetCompanyTerritoryAccess { - target, territory, .. - } => { - validate_company_target(target, valid_company_ids)?; - validate_territory_target(territory, valid_territory_ids)?; - } - RuntimeEffect::SetPlayerVariable { target, index, .. } => { - validate_player_target(target, valid_player_ids)?; - if !(1..=4).contains(index) { - return Err(format!("runtime variable index {index} must be in 1..=4")); - } - } - RuntimeEffect::SetPlayerCash { target, .. } - | RuntimeEffect::DeactivatePlayer { target } => { - validate_player_target(target, valid_player_ids)?; - } - RuntimeEffect::SetChairmanCash { target, .. } - | RuntimeEffect::DeactivateChairman { target } => { - validate_chairman_target(target, valid_chairman_profile_ids)?; - } - RuntimeEffect::RetireTrains { - company_target, - territory_target, - locomotive_name, - } => { - if let Some(company_target) = company_target { - validate_company_target(company_target, valid_company_ids)?; - } - if let Some(territory_target) = territory_target { - validate_territory_target(territory_target, valid_territory_ids)?; - } - if company_target.is_none() && territory_target.is_none() && locomotive_name.is_none() { - return Err( - "retire_trains requires at least one company_target, territory_target, or locomotive_name filter" - .to_string(), - ); - } - if locomotive_name - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err("locomotive_name must not be empty".to_string()); - } - } - RuntimeEffect::SetCandidateAvailability { name, .. } => { - if name.trim().is_empty() { - return Err("name must not be empty".to_string()); - } - } - RuntimeEffect::SetNamedLocomotiveAvailability { name, .. } => { - if name.trim().is_empty() { - return Err("name must not be empty".to_string()); - } - } - RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, .. } => { - if name.trim().is_empty() { - return Err("name must not be empty".to_string()); - } - } - RuntimeEffect::SetNamedLocomotiveCost { name, .. } => { - if name.trim().is_empty() { - return Err("name must not be empty".to_string()); - } - } - RuntimeEffect::SetCargoPriceOverride { target, .. } => match target { - RuntimeCargoPriceTarget::All => {} - RuntimeCargoPriceTarget::Named { name } => { - if name.trim().is_empty() { - return Err("name must not be empty".to_string()); - } - } - }, - RuntimeEffect::SetCargoProductionOverride { target, .. } => match target { - RuntimeCargoProductionTarget::All - | RuntimeCargoProductionTarget::Factory - | RuntimeCargoProductionTarget::FarmMine => {} - RuntimeCargoProductionTarget::Named { name } => { - if name.trim().is_empty() { - return Err("name must not be empty".to_string()); - } - } - }, - RuntimeEffect::SetCargoProductionSlot { slot, .. } => { - if !(1..=11).contains(slot) { - return Err("slot must be in 1..=11".to_string()); - } - } - RuntimeEffect::SetTerritoryVariable { target, index, .. } => { - validate_territory_target(target, valid_territory_ids)?; - if !(1..=4).contains(index) { - return Err(format!("runtime variable index {index} must be in 1..=4")); - } - } - RuntimeEffect::SetTerritoryAccessCost { .. } => {} - RuntimeEffect::SetSpecialCondition { label, .. } => { - if label.trim().is_empty() { - return Err("label must not be empty".to_string()); - } - } - RuntimeEffect::AppendEventRecord { record } => { - validate_event_record_template( - record, - valid_company_ids, - valid_player_ids, - valid_chairman_profile_ids, - valid_territory_ids, - )?; - } - RuntimeEffect::ActivateEventRecord { .. } - | RuntimeEffect::DeactivateEventRecord { .. } - | RuntimeEffect::RemoveEventRecord { .. } => {} - } - - Ok(()) -} - -fn validate_event_record_template( - record: &RuntimeEventRecordTemplate, - valid_company_ids: &BTreeSet, - valid_player_ids: &BTreeSet, - valid_chairman_profile_ids: &BTreeSet, - valid_territory_ids: &BTreeSet, -) -> Result<(), String> { - for (condition_index, condition) in record.conditions.iter().enumerate() { - validate_runtime_condition( - condition, - valid_company_ids, - valid_player_ids, - valid_chairman_profile_ids, - valid_territory_ids, - ) - .map_err(|err| { - format!( - "template record_id={}.conditions[{condition_index}] {err}", - record.record_id - ) - })?; - } - for (effect_index, effect) in record.effects.iter().enumerate() { - validate_runtime_effect( - effect, - valid_company_ids, - valid_player_ids, - valid_chairman_profile_ids, - valid_territory_ids, - ) - .map_err(|err| { - format!( - "template record_id={}.effects[{effect_index}] {err}", - record.record_id - ) - })?; - } - - Ok(()) -} - -fn validate_runtime_condition( - condition: &RuntimeCondition, - valid_company_ids: &BTreeSet, - valid_player_ids: &BTreeSet, - valid_chairman_profile_ids: &BTreeSet, - valid_territory_ids: &BTreeSet, -) -> Result<(), String> { - match condition { - RuntimeCondition::WorldVariableThreshold { index, .. } => { - if !(1..=4).contains(index) { - Err("index must be in 1..=4".to_string()) - } else { - Ok(()) - } - } - RuntimeCondition::CompanyNumericThreshold { target, .. } => { - validate_company_target(target, valid_company_ids) - } - RuntimeCondition::CompanyVariableThreshold { target, index, .. } => { - validate_company_target(target, valid_company_ids)?; - if !(1..=4).contains(index) { - Err("index must be in 1..=4".to_string()) - } else { - Ok(()) - } - } - RuntimeCondition::ChairmanNumericThreshold { target, .. } => { - validate_chairman_target(target, valid_chairman_profile_ids) - } - RuntimeCondition::PlayerVariableThreshold { target, index, .. } => { - validate_player_target(target, valid_player_ids)?; - if !(1..=4).contains(index) { - Err("index must be in 1..=4".to_string()) - } else { - Ok(()) - } - } - RuntimeCondition::TerritoryNumericThreshold { target, .. } => { - validate_territory_target(target, valid_territory_ids) - } - RuntimeCondition::TerritoryVariableThreshold { target, index, .. } => { - validate_territory_target(target, valid_territory_ids)?; - if !(1..=4).contains(index) { - Err("index must be in 1..=4".to_string()) - } else { - Ok(()) - } - } - RuntimeCondition::CompanyTerritoryNumericThreshold { - target, territory, .. - } => { - validate_company_target(target, valid_company_ids)?; - validate_territory_target(territory, valid_territory_ids) - } - RuntimeCondition::SpecialConditionThreshold { label, .. } => { - if label.trim().is_empty() { - Err("label must not be empty".to_string()) - } else { - Ok(()) - } - } - RuntimeCondition::CandidateAvailabilityThreshold { name, .. } => { - if name.trim().is_empty() { - Err("name must not be empty".to_string()) - } else { - Ok(()) - } - } - RuntimeCondition::NamedLocomotiveAvailabilityThreshold { name, .. } - | RuntimeCondition::NamedLocomotiveCostThreshold { name, .. } => { - if name.trim().is_empty() { - Err("name must not be empty".to_string()) - } else { - Ok(()) - } - } - RuntimeCondition::CargoProductionSlotThreshold { slot, label, .. } => { - if !(1..=11).contains(slot) { - Err("slot must be in 1..=11".to_string()) - } else if label.trim().is_empty() { - Err("label must not be empty".to_string()) - } else { - Ok(()) - } - } - RuntimeCondition::CargoProductionTotalThreshold { .. } - | RuntimeCondition::FactoryProductionTotalThreshold { .. } - | RuntimeCondition::FarmMineProductionTotalThreshold { .. } - | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } - | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } - | RuntimeCondition::TerritoryAccessCostThreshold { .. } => Ok(()), - RuntimeCondition::EconomicStatusCodeThreshold { .. } => Ok(()), - RuntimeCondition::WorldFlagEquals { key, .. } => { - if key.trim().is_empty() { - Err("key must not be empty".to_string()) - } else { - Ok(()) - } - } - } -} - -fn validate_company_target( - target: &RuntimeCompanyTarget, - valid_company_ids: &BTreeSet, -) -> Result<(), String> { - match target { - RuntimeCompanyTarget::AllActive - | RuntimeCompanyTarget::HumanCompanies - | RuntimeCompanyTarget::AiCompanies - | RuntimeCompanyTarget::SelectedCompany - | RuntimeCompanyTarget::ConditionTrueCompany => Ok(()), - RuntimeCompanyTarget::Ids { ids } => { - if ids.is_empty() { - return Err("target ids must not be empty".to_string()); - } - for company_id in ids { - if !valid_company_ids.contains(company_id) { - return Err(format!("target references unknown company_id {company_id}")); - } - } - Ok(()) - } - } -} - -fn validate_player_target( - target: &RuntimePlayerTarget, - valid_player_ids: &BTreeSet, -) -> Result<(), String> { - match target { - RuntimePlayerTarget::AllActive - | RuntimePlayerTarget::HumanPlayers - | RuntimePlayerTarget::AiPlayers - | RuntimePlayerTarget::SelectedPlayer - | RuntimePlayerTarget::ConditionTruePlayer => Ok(()), - RuntimePlayerTarget::Ids { ids } => { - if ids.is_empty() { - return Err("target ids must not be empty".to_string()); - } - for player_id in ids { - if !valid_player_ids.contains(player_id) { - return Err(format!("target references unknown player_id {player_id}")); - } - } - Ok(()) - } - } -} - -fn validate_chairman_target( - target: &RuntimeChairmanTarget, - valid_chairman_profile_ids: &BTreeSet, -) -> Result<(), String> { - match target { - RuntimeChairmanTarget::AllActive - | RuntimeChairmanTarget::HumanChairmen - | RuntimeChairmanTarget::AiChairmen - | RuntimeChairmanTarget::SelectedChairman - | RuntimeChairmanTarget::ConditionTrueChairman => Ok(()), - RuntimeChairmanTarget::Ids { ids } => { - if ids.is_empty() { - return Err("target ids must not be empty".to_string()); - } - for profile_id in ids { - if !valid_chairman_profile_ids.contains(profile_id) { - return Err(format!( - "target references unknown chairman profile_id {profile_id}" - )); - } - } - Ok(()) - } - } -} - -fn validate_company_governance_scalar_metric(metric: RuntimeCompanyMetric) -> Result<(), String> { - match metric { - RuntimeCompanyMetric::CreditRating - | RuntimeCompanyMetric::PrimeRate - | RuntimeCompanyMetric::BookValuePerShare - | RuntimeCompanyMetric::InvestorConfidence - | RuntimeCompanyMetric::ManagementAttitude => Ok(()), - _ => Err( - "governance scalar effect requires a writable company governance metric".to_string(), - ), - } -} - -fn validate_territory_target( - target: &RuntimeTerritoryTarget, - valid_territory_ids: &BTreeSet, -) -> Result<(), String> { - match target { - RuntimeTerritoryTarget::AllTerritories => Ok(()), - RuntimeTerritoryTarget::Ids { ids } => { - if ids.is_empty() { - return Err("territory target ids must not be empty".to_string()); - } - for territory_id in ids { - if !valid_territory_ids.contains(territory_id) { - return Err(format!( - "territory target references unknown territory_id {territory_id}" - )); - } - } - Ok(()) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rejects_duplicate_company_ids() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - absolute_counter_raw_u32: Some(5), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![ - RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - }, - RuntimeCompany { - company_id: 1, - current_cash: 200, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - }, - ], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_partial_world_restore_without_year_lane() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - selected_year_profile_lane: None, - campaign_scenario_enabled: Some(false), - sandbox_enabled: Some(true), - seed_tuple_written_from_raw_lane: Some(true), - absolute_counter_requires_shell_context: Some(true), - absolute_counter_reconstructible_from_save: Some(false), - packed_year_word_raw_u16: None, - partial_year_progress_raw_u8: None, - current_calendar_tuple_word_raw_u32: None, - current_calendar_tuple_word_2_raw_u32: None, - absolute_counter_raw_u32: None, - absolute_counter_mirror_raw_u32: None, - disable_cargo_economy_special_condition_slot: Some(30), - disable_cargo_economy_special_condition_reconstructible_from_save: Some(true), - disable_cargo_economy_special_condition_write_side_grounded: Some(true), - disable_cargo_economy_special_condition_enabled: Some(false), - use_bio_accelerator_cars_enabled: Some(false), - use_wartime_cargos_enabled: Some(false), - disable_train_crashes_enabled: Some(false), - disable_train_crashes_and_breakdowns_enabled: Some(false), - ai_ignore_territories_at_startup_enabled: Some(false), - limited_track_building_amount: None, - economic_status_code: None, - territory_access_cost: None, - linked_site_removal_follow_on_gate_raw_u8: None, - linked_site_removal_follow_on_gate_enabled: None, - auto_show_grade_during_track_lay_raw_u8: None, - starting_building_density_level_raw_u8: None, - post_text_building_density_growth_raw_u8: None, - leftover_simulation_time_accumulator_raw_u32: None, - leftover_simulation_time_accumulator_value_f32_text: None, - selected_year_lane_snapshot_raw_u8: None, - all_steam_locomotives_available_raw_u8: None, - all_steam_locomotives_available_enabled: None, - all_diesel_locomotives_available_raw_u8: None, - all_diesel_locomotives_available_enabled: None, - all_electric_locomotives_available_raw_u8: None, - all_electric_locomotives_available_enabled: None, - cached_available_locomotive_rating_raw_u32: None, - cached_available_locomotive_rating_value_f32_text: None, - issue_37_value: None, - issue_38_value: None, - issue_39_value: None, - issue_3a_value: None, - issue_37_multiplier_raw_u32: None, - issue_37_multiplier_value_f32_text: None, - stock_issue_and_buyback_policy_raw_u8: None, - bond_issue_and_repayment_policy_raw_u8: None, - bankruptcy_policy_raw_u8: None, - dividend_policy_raw_u8: None, - building_density_growth_setting_raw_u32: None, - stock_issue_and_buyback_allowed: None, - bond_issue_and_repayment_allowed: None, - bankruptcy_allowed: None, - dividend_adjustment_allowed: None, - finance_neighborhood_candidates: Vec::new(), - economic_tuning_mirror_raw_u32: None, - economic_tuning_mirror_value_f32_text: None, - economic_tuning_lane_raw_u32: Vec::new(), - economic_tuning_lane_value_f32_text: Vec::new(), - selected_year_bucket_scalar_raw_u32: None, - selected_year_bucket_scalar_value_f32_text: None, - selected_year_bucket_direct_lane_raw_u32: Vec::new(), - selected_year_bucket_direct_lane_value_f32_text: Vec::new(), - selected_year_bucket_complement_lane_raw_u32: Vec::new(), - selected_year_bucket_complement_lane_value_f32_text: Vec::new(), - selected_year_bucket_scaled_companion_lane_raw_u32: Vec::new(), - selected_year_bucket_scaled_companion_lane_value_f32_text: Vec::new(), - selected_year_gap_scalar_raw_u32: None, - selected_year_gap_scalar_value_f32_text: None, - absolute_counter_restore_kind: Some( - "mode-adjusted-selected-year-lane".to_string(), - ), - absolute_counter_adjustment_context: Some( - "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30".to_string(), - ), - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_event_effect_targeting_unknown_company() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - absolute_counter_raw_u32: Some(5), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 7, - trigger_kind: 1, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::Ids { ids: vec![2] }, - delta: 50, - }], - }], - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_template_effect_targeting_unknown_company() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - absolute_counter_raw_u32: Some(5), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 7, - trigger_kind: 1, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AppendEventRecord { - record: Box::new(RuntimeEventRecordTemplate { - record_id: 8, - trigger_kind: 0x0a, - active: true, - marks_collection_dirty: false, - one_shot: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::Ids { ids: vec![2] }, - delta: 50, - }], - }), - }], - }], - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_invalid_packed_event_collection_summary() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: Some(RuntimePackedEventCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 4, - live_record_count: 2, - live_entry_ids: vec![3, 3], - decoded_record_count: 0, - imported_runtime_record_count: 0, - records: vec![ - RuntimePackedEventRecordSummary { - record_index: 0, - live_entry_id: 3, - payload_offset: None, - payload_len: None, - decode_status: "unsupported_framing".to_string(), - payload_family: "unsupported_framing".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: None, - notes: vec!["test".to_string()], - }, - RuntimePackedEventRecordSummary { - record_index: 1, - live_entry_id: 3, - payload_offset: None, - payload_len: None, - decode_status: "unsupported_framing".to_string(), - payload_family: "unsupported_framing".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: None, - notes: vec!["test".to_string()], - }, - ], - }), - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_selected_company_id_that_does_not_exist() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: Some(2), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_selected_company_id_that_is_inactive() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: false, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: Some(1), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_duplicate_train_ids() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: vec![ - RuntimeTrain { - train_id: 7, - owner_company_id: 1, - territory_id: None, - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 7, - owner_company_id: 1, - territory_id: None, - locomotive_name: Some("Orca".to_string()), - active: true, - retired: false, - }, - ], - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_train_with_unknown_owner_company() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: vec![RuntimeTrain { - train_id: 7, - owner_company_id: 2, - territory_id: None, - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }], - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_train_with_unknown_territory() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: vec![RuntimeTrain { - train_id: 7, - owner_company_id: 1, - territory_id: Some(9), - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }], - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: vec![RuntimeTerritory { - territory_id: 1, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_train_marked_active_and_retired() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: vec![RuntimeTrain { - train_id: 7, - owner_company_id: 1, - territory_id: None, - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: true, - }], - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_duplicate_company_territory_access_pairs() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: vec![RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - company_territory_track_piece_counts: Vec::new(), - company_territory_access: vec![ - RuntimeCompanyTerritoryAccess { - company_id: 1, - territory_id: 7, - }, - RuntimeCompanyTerritoryAccess { - company_id: 1, - territory_id: 7, - }, - ], - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_company_territory_access_with_unknown_company() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: vec![RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - company_territory_track_piece_counts: Vec::new(), - company_territory_access: vec![RuntimeCompanyTerritoryAccess { - company_id: 2, - territory_id: 7, - }], - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_company_territory_access_with_unknown_territory() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: vec![RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - company_territory_track_piece_counts: Vec::new(), - company_territory_access: vec![RuntimeCompanyTerritoryAccess { - company_id: 1, - territory_id: 8, - }], - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_company_with_unknown_linked_chairman_profile() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: Some(9), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn rejects_mismatched_company_chairman_back_links() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: Some(1), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 0, - linked_company_id: None, - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert!(state.validate().is_err()); - } - - #[test] - fn refreshes_chairman_totals_from_company_market_state() { - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![ - RuntimeCompany { - company_id: 1, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }, - RuntimeCompany { - company_id: 2, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }, - ], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 100, - linked_company_id: Some(1), - company_holdings: BTreeMap::from([(1, 2), (2, 3)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 400, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([ - ( - 1, - RuntimeCompanyMarketState { - cached_share_price_raw_u32: 0x41200000, - ..RuntimeCompanyMarketState::default() - }, - ), - ( - 2, - RuntimeCompanyMarketState { - cached_share_price_raw_u32: 0x41a00000, - ..RuntimeCompanyMarketState::default() - }, - ), - ]), - ..RuntimeServiceState::default() - }, - }; - - state.refresh_derived_market_state(); - - assert_eq!(state.chairman_profiles[0].holdings_value_total, 80); - assert_eq!(state.chairman_profiles[0].net_worth_total, 180); - assert_eq!(state.chairman_profiles[0].purchasing_power_total, 400); - } - - #[test] - fn refreshes_chairman_purchasing_power_when_cash_changes() { - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 50, - linked_company_id: Some(1), - company_holdings: BTreeMap::from([(1, 2)]), - holdings_value_total: 20, - net_worth_total: 70, - purchasing_power_total: 130, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 1, - RuntimeCompanyMarketState { - cached_share_price_raw_u32: 0x41200000, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - state.chairman_profiles[0].current_cash = 80; - - state.refresh_derived_market_state(); - - assert_eq!(state.chairman_profiles[0].holdings_value_total, 20); - assert_eq!(state.chairman_profiles[0].net_worth_total, 100); - assert_eq!(state.chairman_profiles[0].purchasing_power_total, 130); - } - - #[test] - fn refreshes_company_leaf_fields_from_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = 275.0f64.to_bits(); - - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - issue_37_value: Some(5.0f32.to_bits()), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: { - let mut terms = vec![0; 0x3b]; - terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100; - terms - }, - company_market_state: BTreeMap::from([( - 1, - RuntimeCompanyMarketState { - cached_share_price_raw_u32: 37.0f32.to_bits(), - issue_opinion_terms_raw_i32: { - let mut terms = vec![0; 0x3b]; - terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 58; - terms - }, - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x32f, - 2620.0f32.to_bits(), - )]), - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - state.refresh_derived_market_state(); - - assert_eq!(state.companies[0].current_cash, 275); - assert_eq!(state.companies[0].book_value_per_share, 2620); - assert_eq!(state.companies[0].prime_rate, Some(6)); - assert_eq!(state.companies[0].investor_confidence, 37); - assert_eq!(state.companies[0].management_attitude, 58); - } - - #[test] - fn seeds_company_periodic_side_latch_state_from_company_market_state() { - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - }], - selected_company_id: Some(1), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 1, - RuntimeCompanyMarketState { - city_connection_latch: true, - linked_transit_latch: false, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - state.refresh_derived_market_state(); - - assert_eq!( - state - .service_state - .company_periodic_side_latch_state - .get(&1), - Some(&RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: None, - city_connection_latch: true, - linked_transit_latch: false, - }) - ); - } - - #[test] - fn preserves_company_periodic_side_latch_state_without_market_projection() { - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - }], - selected_company_id: Some(1), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_periodic_side_latch_state: BTreeMap::from([( - 1, - RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: Some(2), - city_connection_latch: true, - linked_transit_latch: false, - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - state.refresh_derived_market_state(); - - assert_eq!( - state - .service_state - .company_periodic_side_latch_state - .get(&1), - Some(&RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: Some(2), - city_connection_latch: true, - linked_transit_latch: false, - }) - ); - } - - #[test] - fn reads_grounded_company_stat_family_slots_from_runtime_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = - 75.0f64.to_bits(); - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 125_000, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 2_620, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!( - runtime_company_stat_value( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - }, - ), - Some(125_000) - ); - assert_eq!( - runtime_company_stat_value( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, - }, - ), - Some(2_620) - ); - assert_eq!( - runtime_company_stat_value( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: 0x12, - }, - ), - Some(75) - ); - assert_eq!( - runtime_company_stat_value( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: 0x2b, - }, - ), - Some(0) - ); - assert_eq!( - runtime_company_stat_value( - &state, - 99, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - }, - ), - None - ); - } - - #[test] - fn reads_book_value_per_share_from_rehosted_direct_company_field_band() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x32f, - 2620.0f32.to_bits(), - )]), - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!(runtime_company_book_value_per_share(&state, 7), Some(2620)); - assert_eq!( - runtime_company_stat_value( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, - }, - ), - Some(2620) - ); - } - - #[test] - fn reads_investor_confidence_from_rehosted_company_share_price_cache() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - absolute_counter_raw_u32: Some(5), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - recent_per_share_cache_absolute_counter: 5, - recent_per_share_cached_value_bits: 14.5f64.to_bits(), - recent_per_share_subscore_raw_u32: 12.0f32.to_bits(), - cached_share_price_raw_u32: 37.0f32.to_bits(), - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!( - runtime_company_recent_per_share_subscore(&state, 7), - Some(14.5) - ); - assert_eq!(runtime_company_investor_confidence(&state, 7), Some(37)); - assert_eq!( - runtime_company_stat_value( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: 0x13, - }, - ), - Some(37) - ); - } - - #[test] - fn derives_company_management_attitude_from_issue3a_owner_state() { - let mut company_terms = vec![0; 0x3b]; - company_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 12; - let mut chairman_terms = vec![0; 0x3b]; - chairman_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 6; - let mut world_terms = vec![0; 0x3b]; - world_terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 40; - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: Some(3), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 3, - name: "Chairman".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(7), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: world_terms, - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - issue_opinion_terms_raw_i32: company_terms, - ..RuntimeCompanyMarketState::default() - }, - )]), - chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(3, chairman_terms)]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!(runtime_company_management_attitude(&state, 7), Some(58)); - } - - #[test] - fn reads_year_relative_company_stat_family_from_saved_market_matrix() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_year_value = |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10.0); - write_year_value(&mut year_stat_family_qword_bits, 0x02, 1, 20.0); - write_year_value(&mut year_stat_family_qword_bits, 0x03, 1, 30.0); - write_year_value(&mut year_stat_family_qword_bits, 0x04, 1, 40.0); - write_year_value(&mut year_stat_family_qword_bits, 0x05, 1, 5.0); - write_year_value(&mut year_stat_family_qword_bits, 0x06, 1, 6.0); - write_year_value(&mut year_stat_family_qword_bits, 0x07, 1, 7.0); - write_year_value(&mut year_stat_family_qword_bits, 0x08, 1, 8.0); - write_year_value(&mut year_stat_family_qword_bits, 0x09, 1, 9.0); - write_year_value(&mut year_stat_family_qword_bits, 0x0a, 1, 10.0); - write_year_value(&mut year_stat_family_qword_bits, 0x0b, 1, 11.0); - write_year_value(&mut year_stat_family_qword_bits, 0x0c, 1, 12.0); - write_year_value(&mut year_stat_family_qword_bits, 0x0d, 1, 13.0); - write_year_value(&mut year_stat_family_qword_bits, 0x0e, 1, 14.0); - write_year_value(&mut year_stat_family_qword_bits, 0x0f, 1, 15.0); - write_year_value(&mut year_stat_family_qword_bits, 0x10, 1, 16.0); - write_year_value(&mut year_stat_family_qword_bits, 0x11, 1, 17.0); - write_year_value(&mut year_stat_family_qword_bits, 0x12, 1, 18.0); - write_year_value(&mut year_stat_family_qword_bits, 0x16, 1, 4.0); - write_year_value(&mut year_stat_family_qword_bits, 0x17, 1, 10.0); - write_year_value(&mut year_stat_family_qword_bits, 0x18, 1, 20.0); - write_year_value(&mut year_stat_family_qword_bits, 0x19, 1, 25.0); - write_year_value(&mut year_stat_family_qword_bits, 0x1a, 1, 50.0); - write_year_value(&mut year_stat_family_qword_bits, 0x1b, 1, 100.0); - write_year_value(&mut year_stat_family_qword_bits, 0x24, 1, 5.0); - - let mut special_stat_family_232a_qword_bits = - vec![0u64; RUNTIME_COMPANY_STAT_SLOT_COUNT as usize]; - special_stat_family_232a_qword_bits[RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize] = - 111.0f64.to_bits(); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 125_000, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 2_620, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - year_stat_family_qword_bits, - special_stat_family_232a_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let prior_year = RuntimeCompanyStatSelector { - family_id: 1844, - slot_id: 0x09, - }; - assert_eq!( - runtime_company_stat_value_f64(&state, 7, prior_year), - Some(9.0) - ); - assert_eq!( - runtime_company_stat_value_f64( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: 1844, - slot_id: 0x2c, - }, - ), - Some(100.0) - ); - assert_eq!( - runtime_company_stat_value_f64( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: 1844, - slot_id: 0x2b, - }, - ), - Some(168.0) - ); - assert_eq!( - runtime_company_stat_value_f64( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: 1844, - slot_id: 0x32, - }, - ), - Some(20.0) - ); - assert_eq!( - runtime_company_stat_value_f64( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: 1844, - slot_id: 0x38, - }, - ), - Some(1.0) - ); - assert_eq!( - runtime_company_stat_value_f64( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A, - slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - }, - ), - Some(111.0) - ); - } - - #[test] - fn carries_trailing_full_year_finance_lanes_into_annual_finance_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_year_value = |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - for (year_delta, revenue_parts, extra_profit_parts, fuel_cost) in [ - ( - 1, - [60.0, 50.0, 40.0, 30.0], - [10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0], - 18.0, - ), - ( - 2, - [50.0, 45.0, 40.0, 35.0], - [9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0], - 17.0, - ), - ( - 3, - [50.0, 40.0, 35.0, 35.0], - [8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0], - 16.0, - ), - ( - 4, - [45.0, 40.0, 35.0, 30.0], - [7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0], - 15.0, - ), - ] { - for (slot_id, value) in [0x01, 0x02, 0x03, 0x04].into_iter().zip(revenue_parts) { - write_year_value(&mut year_stat_family_qword_bits, slot_id, year_delta, value); - } - for (slot_id, value) in [0x05, 0x06, 0x07, 0x08, 0x0a, 0x0b, 0x0c] - .into_iter() - .zip(extra_profit_parts) - { - write_year_value(&mut year_stat_family_qword_bits, slot_id, year_delta, value); - } - write_year_value( - &mut year_stat_family_qword_bits, - 0x09, - year_delta, - fuel_cost, - ); - } - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 125_000, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 2_620, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - outstanding_shares: 20_000, - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let finance_state = - runtime_company_annual_finance_state(&state, 7).expect("annual finance state"); - assert_eq!( - finance_state.trailing_full_year_year_words, - vec![1844, 1843, 1842, 1841] - ); - assert_eq!( - finance_state.trailing_full_year_net_profits, - vec![247, 229, 211, 193] - ); - assert_eq!( - finance_state.trailing_full_year_revenues, - vec![180, 170, 160, 150] - ); - assert_eq!( - finance_state.trailing_full_year_fuel_costs, - vec![18, 17, 16, 15] - ); - } - - #[test] - fn reads_grounded_world_issue_state_from_runtime_restore_state() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - issue_37_value: Some(3), - issue_38_value: Some(1), - issue_39_value: Some(2), - issue_3a_value: Some(4), - issue_37_multiplier_raw_u32: Some(0x3d75c28f), - issue_37_multiplier_value_f32_text: Some("0.060000".to_string()), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let issue = runtime_world_issue_state(&state, RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE) - .expect("grounded issue 0x37 state"); - assert_eq!(issue.issue_id, RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE); - assert_eq!(issue.raw_value_u32, 3); - assert_eq!(issue.multiplier_raw_u32, Some(0x3d75c28f)); - assert_eq!(issue.multiplier_value_f32_text.as_deref(), Some("0.060000")); - assert_eq!( - runtime_world_issue_state(&state, RUNTIME_WORLD_ISSUE_CREDIT_MARKET) - .expect("grounded issue 0x38 state") - .raw_value_u32, - 1 - ); - assert_eq!( - runtime_world_issue_state(&state, RUNTIME_WORLD_ISSUE_PRIME_RATE) - .expect("grounded issue 0x39 state") - .raw_value_u32, - 2 - ); - assert_eq!( - runtime_world_issue_state(&state, RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE) - .expect("grounded issue 0x3a state") - .raw_value_u32, - 4 - ); - assert_eq!(runtime_world_issue_state(&state, 0x40), None); - assert_eq!(runtime_world_absolute_counter(&state), None); - } - - #[test] - fn sums_save_native_issue_opinion_terms_with_linked_company_fallback() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: Some(3), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 3, - name: "Chairman".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(7), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: (0..0x3b) - .map(|value| value as i32) - .collect(), - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - issue_opinion_terms_raw_i32: (0..0x3b) - .map(|value| (value as i32) * 2) - .collect(), - ..RuntimeCompanyMarketState::default() - }, - )]), - chairman_issue_opinion_terms_raw_i32: BTreeMap::from([( - 3, - (0..0x3b).map(|value| (value as i32) * 3).collect(), - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!( - runtime_world_issue_opinion_term_sum_raw(&state, 0x39, Some(3), None, None), - Some(57 + 114 + 171) - ); - assert_eq!( - runtime_world_issue_opinion_term_sum_raw(&state, 0x39, None, Some(7), None), - Some(57 + 114) - ); - } - - #[test] - fn computes_save_native_issue_opinion_multiplier_with_floor() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 5, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 9, - name: "Chairman".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(5), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: { - let mut values = vec![0; 0x3b]; - values[0x38] = -150; - values - }, - company_market_state: BTreeMap::from([( - 5, - RuntimeCompanyMarketState { - issue_opinion_terms_raw_i32: { - let mut values = vec![0; 0x3b]; - values[0x38] = -60; - values - }, - ..RuntimeCompanyMarketState::default() - }, - )]), - chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(9, { - let mut values = vec![0; 0x3b]; - values[0x38] = 50; - values - })]), - ..RuntimeServiceState::default() - }, - }; - - let multiplier = runtime_world_issue_opinion_multiplier(&state, 0x38, Some(9), None, None) - .expect("issue multiplier"); - assert!((multiplier - 0.01).abs() < f64::EPSILON); - } - - #[test] - fn reads_grounded_world_absolute_counter_from_runtime_restore_state() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - absolute_counter_raw_u32: Some(5), - absolute_counter_mirror_raw_u32: Some(5), - packed_year_word_raw_u16: Some(0x0210), - partial_year_progress_raw_u8: Some(8), - current_calendar_tuple_word_raw_u32: Some(0x0108_0210), - current_calendar_tuple_word_2_raw_u32: Some(0x35e6_3160), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert_eq!(runtime_world_absolute_counter(&state), Some(5)); - assert_eq!( - runtime_world_partial_year_weight_numerator(&state), - Some(35) - ); - } - - #[test] - fn derives_prime_rate_baseline_from_saved_world_raw_word() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - issue_37_value: Some(5.0f32.to_bits()), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - assert_eq!(runtime_world_prime_rate_baseline(&state), Some(5.0)); - } - - #[test] - fn derives_company_prime_rate_from_issue39_owner_state() { - let mut company_terms = vec![0; 0x3b]; - company_terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 50; - let mut chairman_terms = vec![0; 0x3b]; - chairman_terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 25; - let mut world_terms = vec![0; 0x3b]; - world_terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100; - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - issue_37_value: Some(5.0f32.to_bits()), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: Some(3), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 3, - name: "Chairman".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(7), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: world_terms, - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - issue_opinion_terms_raw_i32: company_terms, - ..RuntimeCompanyMarketState::default() - }, - )]), - chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(3, chairman_terms)]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!(runtime_company_prime_rate(&state, 7), Some(7)); - } - - #[test] - fn derives_company_credit_rating_from_rehosted_finance_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = - 20.0f64.to_bits(); - year_stat_family_qword_bits[(0x01 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = - 100.0f64.to_bits(); - year_stat_family_qword_bits[(0x09 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = - 0.0f64.to_bits(); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1835, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - issue_37_value: Some(5.0f32.to_bits()), - issue_38_value: Some(2), - packed_year_word_raw_u16: Some(1835), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1830, - last_bankruptcy_year: 1800, - year_stat_family_qword_bits, - live_bond_slots: vec![RuntimeCompanyBondSlot { - slot_index: 0, - principal: 100_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.05f32.to_bits(), - }], - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let annual_finance_state = - runtime_company_annual_finance_state(&state, 7).expect("annual finance state"); - assert_eq!( - annual_finance_state.trailing_full_year_net_profits, - vec![100, 0, 0, 0] - ); - assert_eq!( - runtime_company_control_transfer_stat_value_f64(&state, 7, 0x12), - Some(20.0) - ); - assert_eq!( - runtime_company_derived_stat_value_f64( - &state, - 7, - RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - 0x30, - ), - Some(100.0) - ); - assert_eq!( - runtime_company_derived_stat_value_f64( - &state, - 7, - RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - 0x31, - ), - Some(120.0) - ); - let average_live_bond_coupon = - runtime_company_average_live_bond_coupon(&state, 7).expect("average coupon"); - assert!((average_live_bond_coupon - 0.05).abs() < 1e-6); - assert_eq!( - annual_finance_state.live_bond_coupon_burden_total, - Some(5_000) - ); - assert_eq!(runtime_world_prime_rate_baseline(&state), Some(5.0)); - assert_eq!( - runtime_world_issue_opinion_term_sum_raw( - &state, - RUNTIME_WORLD_ISSUE_PRIME_RATE, - None, - Some(7), - None, - ), - Some(0) - ); - assert_eq!( - runtime_world_issue_opinion_term_sum_raw( - &state, - RUNTIME_WORLD_ISSUE_CREDIT_MARKET, - None, - Some(7), - None, - ), - Some(0) - ); - assert_eq!(runtime_world_credit_market_scale(&state), Some(1.0)); - - assert_eq!(runtime_company_credit_rating(&state, 7), Some(10)); - assert_eq!( - runtime_company_stat_value( - &state, - 7, - RuntimeCompanyStatSelector { - family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING, - }, - ), - Some(10) - ); - } - - #[test] - fn computes_weighted_average_live_bond_coupon_from_owned_market_slots() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - live_bond_slots: vec![ - RuntimeCompanyBondSlot { - slot_index: 0, - principal: 100_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.04f32.to_bits(), - }, - RuntimeCompanyBondSlot { - slot_index: 1, - principal: 300_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.08f32.to_bits(), - }, - ], - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let average = runtime_company_average_live_bond_coupon(&state, 7) - .expect("weighted average live bond coupon"); - assert!((average - 0.07).abs() < 1e-6); - } - - #[test] - fn decodes_and_packs_company_issue_calendar_tuple() { - let tuple = runtime_decode_packed_calendar_tuple(0x0101_0726, 0x0001_0001); - assert_eq!( - tuple, - RuntimePackedCalendarTuple { - year_word: 0x0726, - month_1_based: 1, - week_1_based: 1, - day_1_based: 1, - hour_0_based: 0, - quarter_day_1_based: 1, - minute_0_based: 0, - } - ); - assert_eq!( - runtime_pack_packed_calendar_tuple_to_absolute_counter(tuple), - Some(885_427_200) - ); - assert_eq!( - runtime_pack_packed_calendar_tuple_to_absolute_counter(RuntimePackedCalendarTuple { - year_word: 1830, - month_1_based: 13, - week_1_based: 1, - day_1_based: 1, - hour_0_based: 0, - quarter_day_1_based: 1, - minute_0_based: 0, - }), - None - ); - } - - #[test] - fn derives_and_encodes_packed_calendar_tuple_from_runtime_calendar() { - let tuple = runtime_derive_packed_calendar_tuple_from_calendar_point(CalendarPoint { - year: 1830, - month_slot: 11, - phase_slot: 27, - tick_slot: 179, - }) - .expect("runtime calendar tuple"); - assert_eq!( - tuple, - RuntimePackedCalendarTuple { - year_word: 1830, - month_1_based: 12, - week_1_based: 4, - day_1_based: 28, - hour_0_based: 23, - quarter_day_1_based: 4, - minute_0_based: 52, - } - ); - assert_eq!( - runtime_encode_packed_calendar_tuple(tuple), - (0x040c_0726, 0x3404_171c) - ); - } - - #[test] - fn derives_selected_year_gap_scalar_from_year_word() { - assert_eq!( - runtime_world_selected_year_bucket_scalar_from_year_word(1830), - Some(25.0) - ); - assert_eq!( - runtime_world_selected_year_bucket_scalar_from_year_word(1835), - Some(31.5) - ); - assert_eq!( - runtime_world_selected_year_bucket_scalar_from_year_word(2000), - Some(123.0) - ); - let bands = runtime_selected_year_bucket_bands_from_scalar(25.0) - .expect("selected-year bucket companion bands"); - assert!((bands.direct[0] - 22.5).abs() < 1e-6); - assert!((bands.direct[1] - 26.25).abs() < 1e-5); - assert!((bands.direct[2] - 17.5).abs() < 1e-6); - assert!((bands.complement[0] - 0.999121).abs() < 1e-6); - assert!((bands.scaled_companion[0] - 139.16667).abs() < 1e-4); - assert_eq!( - runtime_world_selected_year_gap_scalar_from_year_word(1830), - Some((1.0f32 / 3.0).clamp(1.0 / 3.0, 1.0)) - ); - assert_eq!( - runtime_world_selected_year_gap_scalar_from_year_word(1900), - Some((50.0f32 / 150.0).clamp(1.0 / 3.0, 1.0)) - ); - assert_eq!( - runtime_world_selected_year_gap_scalar_from_year_word(2000), - Some(1.0) - ); - } - - #[test] - fn refreshes_selected_year_gap_scalar_from_world_restore_calendar() { - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1900), - economic_tuning_lane_raw_u32: vec![0x3f400000], - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - state.refresh_derived_world_state(); - - assert_eq!( - state.world_restore.economic_tuning_mirror_raw_u32, - Some(0x3f400000) - ); - assert_eq!( - state - .world_restore - .economic_tuning_mirror_value_f32_text - .as_deref(), - Some("0.750000") - ); - assert_eq!( - state.world_restore.selected_year_bucket_scalar_raw_u32, - Some(70.0f32.to_bits()) - ); - assert_eq!( - state - .world_restore - .selected_year_bucket_scalar_value_f32_text - .as_deref(), - Some("70.000000") - ); - assert_eq!( - state - .world_restore - .selected_year_bucket_direct_lane_value_f32_text, - vec![ - "63.000000".to_string(), - "73.500000".to_string(), - "49.000000".to_string() - ] - ); - assert_eq!( - state - .world_restore - .selected_year_bucket_complement_lane_value_f32_text, - vec![ - "0.998400".to_string(), - "0.998213".to_string(), - "0.998649".to_string() - ] - ); - assert_eq!( - state - .world_restore - .selected_year_bucket_scaled_companion_lane_value_f32_text, - vec![ - "64.166672".to_string(), - "58.214291".to_string(), - "76.071426".to_string() - ] - ); - assert_eq!( - state.world_restore.selected_year_gap_scalar_raw_u32, - Some(((50.0f32 / 150.0).clamp(1.0 / 3.0, 1.0)).to_bits()) - ); - assert_eq!( - state - .world_restore - .selected_year_gap_scalar_value_f32_text - .as_deref(), - Some("0.333333") - ); - } - - #[test] - fn derives_company_unassigned_share_pool_from_market_state_and_holdings() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 4, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![ - RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(4), - company_holdings: BTreeMap::from([(4, 8_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }, - RuntimeChairmanProfile { - profile_id: 2, - name: "Chairman Two".to_string(), - active: true, - current_cash: 0, - linked_company_id: None, - company_holdings: BTreeMap::from([(4, 7_500)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }, - ], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 4, - RuntimeCompanyMarketState { - outstanding_shares: 20_000, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!( - runtime_company_unassigned_share_pool(&state, 4), - Some(4_500) - ); - assert_eq!(runtime_company_unassigned_share_pool(&state, 99), None); - } - - #[test] - fn derives_company_annual_finance_state_from_owned_runtime_market_state() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 4, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![ - RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(4), - company_holdings: BTreeMap::from([(4, 8_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }, - RuntimeChairmanProfile { - profile_id: 2, - name: "Chairman Two".to_string(), - active: true, - current_cash: 0, - linked_company_id: None, - company_holdings: BTreeMap::from([(4, 7_500)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }, - ], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 4, - RuntimeCompanyMarketState { - outstanding_shares: 20_000, - bond_count: 3, - largest_live_bond_principal: Some(650_000), - highest_coupon_live_bond_principal: Some(500_000), - cached_share_price_raw_u32: 0x42200000, - chairman_salary_baseline: 24, - chairman_salary_current: 30, - chairman_bonus_year: 1842, - chairman_bonus_amount: 750, - founding_year: 1831, - last_bankruptcy_year: 0, - last_dividend_year: 1841, - current_issue_calendar_word: 5, - current_issue_calendar_word_2: 6, - prior_issue_calendar_word: 4, - prior_issue_calendar_word_2: 5, - city_connection_latch: true, - linked_transit_latch: false, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!(runtime_company_assigned_share_pool(&state, 4), Some(15_500)); - assert_eq!( - runtime_company_annual_finance_state(&state, 4), - Some(RuntimeCompanyAnnualFinanceState { - company_id: 4, - outstanding_shares: 20_000, - bond_count: 3, - largest_live_bond_principal: Some(650_000), - highest_coupon_live_bond_principal: Some(500_000), - live_bond_coupon_burden_total: Some(0), - assigned_share_pool: 15_500, - unassigned_share_pool: 4_500, - cached_share_price: Some(40), - chairman_salary_baseline: 24, - chairman_salary_current: 30, - chairman_bonus_year: 1842, - chairman_bonus_amount: 750, - founding_year: 1831, - last_bankruptcy_year: 0, - last_dividend_year: 1841, - years_since_founding: None, - years_since_last_bankruptcy: None, - years_since_last_dividend: None, - current_partial_year_weight_numerator: None, - trailing_full_year_year_words: Vec::new(), - trailing_full_year_net_profits: Vec::new(), - trailing_full_year_revenues: Vec::new(), - trailing_full_year_fuel_costs: Vec::new(), - current_issue_absolute_counter: None, - prior_issue_absolute_counter: None, - current_issue_age_absolute_counter_delta: None, - current_issue_calendar_word: 5, - current_issue_calendar_word_2: 6, - prior_issue_calendar_word: 4, - prior_issue_calendar_word_2: 5, - preferred_locomotive_engine_type_raw_u8: None, - city_connection_latch: true, - linked_transit_latch: false, - }) - ); - assert_eq!(runtime_company_assigned_share_pool(&state, 99), None); - assert_eq!(runtime_company_annual_finance_state(&state, 99), None); - } - - #[test] - fn derives_company_periodic_service_state_from_side_latches_and_world_route_preference() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - auto_show_grade_during_track_lay_raw_u8: Some(1), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 4, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(4), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 4, - RuntimeCompanyMarketState { - city_connection_latch: true, - linked_transit_latch: false, - ..RuntimeCompanyMarketState::default() - }, - )]), - company_periodic_side_latch_state: BTreeMap::from([( - 4, - RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: Some(2), - city_connection_latch: false, - linked_transit_latch: true, - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!( - runtime_company_periodic_service_state(&state, 4), - Some(RuntimeCompanyPeriodicServiceState { - company_id: 4, - preferred_locomotive_engine_type_raw_u8: Some(2), - city_connection_latch: false, - linked_transit_latch: true, - base_route_preference_raw_u8: Some(1), - effective_route_preference_raw_u8: Some(2), - electric_route_preference_override_active: true, - effective_route_quality_multiplier_basis_points: 180, - }) - ); - assert_eq!(runtime_company_periodic_service_state(&state, 99), None); - } - - #[test] - fn periodic_service_state_falls_back_to_market_latches_and_world_base_route_preference() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - auto_show_grade_during_track_lay_raw_u8: Some(3), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 8, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(8), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 8, - RuntimeCompanyMarketState { - city_connection_latch: true, - linked_transit_latch: false, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!( - runtime_company_periodic_service_state(&state, 8), - Some(RuntimeCompanyPeriodicServiceState { - company_id: 8, - preferred_locomotive_engine_type_raw_u8: None, - city_connection_latch: true, - linked_transit_latch: false, - base_route_preference_raw_u8: Some(3), - effective_route_preference_raw_u8: Some(3), - electric_route_preference_override_active: false, - effective_route_quality_multiplier_basis_points: 140, - }) - ); - } - - #[test] - fn applies_and_restores_company_periodic_route_preference_override() { - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - auto_show_grade_during_track_lay_raw_u8: Some(1), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 4, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(4), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 4, - RuntimeCompanyMarketState { - city_connection_latch: true, - linked_transit_latch: false, - ..RuntimeCompanyMarketState::default() - }, - )]), - company_periodic_side_latch_state: BTreeMap::from([( - 4, - RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: Some(2), - city_connection_latch: true, - linked_transit_latch: false, - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let applied = runtime_begin_company_periodic_route_preference_override(&mut state, 4) - .expect("electric override should apply"); - assert_eq!( - applied, - RuntimeWorldRoutePreferenceOverrideState { - company_id: 4, - base_route_preference_raw_u8: Some(1), - effective_route_preference_raw_u8: Some(2), - electric_route_preference_override_active: true, - } - ); - assert_eq!( - state.world_restore.auto_show_grade_during_track_lay_raw_u8, - Some(2) - ); - assert_eq!( - state - .service_state - .active_periodic_route_preference_override, - Some(applied.clone()) - ); - assert_eq!( - state.service_state.last_periodic_route_preference_override, - Some(applied.clone()) - ); - assert_eq!( - state - .service_state - .periodic_route_preference_override_apply_count, - 1 - ); - assert_eq!( - state - .service_state - .periodic_route_preference_override_restore_count, - 0 - ); - - let restored = runtime_end_company_periodic_route_preference_override(&mut state) - .expect("override should restore"); - assert_eq!(restored, applied); - assert_eq!( - state.world_restore.auto_show_grade_during_track_lay_raw_u8, - Some(1) - ); - assert_eq!( - state - .service_state - .active_periodic_route_preference_override, - None - ); - assert_eq!( - state.service_state.last_periodic_route_preference_override, - Some(restored) - ); - assert_eq!( - state - .service_state - .periodic_route_preference_override_apply_count, - 1 - ); - assert_eq!( - state - .service_state - .periodic_route_preference_override_restore_count, - 1 - ); - } - - #[test] - fn skips_company_periodic_route_preference_override_without_electric_preference() { - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - auto_show_grade_during_track_lay_raw_u8: Some(3), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 8, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(8), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 8, - RuntimeCompanyMarketState { - city_connection_latch: true, - linked_transit_latch: false, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!( - runtime_begin_company_periodic_route_preference_override(&mut state, 8), - None - ); - assert_eq!( - state.world_restore.auto_show_grade_during_track_lay_raw_u8, - Some(3) - ); - assert_eq!( - state - .service_state - .active_periodic_route_preference_override, - None - ); - assert_eq!( - state.service_state.last_periodic_route_preference_override, - None - ); - } - - #[test] - fn derives_annual_creditor_pressure_from_rehosted_finance_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x0d, -700_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x12, 0.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 80_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -115_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -110_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - founding_year: 1841, - last_bankruptcy_year: 1832, - cached_share_price_raw_u32: 25.0f32.to_bits(), - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!(runtime_world_annual_finance_mode_active(&state), Some(true)); - assert_eq!(runtime_world_bankruptcy_allowed(&state), Some(true)); - let pressure_state = runtime_company_annual_creditor_pressure_state(&state, 7) - .expect("creditor pressure state"); - assert_eq!(pressure_state.recent_bad_net_profit_year_count, 3); - assert_eq!(pressure_state.recent_peak_revenue, Some(100_000)); - assert_eq!( - pressure_state.recent_three_year_net_profit_total, - Some(-65_000) - ); - assert_eq!(pressure_state.pressure_ladder_cash_floor, Some(-600_000)); - assert_eq!( - pressure_state.current_cash_plus_slot_12_total, - Some(-700_000) - ); - assert_eq!(pressure_state.support_adjusted_share_price_floor, Some(20)); - assert_eq!(pressure_state.support_adjusted_share_price_scalar, Some(25)); - assert_eq!(pressure_state.current_fuel_cost, Some(-50_000)); - assert_eq!(pressure_state.current_fuel_cost_floor, Some(-48_000)); - assert!(pressure_state.eligible_for_bankruptcy_branch); - } - - #[test] - fn derives_annual_deep_distress_bankruptcy_fallback_from_rehosted_finance_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, -350_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 9, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 9, - RuntimeCompanyMarketState { - founding_year: 1841, - last_bankruptcy_year: 1840, - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let pressure_state = - runtime_company_annual_deep_distress_state(&state, 9).expect("deep distress state"); - assert_eq!(pressure_state.current_cash, Some(-350_000)); - assert_eq!( - pressure_state.recent_first_three_net_profit_years, - vec![-25_000, -23_000, -21_000] - ); - assert_eq!(pressure_state.deep_distress_cash_floor, Some(-300_000)); - assert_eq!(pressure_state.deep_distress_net_profit_floor, Some(-20_000)); - assert!(pressure_state.eligible_for_bankruptcy_fallback); - } - - #[test] - fn derives_annual_stock_repurchase_state_from_rehosted_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, 1_600_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - partial_year_progress_raw_u8: Some(0x0c), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - building_density_growth_setting_raw_u32: Some(1), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 12, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: Some(3), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 3, - name: "Jay".to_string(), - active: true, - current_cash: 200, - linked_company_id: Some(12), - company_holdings: BTreeMap::from([(12, 14_500)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 12, - RuntimeCompanyMarketState { - outstanding_shares: 20_000, - cached_share_price_raw_u32: 20.0f32.to_bits(), - founding_year: 1835, - city_connection_latch: true, - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - chairman_personality_raw_u8: BTreeMap::from([(3, 20)]), - ..RuntimeServiceState::default() - }, - }; - - let repurchase_state = runtime_company_annual_stock_repurchase_state(&state, 12) - .expect("stock repurchase state"); - assert_eq!(repurchase_state.building_density_growth_setting, Some(1)); - assert_eq!( - repurchase_state.linked_chairman_personality_raw_u8, - Some(20) - ); - assert_eq!(repurchase_state.repurchase_batch_size, Some(1_000)); - assert_eq!(repurchase_state.repurchase_factor_basis_points, Some(432)); - assert_eq!(repurchase_state.current_cash, Some(1_600_000)); - assert_eq!( - repurchase_state.stock_value_gate_cash_floor, - Some(3_456_000) - ); - assert_eq!( - repurchase_state.support_adjusted_share_price_scalar, - Some(20) - ); - assert_eq!(repurchase_state.affordability_cash_floor, Some(103_680)); - assert_eq!(repurchase_state.unassigned_share_pool, Some(5_500)); - assert!(!repurchase_state.eligible_for_single_batch_repurchase); - } - - #[test] - fn derives_annual_bond_policy_state_from_rehosted_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, -400_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - partial_year_progress_raw_u8: Some(0x0c), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 11, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 11, - RuntimeCompanyMarketState { - bond_count: 2, - linked_transit_latch: true, - live_bond_slots: vec![ - RuntimeCompanyBondSlot { - slot_index: 0, - principal: 200_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.09f32.to_bits(), - }, - RuntimeCompanyBondSlot { - slot_index: 1, - principal: 150_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.08f32.to_bits(), - }, - ], - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let bond_state = - runtime_company_annual_bond_policy_state(&state, 11).expect("bond policy state"); - assert_eq!(bond_state.live_bond_count, Some(2)); - assert_eq!(bond_state.live_bond_principal_total, Some(350_000)); - assert_eq!(bond_state.matured_live_bond_count, Some(0)); - assert_eq!(bond_state.matured_live_bond_principal_total, Some(0)); - assert_eq!(bond_state.next_live_bond_maturity_year, None); - assert_eq!(bond_state.live_bond_coupon_burden_total, Some(30_000)); - assert_eq!(bond_state.current_cash, Some(-400_000)); - assert_eq!(bond_state.cash_after_full_repayment, Some(-750_000)); - assert_eq!(bond_state.issue_cash_floor, Some(-30_000)); - assert_eq!(bond_state.issue_principal_step, Some(500_000)); - assert_eq!(bond_state.proposed_issue_bond_count, Some(2)); - assert_eq!(bond_state.proposed_issue_total_principal, Some(1_000_000)); - assert_eq!(bond_state.proposed_issue_years_to_maturity, Some(30)); - assert!(bond_state.eligible_for_bond_issue_branch); - } - - #[test] - fn classifies_annual_bond_principal_flow_relation() { - assert_eq!( - runtime_annual_bond_principal_flow_relation_label(0, 0), - None - ); - assert_eq!( - runtime_annual_bond_principal_flow_relation_label(350_000, 0), - Some("retired_only") - ); - assert_eq!( - runtime_annual_bond_principal_flow_relation_label(0, 500_000), - Some("issued_only") - ); - assert_eq!( - runtime_annual_bond_principal_flow_relation_label(350_000, 1_000_000), - Some("issued_exceeds_retired") - ); - assert_eq!( - runtime_annual_bond_principal_flow_relation_label(500_000, 500_000), - Some("retired_equals_issued") - ); - } - - #[test] - fn maps_annual_bond_news_selector_from_principal_flow_relation() { - assert_eq!( - runtime_annual_finance_news_family_candidate_label( - RuntimeCompanyAnnualFinancePolicyAction::BondIssue, - 500_000, - 500_000, - 0, - 0, - ), - Some("2882") - ); - assert_eq!( - runtime_annual_finance_news_family_candidate_label( - RuntimeCompanyAnnualFinancePolicyAction::BondIssue, - 350_000, - 1_000_000, - 0, - 0, - ), - Some("2883") - ); - assert_eq!( - runtime_annual_finance_news_family_candidate_label( - RuntimeCompanyAnnualFinancePolicyAction::BondIssue, - 900_000, - 500_000, - 0, - 0, - ), - Some("2884") - ); - assert_eq!( - runtime_annual_finance_news_family_candidate_label( - RuntimeCompanyAnnualFinancePolicyAction::BondIssue, - 350_000, - 0, - 0, - 0, - ), - Some("2885") - ); - assert_eq!( - runtime_annual_finance_news_family_candidate_label( - RuntimeCompanyAnnualFinancePolicyAction::BondIssue, - 0, - 500_000, - 0, - 0, - ), - Some("2886") - ); - } - - #[test] - fn annual_bond_policy_stays_eligible_for_repayment_without_new_issue() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, 900_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - partial_year_progress_raw_u8: Some(0x0c), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 12, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 12, - RuntimeCompanyMarketState { - bond_count: 2, - live_bond_slots: vec![ - RuntimeCompanyBondSlot { - slot_index: 0, - principal: 200_000, - maturity_year: 1845, - coupon_rate_raw_u32: 0.09f32.to_bits(), - }, - RuntimeCompanyBondSlot { - slot_index: 1, - principal: 150_000, - maturity_year: 1847, - coupon_rate_raw_u32: 0.08f32.to_bits(), - }, - ], - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let bond_state = - runtime_company_annual_bond_policy_state(&state, 12).expect("bond policy state"); - assert_eq!(bond_state.live_bond_principal_total, Some(350_000)); - assert_eq!(bond_state.cash_after_full_repayment, Some(550_000)); - assert_eq!(bond_state.proposed_issue_bond_count, Some(0)); - assert!(bond_state.eligible_for_bond_issue_branch); - } - - #[test] - fn derives_annual_stock_issue_state_from_rehosted_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, 250_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - partial_year_progress_raw_u8: Some(0x0c), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - issue_37_value: Some(2), - issue_37_multiplier_raw_u32: Some(1.0f32.to_bits()), - issue_37_multiplier_value_f32_text: Some("1.000000".to_string()), - absolute_counter_raw_u32: Some(885_911_040), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 14, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: Some(8), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 8, - name: "Taylor".to_string(), - active: true, - current_cash: 200, - linked_company_id: Some(14), - company_holdings: BTreeMap::from([(14, 14_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - chairman_personality_raw_u8: BTreeMap::from([(8, 20)]), - company_market_state: BTreeMap::from([( - 14, - RuntimeCompanyMarketState { - outstanding_shares: 20_000, - bond_count: 2, - highest_coupon_live_bond_principal: Some(300_000), - current_issue_calendar_word: 0x0101_0725, - current_issue_calendar_word_2: 0x0001_0001, - founding_year: 1840, - cached_share_price_raw_u32: 35.0f32.to_bits(), - recent_per_share_cache_absolute_counter: 885_911_040, - recent_per_share_cached_value_bits: 34.0f64.to_bits(), - city_connection_latch: false, - live_bond_slots: vec![ - RuntimeCompanyBondSlot { - slot_index: 0, - principal: 300_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.11f32.to_bits(), - }, - RuntimeCompanyBondSlot { - slot_index: 1, - principal: 200_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.07f32.to_bits(), - }, - ], - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x32f, - 30.0f32.to_bits(), - )]), - year_stat_family_qword_bits, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let stock_issue_state = - runtime_company_annual_stock_issue_state(&state, 14).expect("stock issue state"); - assert_eq!(stock_issue_state.live_bond_count, Some(2)); - assert_eq!(stock_issue_state.initial_issue_batch_size, Some(2_000)); - assert_eq!(stock_issue_state.trimmed_issue_batch_size, Some(2_000)); - assert_eq!(stock_issue_state.share_pressure_basis_points, Some(-1_000)); - assert_eq!( - stock_issue_state.pressured_support_adjusted_share_price_scalar, - Some(35) - ); - assert_eq!(stock_issue_state.pressured_proceeds, Some(70_000)); - assert_eq!( - stock_issue_state.book_value_per_share_floor_applied, - Some(30) - ); - assert_eq!( - stock_issue_state.price_to_book_ratio_basis_points, - Some(11_667) - ); - assert_eq!( - stock_issue_state.highest_coupon_live_bond_rate_basis_points, - Some(1_100) - ); - assert_eq!( - stock_issue_state.minimum_price_to_book_ratio_basis_points, - Some(8_000) - ); - assert_eq!(stock_issue_state.current_cash, Some(250_000)); - assert_eq!( - stock_issue_state.highest_coupon_live_bond_principal, - Some(300_000) - ); - assert_eq!( - stock_issue_state.current_issue_age_absolute_counter_delta, - Some(967_680) - ); - assert_eq!( - stock_issue_state.current_issue_cooldown_floor, - Some(483_840) - ); - assert_eq!(stock_issue_state.passes_share_price_floor, Some(true)); - assert_eq!(stock_issue_state.passes_proceeds_floor, Some(true)); - assert_eq!(stock_issue_state.passes_cash_gate, Some(true)); - assert_eq!(stock_issue_state.passes_issue_cooldown_gate, Some(true)); - assert_eq!( - stock_issue_state.passes_coupon_price_to_book_gate, - Some(true) - ); - assert!(stock_issue_state.eligible_for_double_tranche_issue); - } - - #[test] - fn derives_annual_dividend_policy_state_from_rehosted_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - dividend_policy_raw_u8: Some(0), - dividend_adjustment_allowed: Some(true), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - building_density_growth_setting_raw_u32: Some(1), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 15, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 3, - name: "Chairman Three".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(15), - company_holdings: BTreeMap::from([(15, 9_500)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 15, - RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1840, - last_dividend_year: 1844, - year_stat_family_qword_bits, - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x33f, - 0.4f32.to_bits(), - )]), - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let dividend_state = runtime_company_annual_dividend_policy_state(&state, 15) - .expect("annual dividend policy state"); - assert_eq!(dividend_state.years_since_last_dividend, Some(1)); - assert_eq!(dividend_state.years_since_founding, Some(5)); - assert_eq!(dividend_state.outstanding_shares, Some(10_000)); - assert_eq!(dividend_state.unassigned_share_pool, Some(500)); - assert_eq!( - dividend_state.weighted_recent_net_profit_total, - Some(600_000) - ); - assert_eq!( - dividend_state.weighted_recent_net_profit_average, - Some(100_000) - ); - assert_eq!(dividend_state.current_cash, Some(300_000)); - assert!(dividend_state.tiny_unassigned_share_cash_supplement_branch); - assert_eq!( - dividend_state.tentative_target_dividend_per_share_tenths, - Some(133) - ); - assert_eq!(dividend_state.current_dividend_per_share_tenths, Some(4)); - assert_eq!( - dividend_state.growth_adjusted_current_dividend_per_share_tenths, - Some(3) - ); - assert_eq!( - dividend_state.board_approved_dividend_rate_ceiling_tenths, - Some(18) - ); - assert_eq!(dividend_state.proposed_dividend_per_share_tenths, Some(18)); - assert!(dividend_state.eligible_for_dividend_adjustment_branch); - } - - #[test] - fn derives_annual_finance_policy_action_from_branch_priority_order() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - dividend_policy_raw_u8: Some(0), - dividend_adjustment_allowed: Some(true), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - building_density_growth_setting_raw_u32: Some(1), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 16, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: Some(4), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(16), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 4, - name: "Chairman Four".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(16), - company_holdings: BTreeMap::from([(16, 9_500)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - chairman_personality_raw_u8: BTreeMap::from([(4, 20)]), - company_market_state: BTreeMap::from([( - 16, - RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1840, - last_dividend_year: 1844, - city_connection_latch: true, - year_stat_family_qword_bits, - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x33f, - 0.4f32.to_bits(), - )]), - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let policy_state = runtime_company_annual_finance_policy_state(&state, 16) - .expect("annual finance policy state"); - assert_eq!( - policy_state.action, - RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment - ); - assert!(!policy_state.stock_repurchase_eligible); - assert!(!policy_state.stock_issue_eligible); - assert!(policy_state.dividend_adjustment_eligible); - } - - #[test] - fn reads_company_market_metrics_from_annual_finance_reader() { - let current_issue_calendar_word = 0x0101_0726; - let current_issue_calendar_word_2 = 0x0001_0001; - let prior_issue_calendar_word = 0x0101_0725; - let prior_issue_calendar_word_2 = 0x0001_0001; - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - absolute_counter_raw_u32: Some(885_427_260), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(7), - company_holdings: BTreeMap::from([(7, 12_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 7, - RuntimeCompanyMarketState { - outstanding_shares: 20_000, - bond_count: 2, - largest_live_bond_principal: Some(500_000), - highest_coupon_live_bond_principal: Some(300_000), - cached_share_price_raw_u32: 0x42200000, - chairman_salary_baseline: 18, - chairman_salary_current: 27, - chairman_bonus_year: 1843, - chairman_bonus_amount: 625, - founding_year: 1832, - last_bankruptcy_year: 0, - last_dividend_year: 1842, - current_issue_calendar_word, - current_issue_calendar_word_2, - prior_issue_calendar_word, - prior_issue_calendar_word_2, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - assert_eq!( - runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::OutstandingShares), - Some(20_000) - ); - assert_eq!( - runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::BondCount), - Some(2) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::LargestLiveBondPrincipal - ), - Some(500_000) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::HighestCouponLiveBondPrincipal - ), - Some(300_000) - ); - assert_eq!( - runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::AssignedSharePool), - Some(12_000) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::UnassignedSharePool - ), - Some(8_000) - ); - assert_eq!( - runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::CachedSharePrice), - Some(40) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::CurrentIssueAbsoluteCounter - ), - Some(885_427_200) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::PriorIssueAbsoluteCounter - ), - Some(884_943_360) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::CurrentIssueAgeAbsoluteCounterDelta - ), - Some(60) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::CurrentIssueCalendarWord - ), - Some(i64::from(current_issue_calendar_word)) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::CurrentIssueCalendarWord2 - ), - Some(i64::from(current_issue_calendar_word_2)) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::PriorIssueCalendarWord - ), - Some(i64::from(prior_issue_calendar_word)) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::PriorIssueCalendarWord2 - ), - Some(i64::from(prior_issue_calendar_word_2)) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::ChairmanSalaryCurrent - ), - Some(27) - ); - assert_eq!( - runtime_company_market_value( - &state, - 7, - RuntimeCompanyMarketMetric::ChairmanBonusAmount - ), - Some(625) - ); - assert_eq!( - runtime_company_market_value(&state, 99, RuntimeCompanyMarketMetric::OutstandingShares), - None - ); - } - - #[test] - fn derives_elapsed_company_finance_years_from_calendar_and_saved_market_state() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1844, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 3, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 3, - RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1838, - last_bankruptcy_year: 1841, - last_dividend_year: 1843, - ..RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let finance_state = - runtime_company_annual_finance_state(&state, 3).expect("finance state should derive"); - assert_eq!(finance_state.years_since_founding, Some(6)); - assert_eq!(finance_state.years_since_last_bankruptcy, Some(3)); - assert_eq!(finance_state.years_since_last_dividend, Some(1)); - } -} diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs deleted file mode 100644 index 5f5de78..0000000 --- a/crates/rrt-runtime/src/smp.rs +++ /dev/null @@ -1,33997 +0,0 @@ -use std::cmp::Reverse; -use std::collections::{BTreeMap, BTreeSet}; -use std::fs; -use std::path::Path; -use std::sync::OnceLock; - -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; - -use crate::{ - RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, - RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope, - RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric, - RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, - RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimePlayerTarget, - RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, -}; - -pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec; -const PREAMBLE_U32_WORD_COUNT: usize = 16; -const MIN_ASCII_RUN_LEN: usize = 8; -const ASCII_PREVIEW_CHAR_LIMIT: usize = 160; -const TAG_OFFSET_SAMPLE_LIMIT: usize = 8; -const EARLY_ZERO_RUN_THRESHOLD: usize = 16; -const EARLY_PREVIEW_BYTE_LIMIT: usize = 32; -const EARLY_ALIGNED_WORD_WINDOW_COUNT: usize = 8; -const SPECIAL_CONDITIONS_OFFSET: usize = 0x0d64; -const SPECIAL_CONDITION_COUNT: usize = 36; -const SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT: usize = 35; -const SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT: usize = 50; -const SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT: usize = 49; -const SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET: usize = 0x4a7f; -const SMP_ALIGNED_RUNTIME_RULE_END_OFFSET: usize = - SPECIAL_CONDITIONS_OFFSET + SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT * 4; -const POST_SPECIAL_CONDITIONS_SCALAR_OFFSET: usize = 0x0df4; -const POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET: usize = 0x0f30; -const POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET: usize = 0x0f58; -const POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET: usize = - SMP_ALIGNED_RUNTIME_RULE_END_OFFSET; -const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET: usize = SMP_ALIGNED_RUNTIME_RULE_END_OFFSET; -const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET: usize = - SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET - + (POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET - SPECIAL_CONDITIONS_OFFSET); -const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET: usize = - SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET - + (POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - SPECIAL_CONDITIONS_OFFSET); -const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET: usize = 0x4b47; -const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN: usize = 0x12c; -const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET: usize = - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET - + POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN; -const POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET: usize = 0x0f59; -const POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET: usize = 0x0f75; -const POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET: usize = 0x4c74; -const POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET: usize = 0x4c78; -const POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET: usize = 0x4c7c; -const POST_TEXT_FIELD_3_RUNTIME_OBJECT_OFFSET: usize = 0x4c80; -const POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET: usize = 0x4c88; -const POST_TEXT_FIELD_5_RUNTIME_OBJECT_OFFSET: usize = 0x4c8c; -const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET: usize = 0x4c80; -const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET: usize = 0x4c8c; -const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET: usize = - SPECIAL_CONDITIONS_OFFSET - + (POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET - - SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET); -const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET: usize = - SPECIAL_CONDITIONS_OFFSET - + (POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET - - SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET); -const LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET: usize = 0x0f78; -const LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET: usize = 0x0fa7; -const LOCOMOTIVE_POLICY_FIELD_NEG3_RUNTIME_OBJECT_OFFSET: usize = 0x4ca2; -const LOCOMOTIVE_POLICY_FIELD_NEG2_RUNTIME_OBJECT_OFFSET: usize = 0x4cae; -const LOCOMOTIVE_POLICY_FIELD_NEG1_RUNTIME_OBJECT_OFFSET: usize = 0x4cb2; -const LOCOMOTIVE_POLICY_FIELD_0_RUNTIME_OBJECT_OFFSET: usize = 0x4c93; -const LOCOMOTIVE_POLICY_FIELD_1_RUNTIME_OBJECT_OFFSET: usize = 0x4c97; -const LOCOMOTIVE_POLICY_FIELD_2_RUNTIME_OBJECT_OFFSET: usize = 0x4c98; -const LOCOMOTIVE_POLICY_FIELD_3_RUNTIME_OBJECT_OFFSET: usize = 0x4c99; -const LOCOMOTIVE_POLICY_FIELD_4_RUNTIME_OBJECT_OFFSET: usize = 0x4cba; -const LOCOMOTIVE_POLICY_FIELD_5_RUNTIME_OBJECT_OFFSET: usize = 0x4cbe; -const PRE_RECIPE_SCALAR_PLATEAU_OFFSET: usize = 0x0fa7; -const PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET: usize = 0x0fe7; -const RECIPE_BOOK_ROOT_OFFSET: usize = 0x0fe7; -const RECIPE_BOOK_COUNT: usize = 12; -const RECIPE_BOOK_STRIDE: usize = 0x4e1; -const RECIPE_BOOK_HEAD_SAMPLE_LEN: usize = 16; -const RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET: usize = 0x3ed; -const RECIPE_BOOK_LINE_AREA_OFFSET: usize = 0x3f1; -const RECIPE_BOOK_LINE_COUNT: usize = 5; -const RECIPE_BOOK_LINE_STRIDE: usize = 0x30; -const RECIPE_BOOK_LINE_AREA_LEN: usize = RECIPE_BOOK_LINE_COUNT * RECIPE_BOOK_LINE_STRIDE; -const RECIPE_BOOK_SUMMARY_END_OFFSET: usize = - RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_COUNT * RECIPE_BOOK_STRIDE; -const RT3_SAVE_WORLD_BLOCK_CHUNK_TAG: u32 = 0x000032c8; -const RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG: u32 = 0x000032c9; -const RT3_SAVE_WORLD_BLOCK_LEN: usize = 0x4f2c; -const RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET: usize = 0x1d; -const RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET: usize = 0x21; -const RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET: usize = 0x25; -const RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET: usize = 0x29; -const RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET: usize = 0x0d; -const RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET: usize = 0x11; -const RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET: usize = 0x15; -const RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET: usize = 0x19; -const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_CANDIDATE_FIELDS: [(&str, usize); 11] = [ - ( - "current_calendar_tuple_word", - RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, - ), - ( - "current_calendar_tuple_word_2", - RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET, - ), - ( - "absolute_calendar_counter", - RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET, - ), - ( - "absolute_calendar_counter_mirror", - RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET, - ), - ("selection_context_candidate_0", 0x1d), - ("selection_context_candidate_1", 0x21), - ("issue_0x37_multiplier", 0x25), - ("issue_0x37_value", 0x29), - ("issue_neighbor_candidate_0", 0x2d), - ("issue_neighbor_candidate_1", 0x31), - ("issue_neighbor_candidate_2", 0x35), -]; -const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET: usize = 0x0d; -const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS: usize = 17; -const RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_BASE_TERMS_OFFSET: usize = 0x8a; -const RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT: usize = 0x3b; -const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET: usize = 0x83; -const RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET: usize = 0xc1; -const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET: usize = 0x0bbf; -const RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET: usize = 0x4a83; -const RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET: usize = 0x4a87; -const RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET: usize = 0x4a8b; -const RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET: usize = 0x4a8f; -const RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET: usize = 0x4c78; -const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET: usize = 0x0bda; -const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS: [usize; 6] = - [0x0bde, 0x0be2, 0x0be6, 0x0bea, 0x0bee, 0x0bf2]; -const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT: usize = 16; -const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE: usize = 9; -const EVENT_RUNTIME_COLLECTION_METADATA_TAG: u16 = 0x4e99; -const EVENT_RUNTIME_COLLECTION_RECORDS_TAG: u16 = 0x4e9a; -const EVENT_RUNTIME_COLLECTION_CLOSE_TAG: u16 = 0x4e9b; -const EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION: u32 = 0x000003e9; -const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19; -const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize = - INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4; -const SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX: usize = 16; -const SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT: usize = 3; -const SAVE_REGION_RECORD_NAME_TAG: u16 = 0x55f1; -const SAVE_REGION_RECORD_POLICY_TAG: u16 = 0x55f2; -const SAVE_REGION_RECORD_PROFILE_TAG: u16 = 0x55f3; -const SAVE_REGION_FIXED_ROW_STRIDE: usize = 0x29; -const SAVE_REGION_FIXED_ROW_DWORD_LANE_COUNT: usize = SAVE_REGION_FIXED_ROW_STRIDE / 4; -const SAVE_REGION_FIXED_ROW_CANDIDATE_PROBE_LIMIT: usize = 24; -const SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED: u32 = 0x005c87a8; -const SAVE_REGION_QUEUED_NOTICE_NODE_KIND: u32 = 7; -const SAVE_REGION_QUEUED_NOTICE_NODE_LEN: usize = 0x20; -const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001"; -const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1"; -const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1"; -const PACKED_EVENT_REAL_CONDITION_MARKER: u16 = 0x526f; -const PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER: u16 = 0x4eb8; -const PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER: u16 = 0x4eb9; -const PACKED_EVENT_REAL_CONDITION_ROW_LEN: usize = 0x1e; -const PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN: usize = 0x28; -const PACKED_EVENT_REAL_GROUP_COUNT: usize = 4; -const PACKED_EVENT_REAL_COMPACT_CONTROL_LEN: usize = 37; -const PACKED_EVENT_NONDIRECT_CONDITION_ROW_SERIALIZED_LEN: usize = 22; -const PACKED_EVENT_NONDIRECT_GROUPED_EFFECT_ROW_SERIALIZED_LEN: usize = 45; -const PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN: usize = 0x64; -const MAP_TITLE_HINT_ASCII_FRAGMENT_MAX_LEN: usize = 160; -const MAP_TITLE_HINT_REFERENCE_PAIR_DISTANCE_LIMIT: usize = 0x100; -const POST_LOAD_SCENARIO_FIXUP_TITLE_SET: [&str; 14] = [ - "Go West!", - "Germany", - "France", - "State of Germany", - "New Beginnings", - "Dutchlantis", - "Britain", - "New Zealand", - "South East Australia", - "Tex-Mex", - "Germantown", - "The American", - "Central Pacific", - "Orient Express", -]; -const PACKED_EVENT_TEXT_BAND_LABELS: [&str; 6] = [ - "primary_text_band", - "secondary_text_band_0", - "secondary_text_band_1", - "secondary_text_band_2", - "secondary_text_band_3", - "secondary_text_band_4", -]; -const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX: usize = - (POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - SPECIAL_CONDITIONS_OFFSET) / 4; -const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT: usize = - (SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET) / 4; -const SHARED_SIGNATURE_WORDS_1_TO_7: [u32; 7] = [ - 0x00002ee0, 0x00040001, 0x00028000, 0x00010000, 0x00000771, 0x00000771, 0x00000771, -]; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct RealGroupedEffectDescriptorMetadata { - descriptor_id: u32, - label: &'static str, - target_mask_bits: u8, - parameter_family: &'static str, - runtime_key: Option<&'static str>, - runtime_status: RealGroupedEffectRuntimeStatus, - executable_in_runtime: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RealGroupedEffectRuntimeStatus { - Executable, - ShellOwned, - EvidenceBlocked, - VariantOrScopeBlocked, -} - -#[derive(Debug, Clone, PartialEq, Deserialize)] -struct CheckedInEventEffectsSemanticCatalogArtifact { - descriptors: Vec, -} - -#[derive(Debug, Clone, PartialEq, Deserialize)] -struct CheckedInEventEffectSemanticRow { - descriptor_id: u32, - label: String, - target_mask_bits: u8, - parameter_family: String, - runtime_key: Option, - runtime_status: String, - executable_in_runtime: bool, -} - -const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 12] = [ - RealGroupedEffectDescriptorMetadata { - descriptor_id: 1, - label: "Player Cash", - target_mask_bits: 0x02, - parameter_family: "player_finance_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 2, - label: "Company Cash", - target_mask_bits: 0x01, - parameter_family: "company_finance_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 3, - label: "Territory - Allow All", - target_mask_bits: 0x05, - parameter_family: "territory_access_toggle", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 8, - label: "Economic Status", - target_mask_bits: 0x08, - parameter_family: "whole_game_state_enum", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 108, - label: "Use Wartime Cargos", - target_mask_bits: 0x08, - parameter_family: "special_condition_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 109, - label: "Turbo Diesel Availability", - target_mask_bits: 0x08, - parameter_family: "candidate_availability_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 110, - label: "Disable Stock Buying and Selling", - target_mask_bits: 0x08, - parameter_family: "world_flag_toggle", - runtime_key: Some("world.disable_stock_buying_and_selling"), - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 9, - label: "Confiscate All", - target_mask_bits: 0x01, - parameter_family: "company_confiscation_variant", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 13, - label: "Deactivate Company", - target_mask_bits: 0x01, - parameter_family: "company_lifecycle_toggle", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 14, - label: "Deactivate Player", - target_mask_bits: 0x02, - parameter_family: "player_lifecycle_toggle", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 15, - label: "Retire Train", - target_mask_bits: 0x0d, - parameter_family: "company_or_territory_asset_toggle", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, - RealGroupedEffectDescriptorMetadata { - descriptor_id: 16, - label: "Company Track Pieces Buildable", - target_mask_bits: 0x01, - parameter_family: "company_build_limit_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }, -]; - -fn real_grouped_effect_runtime_status_name(status: RealGroupedEffectRuntimeStatus) -> &'static str { - match status { - RealGroupedEffectRuntimeStatus::Executable => "executable", - RealGroupedEffectRuntimeStatus::ShellOwned => "shell_owned", - RealGroupedEffectRuntimeStatus::EvidenceBlocked => "evidence_blocked", - RealGroupedEffectRuntimeStatus::VariantOrScopeBlocked => "variant_or_scope_blocked", - } -} - -fn checked_in_event_effect_descriptor_rows() --> &'static BTreeMap { - static ROWS: OnceLock> = OnceLock::new(); - ROWS.get_or_init(|| { - let artifact: CheckedInEventEffectsSemanticCatalogArtifact = serde_json::from_str( - include_str!("../../../artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json"), - ) - .expect("checked-in event-effects semantic catalog should parse"); - artifact - .descriptors - .into_iter() - .map(|row| { - ( - row.descriptor_id, - checked_in_event_effect_descriptor_metadata(row), - ) - }) - .collect() - }) -} - -fn checked_in_event_effect_descriptor_metadata( - row: CheckedInEventEffectSemanticRow, -) -> RealGroupedEffectDescriptorMetadata { - let label = Box::leak(row.label.clone().into_boxed_str()) as &'static str; - let parameter_family = Box::leak(row.parameter_family.into_boxed_str()) as &'static str; - let runtime_key = row - .runtime_key - .map(|key| Box::leak(key.into_boxed_str()) as &'static str); - let runtime_status = match row.runtime_status.as_str() { - "executable" => RealGroupedEffectRuntimeStatus::Executable, - "shell_owned" => RealGroupedEffectRuntimeStatus::ShellOwned, - "evidence_blocked" => RealGroupedEffectRuntimeStatus::EvidenceBlocked, - "variant_or_scope_blocked" => RealGroupedEffectRuntimeStatus::VariantOrScopeBlocked, - other => panic!("unknown checked-in event-effect runtime status {other}"), - }; - RealGroupedEffectDescriptorMetadata { - descriptor_id: row.descriptor_id, - label, - target_mask_bits: row.target_mask_bits, - parameter_family, - runtime_key, - runtime_status, - executable_in_runtime: row.executable_in_runtime, - } -} - -pub(crate) fn grouped_effect_descriptor_runtime_status_name( - descriptor_id: u32, -) -> Option<&'static str> { - real_grouped_effect_descriptor_metadata(descriptor_id) - .map(|metadata| real_grouped_effect_runtime_status_name(metadata.runtime_status)) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RealOrdinaryConditionMetric { - WorldVariable(u32), - Company(RuntimeCompanyMetric), - CompanyVariable(u32), - PlayerVariable(u32), - Chairman(RuntimeChairmanMetric), - Territory(RuntimeTerritoryMetric), - TerritoryVariable(u32), - CompanyTerritory(RuntimeTrackMetric), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RealWorldConditionKind { - SpecialCondition { label: &'static str }, - CandidateAvailability, - NamedLocomotiveAvailability, - NamedLocomotiveCost, - CargoProductionSlot, - CargoProductionTotal, - FactoryProductionTotal, - FarmMineProductionTotal, - OtherCargoProductionTotal, - LimitedTrackBuildingAmount, - TerritoryAccessCost, - EconomicStatus, - WorldFlag { key: &'static str }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RealOrdinaryConditionKind { - Numeric(RealOrdinaryConditionMetric), - WorldState(RealWorldConditionKind), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct RealOrdinaryConditionMetadata { - raw_condition_id: i32, - label: &'static str, - kind: RealOrdinaryConditionKind, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct KnownCargoSlotDefinition { - slot_id: u32, - label: &'static str, - cargo_class: RuntimeCargoClass, - descriptor_id: u32, -} - -const KNOWN_CARGO_SLOT_DEFINITIONS: [KnownCargoSlotDefinition; 11] = [ - KnownCargoSlotDefinition { - slot_id: 1, - label: "Cargo Production Slot 1", - cargo_class: RuntimeCargoClass::Factory, - descriptor_id: 230, - }, - KnownCargoSlotDefinition { - slot_id: 2, - label: "Cargo Production Slot 2", - cargo_class: RuntimeCargoClass::Factory, - descriptor_id: 231, - }, - KnownCargoSlotDefinition { - slot_id: 3, - label: "Cargo Production Slot 3", - cargo_class: RuntimeCargoClass::Factory, - descriptor_id: 232, - }, - KnownCargoSlotDefinition { - slot_id: 4, - label: "Cargo Production Slot 4", - cargo_class: RuntimeCargoClass::Factory, - descriptor_id: 233, - }, - KnownCargoSlotDefinition { - slot_id: 5, - label: "Cargo Production Slot 5", - cargo_class: RuntimeCargoClass::FarmMine, - descriptor_id: 234, - }, - KnownCargoSlotDefinition { - slot_id: 6, - label: "Cargo Production Slot 6", - cargo_class: RuntimeCargoClass::FarmMine, - descriptor_id: 235, - }, - KnownCargoSlotDefinition { - slot_id: 7, - label: "Cargo Production Slot 7", - cargo_class: RuntimeCargoClass::FarmMine, - descriptor_id: 236, - }, - KnownCargoSlotDefinition { - slot_id: 8, - label: "Cargo Production Slot 8", - cargo_class: RuntimeCargoClass::FarmMine, - descriptor_id: 237, - }, - KnownCargoSlotDefinition { - slot_id: 9, - label: "Cargo Production Slot 9", - cargo_class: RuntimeCargoClass::Other, - descriptor_id: 238, - }, - KnownCargoSlotDefinition { - slot_id: 10, - label: "Cargo Production Slot 10", - cargo_class: RuntimeCargoClass::Other, - descriptor_id: 239, - }, - KnownCargoSlotDefinition { - slot_id: 11, - label: "Cargo Production Slot 11", - cargo_class: RuntimeCargoClass::Other, - descriptor_id: 240, - }, -]; - -const GROUNDED_LOCOMOTIVE_PREFIX: [&str; 61] = [ - "2-D-2", - "E-88", - "Adler 2-2-2", - "USA 103", - "American 4-4-0", - "Atlantic 4-4-2", - "Baldwin 0-6-0", - "Be 5/7", - "Beuth 2-2-2", - "Big Boy 4-8-8-4", - "C55 Deltic", - "Camelback 0-6-0", - "Challenger 4-6-6-4", - "Class 01 4-6-2", - "Class 103", - "Class 132", - "Class 500 4-6-0", - "Class 9100", - "Class EF 66", - "Class 6E", - "Consolidation 2-8-0", - "Crampton 4-2-0", - "DD 080-X", - "DD40AX", - "Duke Class 4-4-0", - "E18", - "E428", - "Brenner E412", - "E60CP", - "Eight Wheeler 4-4-0", - "EP-2 Bipolar", - "ET22", - "F3", - "Fairlie 0-6-6-0", - "Firefly 2-2-2", - "FP45", - "Ge 6/6 Crocodile", - "GG1", - "GP7", - "H10 2-8-2", - "HST 125", - "Kriegslok 2-10-0", - "Mallard 4-6-2", - "Norris 4-2-0", - "Northern 4-8-4", - "Orca NX462", - "Pacific 4-6-2", - "Planet 2-2-0", - "Re 6/6", - "Red Devil 4-8-4", - "S3 4-4-0", - "NA-90D", - "Shay (2-Truck)", - "Shinkansen Series 0", - "Stirling 4-2-2", - "Trans-Euro", - "V200", - "VL80T", - "GP 35", - "U1", - "Zephyr", -]; - -const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; -const REAL_WORLD_VARIABLE_1_CONDITION_ID: i32 = 2241; -const REAL_WORLD_VARIABLE_2_CONDITION_ID: i32 = 2242; -const REAL_WORLD_VARIABLE_3_CONDITION_ID: i32 = 2243; -const REAL_WORLD_VARIABLE_4_CONDITION_ID: i32 = 2244; -const REAL_COMPANY_VARIABLE_1_CONDITION_ID: i32 = 2245; -const REAL_COMPANY_VARIABLE_2_CONDITION_ID: i32 = 2246; -const REAL_COMPANY_VARIABLE_3_CONDITION_ID: i32 = 2247; -const REAL_COMPANY_VARIABLE_4_CONDITION_ID: i32 = 2248; -const REAL_PLAYER_VARIABLE_1_CONDITION_ID: i32 = 2249; -const REAL_PLAYER_VARIABLE_2_CONDITION_ID: i32 = 2250; -const REAL_PLAYER_VARIABLE_3_CONDITION_ID: i32 = 2251; -const REAL_PLAYER_VARIABLE_4_CONDITION_ID: i32 = 2252; -const REAL_TERRITORY_VARIABLE_1_CONDITION_ID: i32 = 2253; -const REAL_TERRITORY_VARIABLE_2_CONDITION_ID: i32 = 2254; -const REAL_TERRITORY_VARIABLE_3_CONDITION_ID: i32 = 2255; -const REAL_TERRITORY_VARIABLE_4_CONDITION_ID: i32 = 2256; -const REAL_CHAIRMAN_CASH_CONDITION_ID: i32 = 2218; -const REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID: i32 = 2239; -const REAL_CHAIRMAN_NET_WORTH_CONDITION_ID: i32 = 2240; -const REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID: i32 = 1247; -const REAL_INVESTOR_CONFIDENCE_CONDITION_ID: i32 = 2366; -const REAL_CREDIT_RATING_CONDITION_ID: i32 = 2367; -const REAL_PRIME_RATE_CONDITION_ID: i32 = 2368; -const REAL_MANAGEMENT_ATTITUDE_CONDITION_ID: i32 = 2369; -const REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID: i32 = 2620; -const REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID: i32 = 200; -const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422; -const REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID: i32 = 2423; -const REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2418; -const REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2419; -const REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2420; -const REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2421; -const REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547; -const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516; - -const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 56] = [ - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_WORLD_VARIABLE_1_CONDITION_ID, - label: "Game Variable 1", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(1)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_WORLD_VARIABLE_2_CONDITION_ID, - label: "Game Variable 2", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(2)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_WORLD_VARIABLE_3_CONDITION_ID, - label: "Game Variable 3", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(3)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_WORLD_VARIABLE_4_CONDITION_ID, - label: "Game Variable 4", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(4)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_COMPANY_VARIABLE_1_CONDITION_ID, - label: "Company Variable 1", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(1)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_COMPANY_VARIABLE_2_CONDITION_ID, - label: "Company Variable 2", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(2)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_COMPANY_VARIABLE_3_CONDITION_ID, - label: "Company Variable 3", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(3)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_COMPANY_VARIABLE_4_CONDITION_ID, - label: "Company Variable 4", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(4)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_PLAYER_VARIABLE_1_CONDITION_ID, - label: "Player Variable 1", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(1)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_PLAYER_VARIABLE_2_CONDITION_ID, - label: "Player Variable 2", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(2)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_PLAYER_VARIABLE_3_CONDITION_ID, - label: "Player Variable 3", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(3)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_PLAYER_VARIABLE_4_CONDITION_ID, - label: "Player Variable 4", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(4)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_TERRITORY_VARIABLE_1_CONDITION_ID, - label: "Territory Variable 1", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(1)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_TERRITORY_VARIABLE_2_CONDITION_ID, - label: "Territory Variable 2", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(2)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_TERRITORY_VARIABLE_3_CONDITION_ID, - label: "Territory Variable 3", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(3)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_TERRITORY_VARIABLE_4_CONDITION_ID, - label: "Territory Variable 4", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(4)), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 1802, - label: "Current Cash", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::CurrentCash, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID, - label: "Player Cash", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( - RuntimeChairmanMetric::CurrentCash, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID, - label: "Player Stock Value", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( - RuntimeChairmanMetric::HoldingsValueTotal, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_CHAIRMAN_NET_WORTH_CONDITION_ID, - label: "Player Net Worth", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( - RuntimeChairmanMetric::NetWorthTotal, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID, - label: "Purchasing Power", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( - RuntimeChairmanMetric::PurchasingPowerTotal, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 951, - label: "Total Debt", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::TotalDebt, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_INVESTOR_CONFIDENCE_CONDITION_ID, - label: "Investor Confidence", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::InvestorConfidence, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_CREDIT_RATING_CONDITION_ID, - label: "Credit Rating", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::CreditRating, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_PRIME_RATE_CONDITION_ID, - label: "Prime Rate", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::PrimeRate, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_MANAGEMENT_ATTITUDE_CONDITION_ID, - label: "Management Attitude", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::ManagementAttitude, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID, - label: "Book Value Per Share", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::BookValuePerShare, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2293, - label: "Company Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::TrackPiecesTotal, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2294, - label: "Company Single Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::TrackPiecesSingle, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2295, - label: "Company Double Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::TrackPiecesDouble, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2296, - label: "Company Transition Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::TrackPiecesTransition, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2297, - label: "Company Electric Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::TrackPiecesElectric, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2298, - label: "Company Non-Electric Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( - RuntimeCompanyMetric::TrackPiecesNonElectric, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2313, - label: "Territory Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( - RuntimeTerritoryMetric::TrackPiecesTotal, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2314, - label: "Territory Single Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( - RuntimeTerritoryMetric::TrackPiecesSingle, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2315, - label: "Territory Double Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( - RuntimeTerritoryMetric::TrackPiecesDouble, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2316, - label: "Territory Transition Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( - RuntimeTerritoryMetric::TrackPiecesTransition, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2317, - label: "Territory Electric Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( - RuntimeTerritoryMetric::TrackPiecesElectric, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2318, - label: "Territory Non-Electric Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( - RuntimeTerritoryMetric::TrackPiecesNonElectric, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2323, - label: "Company-Territory Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( - RuntimeTrackMetric::Total, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2324, - label: "Company-Territory Single Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( - RuntimeTrackMetric::Single, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2325, - label: "Company-Territory Double Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( - RuntimeTrackMetric::Double, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2326, - label: "Company-Territory Transition Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( - RuntimeTrackMetric::Transition, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2327, - label: "Company-Territory Electric Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( - RuntimeTrackMetric::Electric, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2328, - label: "Company-Territory Non-Electric Track Pieces", - kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( - RuntimeTrackMetric::NonElectric, - )), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, - label: "%1 Avail.", - kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID, - label: "%1 Production", - kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID, - label: "Unknown Loco Available", - kind: RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::NamedLocomotiveAvailability, - ), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID, - label: "Unknown Loco Cost", - kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID, - label: "All Cargo Production", - kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID, - label: "All Factory Production", - kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID, - label: "All Farm/Mine Production", - kind: RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::FarmMineProductionTotal, - ), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID, - label: "Unknown Cargo Production", - kind: RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::OtherCargoProductionTotal, - ), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID, - label: "Limited Track Building Amount", - kind: RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::LimitedTrackBuildingAmount, - ), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: REAL_TERRITORY_ACCESS_COST_CONDITION_ID, - label: "Access Rights Cost:", - kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost), - }, - RealOrdinaryConditionMetadata { - raw_condition_id: 2350, - label: "Economic Status", - kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus), - }, -]; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct KnownSpecialConditionDefinition { - slot_index: u8, - hidden: bool, - label_id: u32, - help_id: u32, - label: &'static str, -} - -const KNOWN_SPECIAL_CONDITION_DEFINITIONS: [KnownSpecialConditionDefinition; - SPECIAL_CONDITION_COUNT] = [ - KnownSpecialConditionDefinition { - slot_index: 0, - hidden: false, - label_id: 2535, - help_id: 2564, - label: "Disable Stock Buying and Selling", - }, - KnownSpecialConditionDefinition { - slot_index: 1, - hidden: false, - label_id: 2536, - help_id: 2565, - label: "Disable Margin Buying/Short Selling Stock", - }, - KnownSpecialConditionDefinition { - slot_index: 2, - hidden: false, - label_id: 2537, - help_id: 2566, - label: "Disable Company Issue/Buy Back Stock", - }, - KnownSpecialConditionDefinition { - slot_index: 3, - hidden: false, - label_id: 2538, - help_id: 2567, - label: "Disable Issuing/Repaying Bonds", - }, - KnownSpecialConditionDefinition { - slot_index: 4, - hidden: false, - label_id: 2539, - help_id: 2568, - label: "Disable Declaring Bankruptcy", - }, - KnownSpecialConditionDefinition { - slot_index: 5, - hidden: false, - label_id: 2540, - help_id: 2569, - label: "Disable Changing the Dividend Rate", - }, - KnownSpecialConditionDefinition { - slot_index: 6, - hidden: false, - label_id: 2541, - help_id: 2570, - label: "Disable Replacing a Locomotive", - }, - KnownSpecialConditionDefinition { - slot_index: 7, - hidden: false, - label_id: 2542, - help_id: 2571, - label: "Disable Retiring a Train", - }, - KnownSpecialConditionDefinition { - slot_index: 8, - hidden: false, - label_id: 2543, - help_id: 2572, - label: "Disable Changing Cargo Consist On Train", - }, - KnownSpecialConditionDefinition { - slot_index: 9, - hidden: false, - label_id: 2544, - help_id: 2573, - label: "Disable Buying a Train", - }, - KnownSpecialConditionDefinition { - slot_index: 10, - hidden: false, - label_id: 2545, - help_id: 2574, - label: "Disable All Track Building", - }, - KnownSpecialConditionDefinition { - slot_index: 11, - hidden: false, - label_id: 2546, - help_id: 2575, - label: "Disable Unconnected Track Building", - }, - KnownSpecialConditionDefinition { - slot_index: 12, - hidden: false, - label_id: 2547, - help_id: 2576, - label: "Limited Track Building Amount", - }, - KnownSpecialConditionDefinition { - slot_index: 13, - hidden: false, - label_id: 2548, - help_id: 2577, - label: "Disable Building Stations", - }, - KnownSpecialConditionDefinition { - slot_index: 14, - hidden: false, - label_id: 2549, - help_id: 2578, - label: "Disable Building Hotel/Restaurant/Tavern/Post Office", - }, - KnownSpecialConditionDefinition { - slot_index: 15, - hidden: false, - label_id: 2550, - help_id: 2579, - label: "Disable Building Customs House", - }, - KnownSpecialConditionDefinition { - slot_index: 16, - hidden: false, - label_id: 2551, - help_id: 2580, - label: "Disable Building Industry Buildings", - }, - KnownSpecialConditionDefinition { - slot_index: 17, - hidden: false, - label_id: 2552, - help_id: 2581, - label: "Disable Buying Existing Industry Buildings", - }, - KnownSpecialConditionDefinition { - slot_index: 18, - hidden: false, - label_id: 2553, - help_id: 2582, - label: "Disable Being Fired As Chairman", - }, - KnownSpecialConditionDefinition { - slot_index: 19, - hidden: false, - label_id: 2554, - help_id: 2583, - label: "Disable Resigning as Chairman", - }, - KnownSpecialConditionDefinition { - slot_index: 20, - hidden: false, - label_id: 2555, - help_id: 2584, - label: "Disable Chairmanship Takeover", - }, - KnownSpecialConditionDefinition { - slot_index: 21, - hidden: false, - label_id: 2556, - help_id: 2585, - label: "Disable Starting Any Companies", - }, - KnownSpecialConditionDefinition { - slot_index: 22, - hidden: false, - label_id: 2557, - help_id: 2586, - label: "Disable Starting Multiple Companies", - }, - KnownSpecialConditionDefinition { - slot_index: 23, - hidden: false, - label_id: 2558, - help_id: 2587, - label: "Disable Merging Companies", - }, - KnownSpecialConditionDefinition { - slot_index: 24, - hidden: false, - label_id: 2559, - help_id: 2588, - label: "Disable Bulldozing", - }, - KnownSpecialConditionDefinition { - slot_index: 25, - hidden: false, - label_id: 2560, - help_id: 2589, - label: "Show Visited Track", - }, - KnownSpecialConditionDefinition { - slot_index: 26, - hidden: false, - label_id: 2561, - help_id: 2590, - label: "Show Visited Stations", - }, - KnownSpecialConditionDefinition { - slot_index: 27, - hidden: false, - label_id: 2562, - help_id: 2591, - label: "Use Slow Date", - }, - KnownSpecialConditionDefinition { - slot_index: 28, - hidden: false, - label_id: 2563, - help_id: 2592, - label: "Completely Disable Money-Related Things", - }, - KnownSpecialConditionDefinition { - slot_index: 29, - hidden: false, - label_id: 2874, - help_id: 2875, - label: "Use Bio-Accelerator Cars", - }, - KnownSpecialConditionDefinition { - slot_index: 30, - hidden: false, - label_id: 3722, - help_id: 3723, - label: "Disable Cargo Economy", - }, - KnownSpecialConditionDefinition { - slot_index: 31, - hidden: false, - label_id: 3835, - help_id: 3836, - label: "Use Wartime Cargos", - }, - KnownSpecialConditionDefinition { - slot_index: 32, - hidden: false, - label_id: 3850, - help_id: 3851, - label: "Disable Train Crashes", - }, - KnownSpecialConditionDefinition { - slot_index: 33, - hidden: false, - label_id: 3852, - help_id: 3853, - label: "Disable Train Crashes AND Breakdowns", - }, - KnownSpecialConditionDefinition { - slot_index: 34, - hidden: false, - label_id: 3920, - help_id: 3921, - label: "AI Ignore Territories At Startup", - }, - KnownSpecialConditionDefinition { - slot_index: 35, - hidden: true, - label_id: 3, - help_id: 3, - label: "Hidden sentinel", - }, -]; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct KnownTagDefinition { - tag_id: u16, - label: &'static str, - grounded_meaning: &'static str, -} - -const KNOWN_TAG_DEFINITIONS: [KnownTagDefinition; 4] = [ - KnownTagDefinition { - tag_id: 0x2cee, - label: "overlay_mask_plane_primary", - grounded_meaning: "Primary one-byte overlay mask plane restored into world offset +0x1655.", - }, - KnownTagDefinition { - tag_id: 0x2d51, - label: "overlay_mask_plane_secondary", - grounded_meaning: "Secondary one-byte overlay mask plane restored into world offset +0x1659.", - }, - KnownTagDefinition { - tag_id: 0x9471, - label: "sidecar_byte_plane_family_low", - grounded_meaning: "Lower bound of the grounded sidecar byte-plane chunk family.", - }, - KnownTagDefinition { - tag_id: 0x9472, - label: "sidecar_byte_plane_family_high", - grounded_meaning: "Upper bound of the grounded sidecar byte-plane chunk family.", - }, -]; - -fn known_special_condition_definition_for_label_id( - label_id: u32, -) -> Option { - KNOWN_SPECIAL_CONDITION_DEFINITIONS - .iter() - .copied() - .find(|definition| !definition.hidden && definition.label_id == label_id) -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpKnownTagHit { - pub tag_id: u16, - pub tag_hex: String, - pub label: String, - pub grounded_meaning: String, - pub hit_count: usize, - pub sample_offsets: Vec, - pub last_offset: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPreambleWord { - pub index: usize, - pub offset: usize, - pub value_le: u32, - pub value_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPreamble { - pub byte_len: usize, - pub word_count: usize, - pub words: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpAsciiPreview { - pub offset: usize, - pub byte_len: usize, - pub preview: String, - pub truncated: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpMapTitleHintTitleHit { - pub title: String, - pub earliest_offset: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpMapTitleHintMapReference { - pub offset: usize, - pub text: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpMapTitleHintAdjacentPair { - pub map_reference_offset: usize, - pub map_reference_text: String, - pub title_offset: usize, - pub title: String, - pub byte_distance: usize, - pub normalized_stem_match: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpMapTitleHintProbe { - pub source_kind: String, - pub profile_family: Option, - pub grounded_title_hits: Vec, - pub embedded_map_references: Vec, - pub adjacent_reference_title_pairs: Vec, - pub strongest_same_stem_pair: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSharedHeader { - pub byte_len: usize, - pub root_kind_word: u32, - pub root_kind_word_hex: String, - pub primary_family_tag: u32, - pub primary_family_tag_hex: String, - pub shared_signature_words_1_to_7: Vec, - pub shared_signature_hex_words_1_to_7: Vec, - pub matches_grounded_common_signature: bool, - pub payload_window_words_8_to_9: Vec, - pub payload_window_hex_words_8_to_9: Vec, - pub reserved_words_10_to_14: Vec, - pub reserved_words_10_to_14_all_zero: bool, - pub final_flag_word: u32, - pub final_flag_word_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpHeaderVariantProbe { - pub variant_family: String, - pub variant_evidence: Vec, - pub is_known_family: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpEarlyContentProbe { - pub first_post_text_nonzero_offset: usize, - pub zero_pad_after_text_len: usize, - pub first_post_text_block_len: usize, - pub first_post_text_block_hex: String, - pub trailing_zero_pad_after_first_block_len: usize, - pub secondary_nonzero_offset: Option, - pub secondary_aligned_word_window_offset: Option, - pub secondary_aligned_word_window_words: Vec, - pub secondary_aligned_word_window_hex_words: Vec, - pub secondary_preview_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSecondaryVariantProbe { - pub aligned_window_offset: usize, - pub words: Vec, - pub hex_words: Vec, - pub variant_family: String, - pub variant_evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpContainerProfile { - pub profile_family: String, - pub profile_evidence: Vec, - pub is_known_profile: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveBootstrapBlock { - pub profile_family: String, - pub aligned_window_offset: usize, - pub leading_word: u32, - pub leading_word_hex: String, - pub anchor_word: u32, - pub anchor_word_hex: String, - pub descriptor_word_2: u32, - pub descriptor_word_2_hex: String, - pub descriptor_word_3: u32, - pub descriptor_word_3_hex: String, - pub descriptor_word_4: u32, - pub descriptor_word_4_hex: String, - pub descriptor_word_5: u32, - pub descriptor_word_5_hex: String, - pub descriptor_word_6: u32, - pub descriptor_word_6_hex: String, - pub descriptor_word_7: u32, - pub descriptor_word_7_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveAnchorRunBlock { - pub profile_family: String, - pub cycle_start_offset: usize, - pub cycle_words: Vec, - pub cycle_hex_words: Vec, - pub full_cycle_count: usize, - pub partial_cycle_word_count: usize, - pub trailer_offset: usize, - pub trailer_words: Vec, - pub trailer_hex_words: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRuntimeAnchorCycleBlock { - pub profile_family: String, - pub cycle_start_offset: usize, - pub cycle_words: Vec, - pub cycle_hex_words: Vec, - pub full_cycle_count: usize, - pub partial_cycle_word_count: usize, - pub trailer_offset: usize, - pub trailer_words: Vec, - pub trailer_hex_words: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRuntimeTrailerBlock { - pub profile_family: String, - pub trailer_family: String, - pub trailer_evidence: Vec, - pub trailer_offset: usize, - pub prefix_words_0_to_5: Vec, - pub prefix_hex_words_0_to_5: Vec, - pub tag_word_6: u32, - pub tag_word_6_hex: String, - pub tag_chunk_id_u16: u16, - pub tag_chunk_id_hex: String, - pub tag_chunk_id_grounded_alignment: Option, - pub length_word_7: u32, - pub length_word_7_hex: String, - pub length_high_u16: u16, - pub length_high_hex: String, - pub selector_word_8: u32, - pub selector_word_8_hex: String, - pub selector_high_u16: u16, - pub selector_high_hex: String, - pub layout_word_9: u32, - pub layout_word_9_hex: String, - pub descriptor_word_10: u32, - pub descriptor_word_10_hex: String, - pub descriptor_high_u16: u16, - pub descriptor_high_hex: String, - pub descriptor_word_11: u32, - pub descriptor_word_11_hex: String, - pub counter_word_12: u32, - pub counter_word_12_hex: String, - pub offset_word_13: u32, - pub offset_word_13_hex: String, - pub span_word_14: u32, - pub span_word_14_hex: String, - pub mode_word_15: u32, - pub mode_word_15_hex: String, - pub words: Vec, - pub hex_words: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRuntimePostSpanProbe { - pub profile_family: String, - pub span_target_offset: usize, - pub next_nonzero_offset: Option, - pub next_aligned_candidate_offset: Option, - pub next_aligned_candidate_words: Vec, - pub next_aligned_candidate_hex_words: Vec, - pub header_candidates: Vec, - pub grounded_progress_hits: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRuntimePostSpanHeaderCandidate { - pub offset: usize, - pub words: Vec, - pub hex_words: Vec, - pub dense_word_count: usize, - pub high_u16_words: Vec, - pub high_hex_words: Vec, - pub grounded_alignments: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRt3105PostSpanBridgeProbe { - pub profile_family: String, - pub bridge_family: String, - pub bridge_evidence: Vec, - pub span_target_offset: usize, - pub next_candidate_offset: Option, - pub next_candidate_delta_from_span_target: Option, - pub packed_profile_offset: usize, - pub packed_profile_delta_from_span_target: usize, - pub next_candidate_delta_from_packed_profile: Option, - pub selector_high_u16: u16, - pub selector_high_hex: String, - pub descriptor_high_u16: u16, - pub descriptor_high_hex: String, - pub next_candidate_high_u16_words: Vec, - pub next_candidate_high_hex_words: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRt3105SaveBridgePayloadProbe { - pub profile_family: String, - pub bridge_family: String, - pub primary_block_offset: usize, - pub primary_block_len: usize, - pub primary_block_len_hex: String, - pub primary_words: Vec, - pub primary_hex_words: Vec, - pub secondary_block_offset: usize, - pub secondary_block_delta_from_primary: usize, - pub secondary_block_delta_from_primary_hex: String, - pub secondary_block_end_offset: usize, - pub secondary_block_len: usize, - pub secondary_block_len_hex: String, - pub secondary_preview_word_count: usize, - pub secondary_words: Vec, - pub secondary_hex_words: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveWorldSelectionContextProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub chunk_tag_offset: usize, - pub payload_offset: usize, - pub payload_len: usize, - pub payload_len_hex: String, - pub selected_company_id_offset: usize, - pub selected_company_id: u32, - pub selected_company_id_hex: String, - pub selected_chairman_profile_id_offset: usize, - pub selected_chairman_profile_id: u32, - pub selected_chairman_profile_id_hex: String, - pub chairman_slot_selector_offset: usize, - pub chairman_slot_selectors: Vec, - pub campaign_override_flag_offset: usize, - pub campaign_override_flag: u8, - pub campaign_override_flag_hex: String, - pub chairman_role_gate_offset: usize, - pub chairman_role_gate_bytes: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveWorldEconomicTuningProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub chunk_tag_offset: usize, - pub payload_offset: usize, - pub payload_len: usize, - pub payload_len_hex: String, - pub mirror_lane: SmpSaveDwordCandidate, - pub tuning_lanes: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveWorldIssue37Probe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub chunk_tag_offset: usize, - pub payload_offset: usize, - pub payload_len: usize, - pub payload_len_hex: String, - pub issue_37_raw_u8: u8, - pub issue_37_raw_hex: String, - pub issue_38_raw_u8: u8, - pub issue_38_raw_hex: String, - pub issue_39_raw_u8: u8, - pub issue_39_raw_hex: String, - pub issue_3a_raw_u8: u8, - pub issue_3a_raw_hex: String, - pub issue_value_lane: SmpSaveDwordCandidate, - pub multiplier_lane: SmpSaveDwordCandidate, - #[serde(default)] - pub issue_opinion_base_terms_raw_i32: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveWorldFinanceNeighborhoodProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub chunk_tag_offset: usize, - pub payload_offset: usize, - pub payload_len: usize, - pub payload_len_hex: String, - pub packed_year_word_raw_u16: u16, - pub packed_year_word_raw_hex: String, - pub partial_year_progress_raw_u8: u8, - pub partial_year_progress_raw_hex: String, - pub current_calendar_tuple_word_lane: SmpSaveDwordCandidate, - pub current_calendar_tuple_word_2_lane: SmpSaveDwordCandidate, - pub absolute_counter_lane: SmpSaveDwordCandidate, - pub absolute_counter_mirror_lane: SmpSaveDwordCandidate, - pub stock_policy_raw_u8: u8, - pub stock_policy_raw_hex: String, - pub bond_policy_raw_u8: u8, - pub bond_policy_raw_hex: String, - pub bankruptcy_policy_raw_u8: u8, - pub bankruptcy_policy_raw_hex: String, - pub dividend_policy_raw_u8: u8, - pub dividend_policy_raw_hex: String, - pub building_density_growth_setting_lane: SmpSaveDwordCandidate, - pub dword_candidates: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveTaggedCollectionHeaderProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub metadata_tag_offset: usize, - pub records_tag_offset: usize, - pub close_tag_offset: usize, - pub direct_collection_flag: u32, - pub direct_collection_flag_hex: String, - pub direct_record_stride: u32, - pub direct_record_stride_hex: String, - pub live_id_bound: u32, - pub live_id_bound_hex: String, - pub live_record_count: u32, - pub live_record_count_hex: String, - pub header_words: Vec, - pub header_hex_words: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveUnclassifiedTaggedCollectionHeaderProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub metadata_tag: u32, - pub metadata_tag_hex: String, - pub records_tag: u32, - pub records_tag_hex: String, - pub close_tag: u32, - pub close_tag_hex: String, - pub metadata_tag_offset: usize, - pub records_tag_offset: usize, - pub close_tag_offset: usize, - pub records_span_len: usize, - pub direct_collection_flag: u32, - pub direct_collection_flag_hex: String, - pub direct_record_stride: u32, - pub direct_record_stride_hex: String, - pub live_id_bound: u32, - pub live_id_bound_hex: String, - pub live_record_count: u32, - pub live_record_count_hex: String, - pub header_words: Vec, - pub header_hex_words: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveTrainCollectionDirectoryEntryProbe { - pub live_entry_id: u32, - pub payload_relative_offset: u32, - pub payload_relative_offset_hex: String, - pub payload_absolute_offset: usize, - pub previous_live_entry_id: u32, - pub previous_live_entry_id_hex: String, - pub next_live_entry_id: u32, - pub next_live_entry_id_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveTrainCollectionDirectoryProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub metadata_tag_offset: usize, - pub records_tag_offset: usize, - pub close_tag_offset: usize, - pub directory_root_dword_index: usize, - pub directory_entry_dword_count: usize, - pub live_record_count: u32, - pub live_id_bound: u32, - #[serde(default)] - pub chain_head_live_entry_id: Option, - #[serde(default)] - pub chain_tail_live_entry_id: Option, - pub entries: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveRegionProfileEntryProbe { - pub entry_index: usize, - pub row_relative_offset: usize, - pub name: String, - pub trailing_weight_f32: f32, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveRegionProfileCollectionProbe { - pub direct_collection_flag: u32, - pub entry_stride: u32, - pub live_id_bound: u32, - pub live_record_count: u32, - pub entry_start_relative_offset: usize, - pub trailing_padding_len: usize, - #[serde(default)] - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveRegionRecordTripletEntryProbe { - pub record_index: usize, - pub name: String, - pub record_payload_relative_offset: usize, - pub record_payload_relative_offset_hex: String, - pub name_tag_relative_offset: usize, - pub policy_tag_relative_offset: usize, - pub profile_tag_relative_offset: usize, - pub pre_name_prefix_len: usize, - #[serde(default)] - pub pre_name_prefix_hex_bytes: Vec, - #[serde(default)] - pub pre_name_prefix_dword_candidates: Vec, - pub policy_chunk_len: usize, - pub profile_chunk_len: usize, - pub policy_leading_f32_0: f32, - pub policy_leading_f32_1: f32, - pub policy_leading_f32_2: f32, - #[serde(default)] - pub policy_reserved_dwords: Vec, - #[serde(default)] - pub policy_reserved_dword_candidates: Vec, - pub policy_trailing_word: u16, - pub policy_trailing_word_hex: String, - #[serde(default)] - pub profile_collection: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveRegionRecordTripletProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub records_tag_offset: usize, - pub close_tag_offset: usize, - pub record_count: usize, - #[serde(default)] - pub entries: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpLoadedRegionProfileEntry { - pub entry_index: usize, - pub name: String, - pub trailing_weight_f32: f32, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpLoadedRegionProfileCollection { - pub direct_collection_flag: u32, - pub entry_stride: u32, - pub live_id_bound: u32, - pub live_record_count: u32, - pub trailing_padding_len: usize, - #[serde(default)] - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpLoadedRegionEntry { - pub record_index: usize, - pub name: String, - pub pre_name_prefix_len: usize, - pub policy_leading_f32_0: f32, - pub policy_leading_f32_1: f32, - pub policy_leading_f32_2: f32, - #[serde(default)] - pub policy_reserved_dwords: Vec, - pub policy_trailing_word: u16, - pub policy_trailing_word_hex: String, - #[serde(default)] - pub profile_collection: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpLoadedRegionCollection { - pub source_kind: String, - pub semantic_family: String, - pub observed_entry_count: usize, - #[serde(default)] - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedRegionFixedRowRunSummary { - pub source_kind: String, - pub semantic_family: String, - pub target_row_count: usize, - pub target_row_stride: usize, - pub target_row_stride_hex: String, - #[serde(default)] - pub candidates: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveRegionQueuedNoticeRecordEntryProbe { - pub node_base_offset: usize, - pub payload_seed_offset: usize, - pub next_link_raw: u32, - pub next_link_raw_hex: String, - pub payload_seed_dword: u32, - pub payload_seed_dword_hex: String, - pub kind: u32, - pub kind_hex: String, - pub promotion_latch_dword: u32, - pub promotion_latch_dword_hex: String, - pub region_id: u32, - pub region_id_hex: String, - pub amount: u32, - pub amount_hex: String, - pub trailing_sentinel_i32_0: i32, - pub trailing_sentinel_i32_0_hex: String, - pub trailing_sentinel_i32_1: i32, - pub trailing_sentinel_i32_1_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveRegionQueuedNoticeRecordProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub payload_seed_dword: u32, - pub payload_seed_dword_hex: String, - #[serde(default)] - pub entries: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveFixedRowRunDwordLaneSummary { - pub relative_offset: usize, - pub relative_offset_hex: String, - pub zero_count: usize, - pub nonzero_count: usize, - pub distinct_value_count: usize, - pub probable_normal_f32_count: usize, - pub small_unsigned_count: usize, - #[serde(default)] - pub sample_values_hex: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveRegionFixedRowRunCandidate { - pub count_offset: usize, - pub count_offset_hex: String, - pub row_count: usize, - pub row_stride: usize, - pub row_stride_hex: String, - pub rows_offset: usize, - pub rows_offset_hex: String, - pub rows_end_offset: usize, - pub rows_end_offset_hex: String, - pub distance_to_region_metadata_tag: usize, - pub distance_to_region_metadata_tag_hex: String, - #[serde(default)] - pub dword_lane_summaries: Vec, - pub shape_signature: String, - pub shape_family_signature: String, - pub trailing_byte_zero_count: usize, - pub trailing_byte_nonzero_count: usize, - pub trailing_byte_distinct_value_count: usize, - #[serde(default)] - pub trailing_byte_sample_values_hex: Vec, - #[serde(default)] - pub best_probable_density_lane_relative_offset_hex: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveRegionFixedRowRunCandidateProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub target_row_count: usize, - pub target_row_stride: usize, - pub target_row_stride_hex: String, - pub scan_start_offset: usize, - pub scan_start_offset_hex: String, - pub scan_end_offset: usize, - pub scan_end_offset_hex: String, - #[serde(default)] - pub candidates: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveRegionFixedRowRunSharedShapeMatch { - pub shape_signature: String, - pub left_rank: usize, - pub left_rows_offset_hex: String, - pub left_best_probable_density_lane_relative_offset_hex: Option, - pub right_rank: usize, - pub right_rows_offset_hex: String, - pub right_best_probable_density_lane_relative_offset_hex: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveRegionFixedRowRunComparisonReport { - pub left_profile_family: String, - pub right_profile_family: String, - pub left_best_rows_offset_hex: Option, - pub right_best_rows_offset_hex: Option, - pub left_best_shape_signature: Option, - pub right_best_shape_signature: Option, - pub left_best_shape_family_signature: Option, - pub right_best_shape_family_signature: Option, - #[serde(default)] - pub shared_shape_matches: Vec, - #[serde(default)] - pub shared_shape_family_matches: Vec, - #[serde(default)] - pub left_only_shape_signatures: Vec, - #[serde(default)] - pub right_only_shape_signatures: Vec, - #[serde(default)] - pub left_only_shape_family_signatures: Vec, - #[serde(default)] - pub right_only_shape_family_signatures: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureRecordTripletEntryProbe { - pub record_index: usize, - pub primary_name: String, - pub secondary_name: String, - pub name_tag_relative_offset: usize, - pub policy_tag_relative_offset: usize, - pub profile_tag_relative_offset: usize, - pub policy_chunk_len: usize, - pub profile_chunk_len: usize, - pub policy_f32_lane_0: f32, - pub policy_f32_lane_1: f32, - pub policy_f32_lane_2: f32, - pub policy_f32_lane_3: f32, - pub policy_f32_lane_4: f32, - pub policy_reserved_dword: u32, - pub policy_trailing_word: u16, - pub policy_trailing_word_hex: String, - pub profile_open_marker: u32, - pub profile_open_marker_hex: String, - pub profile_repeated_primary_name: String, - pub profile_repeated_secondary_name: String, - pub profile_footer_relative_offset: usize, - pub profile_footer_relative_offset_hex: String, - pub profile_pre_footer_padding_len: usize, - #[serde(default)] - pub profile_pre_footer_padding_hex_bytes: Vec, - #[serde(default)] - pub profile_companion_byte_u8: Option, - #[serde(default)] - pub profile_companion_byte_hex: Option, - pub profile_payload_dword: u32, - pub profile_payload_dword_hex: String, - pub profile_sentinel_i32: i32, - pub profile_status_kind: String, - pub farm_growth_stage_index: Option, - pub profile_close_marker: u32, - pub profile_close_marker_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureRecordTripletProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub records_tag_offset: usize, - pub close_tag_offset: usize, - pub record_count: usize, - #[serde(default)] - pub entries: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPlacedStructureEntry { - pub record_index: usize, - pub primary_name: String, - pub secondary_name: String, - pub policy_trailing_word: u16, - pub policy_trailing_word_hex: String, - pub profile_payload_dword: u32, - pub profile_payload_dword_hex: String, - pub profile_status_kind: String, - #[serde(default)] - pub farm_growth_stage_index: Option, - #[serde(default)] - pub profile_companion_byte_u8: Option, - #[serde(default)] - pub profile_companion_byte_hex: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPlacedStructureCollection { - pub source_kind: String, - pub semantic_family: String, - pub observed_entry_count: usize, - #[serde(default)] - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub metadata_tag_offset: usize, - pub records_tag_offset: usize, - pub close_tag_offset: usize, - pub records_span_len: usize, - pub direct_record_stride: u32, - pub direct_record_stride_hex: String, - pub live_id_bound: u32, - pub live_id_bound_hex: String, - pub live_record_count: u32, - pub live_record_count_hex: String, - pub owner_shared_dword: u32, - pub owner_shared_dword_hex: String, - pub owner_shared_dword_relative_offset: usize, - pub owner_shared_dword_matches_first_compact_prefix_leading_dword: bool, - #[serde(default)] - pub first_record_child_count_after_owner_shared: Option, - #[serde(default)] - pub first_record_child_count_after_owner_shared_hex: Option, - #[serde(default)] - pub first_record_saved_primary_child_byte_after_owner_shared: Option, - #[serde(default)] - pub first_record_saved_primary_child_byte_after_owner_shared_hex: Option, - #[serde(default)] - pub first_record_first_name_tag_relative_offset_after_owner_shared: Option, - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - pub first_embedded_name_tag_relative_offset: usize, - pub embedded_name_tag_count: usize, - pub decoded_embedded_name_row_count: usize, - pub decoded_embedded_name_row_with_tertiary_name_count: usize, - pub unique_compact_prefix_pattern_count: usize, - pub prefix_leading_dword_matching_embedded_profile_tag_count: usize, - pub unique_embedded_name_pair_count: usize, - #[serde(default)] - pub first_embedded_primary_name: Option, - #[serde(default)] - pub first_embedded_secondary_name: Option, - #[serde(default)] - pub first_embedded_tertiary_name: Option, - #[serde(default)] - pub embedded_name_row_samples: Vec, - #[serde(default)] - pub compact_prefix_pattern_summaries: - Vec, - #[serde(default)] - pub name_pair_summaries: Vec, - #[serde(default)] - pub payload_envelope_summary: - Option, - #[serde(default)] - pub live_entry_prelude_summary: - Option, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPlacedStructureDynamicSideBufferSummary { - pub source_kind: String, - pub semantic_family: String, - pub observed_entry_count: u32, - pub owner_shared_dword_hex: String, - pub unique_embedded_name_pair_count: usize, - pub decoded_embedded_name_row_count: usize, - pub first_prefix_leading_dword_hex: String, - pub first_prefix_trailing_word_hex: String, - pub first_prefix_separator_byte_hex: String, - pub triplet_alignment_overlap_count: usize, - pub triplet_alignment_side_buffer_only_name_pair_count: usize, - #[serde(default)] - pub compact_prefix_pattern_summaries: - Vec, - #[serde(default)] - pub name_pair_summaries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferSampleEntry { - pub sample_index: usize, - pub name_tag_relative_offset: usize, - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - #[serde(default)] - pub tertiary_name: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary { - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - pub count: usize, - pub first_name_tag_relative_offset: usize, - pub prefix_leading_dword_matches_embedded_profile_tag: bool, - pub section_like_primary_name_count: usize, - pub cap_like_primary_name_count: usize, - pub other_primary_name_count: usize, - #[serde(default)] - pub first_primary_name: Option, - #[serde(default)] - pub first_secondary_name: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePairSummary { - pub primary_name: String, - pub secondary_name: String, - pub count: usize, - pub first_name_tag_relative_offset: usize, - pub unique_compact_prefix_pattern_count: usize, - pub dominant_prefix_leading_dword: u32, - pub dominant_prefix_leading_dword_hex: String, - pub dominant_prefix_trailing_word: u16, - pub dominant_prefix_trailing_word_hex: String, - pub dominant_prefix_separator_byte: u8, - pub dominant_prefix_separator_byte_hex: String, - pub dominant_prefix_count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSummary { - pub row_count_with_policy_tag_before_next_name: usize, - pub row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope: usize, - pub row_count_missing_policy_tag_before_next_name: usize, - pub row_count_missing_profile_tag_after_policy: usize, - #[serde(default)] - pub unique_policy_chunk_lens: Vec, - #[serde(default)] - pub unique_profile_chunk_lens: Vec, - #[serde(default)] - pub dominant_policy_chunk_len: Option, - pub dominant_policy_chunk_len_count: usize, - #[serde(default)] - pub dominant_profile_chunk_len: Option, - pub dominant_profile_chunk_len_count: usize, - #[serde(default)] - pub short_profile_flag_pair_summary: - Option, - #[serde(default)] - pub fixed_policy_summary: Option, - #[serde(default)] - pub name_prelude_candidate_summary: - Option, - #[serde(default)] - pub dominant_profile_span_class_summary: - Option, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanClassSummary { - pub profile_chunk_len_to_next_name_or_end: usize, - pub row_count: usize, - pub unique_name_pair_count: usize, - pub unique_compact_prefix_pattern_count: usize, - #[serde(default)] - pub dominant_candidate_pattern: - Option, - #[serde(default)] - pub dominant_primary_name: Option, - #[serde(default)] - pub dominant_secondary_name: Option, - pub dominant_name_pair_count: usize, - #[serde(default)] - pub dominant_prefix_leading_dword: Option, - #[serde(default)] - pub dominant_prefix_leading_dword_hex: Option, - #[serde(default)] - pub dominant_prefix_trailing_word: Option, - #[serde(default)] - pub dominant_prefix_trailing_word_hex: Option, - #[serde(default)] - pub dominant_prefix_separator_byte: Option, - #[serde(default)] - pub dominant_prefix_separator_byte_hex: Option, - pub dominant_prefix_count: usize, - #[serde(default)] - pub sample_rows: Vec, - #[serde(default)] - pub name_pair_summaries: - Vec, - #[serde(default)] - pub compact_prefix_pattern_summaries: - Vec, - #[serde(default)] - pub candidate_pattern_summaries: - Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanSample { - pub sample_index: usize, - pub name_tag_relative_offset: usize, - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - #[serde(default)] - pub child_count_candidate: Option, - #[serde(default)] - pub child_count_candidate_hex: Option, - #[serde(default)] - pub saved_primary_child_byte_candidate: Option, - #[serde(default)] - pub saved_primary_child_byte_candidate_hex: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanPrefixSummary { - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferFixedPolicySummary { - pub row_count_with_0x1a_policy_chunk: usize, - pub unique_trailing_word_count: usize, - #[serde(default)] - pub dominant_trailing_word: Option, - #[serde(default)] - pub dominant_trailing_word_hex: Option, - pub dominant_trailing_word_count: usize, - #[serde(default)] - pub compact_prefix_correlations: - Vec, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferFixedPolicySample { - pub sample_index: usize, - pub name_tag_relative_offset: usize, - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - #[serde(default)] - pub first_triplet_dwords_hex: Vec, - #[serde(default)] - pub second_triplet_dwords_hex: Vec, - pub trailing_word: u16, - pub trailing_word_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferFixedPolicyCompactPrefixCorrelation { - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - pub row_count: usize, - pub unique_policy_tuple_count: usize, - #[serde(default)] - pub dominant_primary_name: Option, - #[serde(default)] - pub dominant_secondary_name: Option, - pub dominant_name_pair_count: usize, - #[serde(default)] - pub dominant_mode_family: Option, - pub dominant_mode_family_count: usize, - #[serde(default)] - pub dominant_first_triplet_dwords_hex: Vec, - #[serde(default)] - pub dominant_second_triplet_dwords_hex: Vec, - #[serde(default)] - pub dominant_trailing_word: Option, - #[serde(default)] - pub dominant_trailing_word_hex: Option, - pub dominant_policy_tuple_count: usize, - #[serde(default)] - pub mode_family_counts: Vec, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSummary { - pub row_count_with_0x06_profile_span: usize, - pub unique_flag_pair_count: usize, - #[serde(default)] - pub dominant_first_flag_byte: Option, - #[serde(default)] - pub dominant_first_flag_byte_hex: Option, - pub dominant_first_flag_byte_count: usize, - #[serde(default)] - pub dominant_second_flag_byte: Option, - #[serde(default)] - pub dominant_second_flag_byte_hex: Option, - pub dominant_second_flag_byte_count: usize, - #[serde(default)] - pub dominant_flag_pair: Option, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPair { - pub first_flag_byte: u8, - pub first_flag_byte_hex: String, - pub second_flag_byte: u8, - pub second_flag_byte_hex: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSample { - pub sample_index: usize, - pub name_tag_relative_offset: usize, - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - pub first_flag_byte: u8, - pub first_flag_byte_hex: String, - pub second_flag_byte: u8, - pub second_flag_byte_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSample { - pub sample_index: usize, - pub name_tag_relative_offset: usize, - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - #[serde(default)] - pub name_payload_end_relative_offset: Option, - #[serde(default)] - pub policy_tag_relative_offset: Option, - #[serde(default)] - pub profile_tag_relative_offset: Option, - #[serde(default)] - pub next_name_tag_relative_offset: Option, - #[serde(default)] - pub name_to_policy_gap_len: Option, - #[serde(default)] - pub policy_chunk_len: Option, - #[serde(default)] - pub profile_chunk_len_to_next_name_or_end: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSummary { - pub row_count_with_candidate_window: usize, - pub unique_candidate_pattern_count: usize, - #[serde(default)] - pub dominant_child_count_candidate: Option, - pub dominant_child_count_candidate_count: usize, - #[serde(default)] - pub dominant_saved_primary_child_byte_candidate: Option, - #[serde(default)] - pub dominant_saved_primary_child_byte_candidate_hex: Option, - pub dominant_saved_primary_child_byte_candidate_count: usize, - #[serde(default)] - pub dominant_candidate_pattern: - Option, - #[serde(default)] - pub candidate_pattern_correlations: - Vec, - #[serde(default)] - pub profile_span_correlations: - Vec, - #[serde(default)] - pub compact_prefix_correlations: - Vec, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - pub child_count_candidate: u16, - pub child_count_candidate_hex: String, - pub saved_primary_child_byte_candidate: u8, - pub saved_primary_child_byte_candidate_hex: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSample { - pub sample_index: usize, - pub name_tag_relative_offset: usize, - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - pub child_count_candidate: u16, - pub child_count_candidate_hex: String, - pub saved_primary_child_byte_candidate: u8, - pub saved_primary_child_byte_candidate_hex: String, - #[serde(default)] - pub previous_profile_chunk_len_to_next_name_or_end: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - pub row_count: usize, - pub unique_name_pair_count: usize, - pub unique_profile_span_count: usize, - #[serde(default)] - pub dominant_primary_name: Option, - #[serde(default)] - pub dominant_secondary_name: Option, - pub dominant_name_pair_count: usize, - #[serde(default)] - pub dominant_profile_span: Option, - pub dominant_profile_span_count: usize, - #[serde(default)] - pub dominant_candidate_pattern: - Option, - #[serde(default)] - pub dominant_mode_family: Option, - pub dominant_mode_family_count: usize, - #[serde(default)] - pub mode_family_counts: Vec, - #[serde(default)] - pub name_pair_summaries: - Vec, - #[serde(default)] - pub profile_span_counts: - Vec, - pub rows_with_previous_short_profile_flag_pair: usize, - #[serde(default)] - pub previous_short_profile_flag_pair_counts: - Vec, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { - pub previous_profile_chunk_len_to_next_name_or_end: usize, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { - pub first_flag_byte: u8, - pub first_flag_byte_hex: String, - pub second_flag_byte: u8, - pub second_flag_byte_hex: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { - pub sample_index: usize, - pub name_tag_relative_offset: usize, - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - pub child_count_candidate: u16, - pub child_count_candidate_hex: String, - pub saved_primary_child_byte_candidate: u8, - pub saved_primary_child_byte_candidate_hex: String, - #[serde(default)] - pub previous_profile_chunk_len_to_next_name_or_end: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePatternCorrelation { - pub child_count_candidate: u16, - pub child_count_candidate_hex: String, - pub saved_primary_child_byte_candidate: u8, - pub saved_primary_child_byte_candidate_hex: String, - pub row_count: usize, - pub unique_name_pair_count: usize, - pub unique_profile_span_count: usize, - #[serde(default)] - pub dominant_primary_name: Option, - #[serde(default)] - pub dominant_secondary_name: Option, - pub dominant_name_pair_count: usize, - #[serde(default)] - pub dominant_profile_span: Option, - pub dominant_profile_span_count: usize, - #[serde(default)] - pub dominant_mode_family: Option, - pub dominant_mode_family_count: usize, - #[serde(default)] - pub mode_family_counts: Vec, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - pub mode_family: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { - pub previous_profile_chunk_len_to_next_name_or_end: usize, - pub row_count: usize, - #[serde(default)] - pub dominant_child_count_candidate: Option, - pub dominant_child_count_candidate_count: usize, - #[serde(default)] - pub dominant_saved_primary_child_byte_candidate: Option, - #[serde(default)] - pub dominant_saved_primary_child_byte_candidate_hex: Option, - pub dominant_saved_primary_child_byte_candidate_count: usize, - #[serde(default)] - pub dominant_candidate_pattern: - Option, - #[serde(default)] - pub dominant_mode_family: Option, - pub dominant_mode_family_count: usize, - #[serde(default)] - pub mode_family_counts: Vec, - #[serde(default)] - pub compact_prefix_pattern_summaries: - Vec, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { - pub sample_index: usize, - pub name_tag_relative_offset: usize, - #[serde(default)] - pub primary_name: Option, - #[serde(default)] - pub secondary_name: Option, - pub prefix_leading_dword: u32, - pub prefix_leading_dword_hex: String, - pub prefix_trailing_word: u16, - pub prefix_trailing_word_hex: String, - pub prefix_separator_byte: u8, - pub prefix_separator_byte_hex: String, - pub child_count_candidate: u16, - pub child_count_candidate_hex: String, - pub saved_primary_child_byte_candidate: u8, - pub saved_primary_child_byte_candidate_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSummary { - pub live_entry_directory_row_count: usize, - pub decoded_live_entry_id_count: usize, - pub payload_relative_offset_monotonic: bool, - pub rows_with_payload_pointer_inside_records_span: usize, - pub rows_with_zero_child_count: usize, - pub rows_with_nonzero_child_count: usize, - pub rows_with_first_name_tag_after_prelude: usize, - pub rows_with_first_name_tag_at_offset_3: usize, - #[serde(default)] - pub unique_child_count_values: Vec, - #[serde(default)] - pub unique_first_name_tag_relative_offsets: Vec, - #[serde(default)] - pub dominant_child_count: Option, - pub dominant_child_count_count: usize, - #[serde(default)] - pub dominant_saved_primary_child_byte: Option, - #[serde(default)] - pub dominant_saved_primary_child_byte_hex: Option, - pub dominant_saved_primary_child_byte_count: usize, - #[serde(default)] - pub dominant_first_name_tag_relative_offset: Option, - pub dominant_first_name_tag_relative_offset_count: usize, - #[serde(default)] - pub sample_rows: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSample { - pub sample_index: usize, - pub live_entry_id: u32, - pub payload_relative_offset: u32, - pub payload_relative_offset_hex: String, - pub payload_relative_to_records: usize, - pub child_count: u16, - pub child_count_hex: String, - pub saved_primary_child_byte: u8, - pub saved_primary_child_byte_hex: String, - pub first_payload_dword_hex: String, - #[serde(default)] - pub first_name_tag_relative_offset: Option, - #[serde(default)] - pub first_primary_name: Option, - #[serde(default)] - pub first_secondary_name: Option, - #[serde(default)] - pub first_tertiary_name: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureDynamicSideBufferAlignmentProbe { - pub unique_side_buffer_name_pair_count: usize, - pub unique_triplet_name_pair_count: usize, - pub overlapping_name_pair_count: usize, - pub side_buffer_row_count: usize, - pub side_buffer_rows_with_matching_triplet_name_pair_count: usize, - pub side_buffer_rows_without_matching_triplet_name_pair_count: usize, - pub triplet_name_pairs_without_side_buffer_match_count: usize, - #[serde(default)] - pub matched_name_pair_samples: Vec, - #[serde(default)] - pub unmatched_side_buffer_name_pair_samples: - Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRt3105SaveNameTableProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub semantic_alignment: Vec, - pub header_offset: usize, - pub header_word_0: u32, - pub header_word_0_hex: String, - pub header_word_1: u32, - pub header_word_1_hex: String, - pub header_word_2: u32, - pub header_word_2_hex: String, - pub entry_stride: usize, - pub entry_stride_hex: String, - pub header_prefix_word_count: usize, - pub observed_entry_capacity: usize, - pub observed_entry_count: usize, - pub zero_trailer_entry_count: usize, - pub nonzero_trailer_entry_count: usize, - pub distinct_trailer_words: Vec, - pub distinct_trailer_hex_words: Vec, - pub zero_trailer_entry_names: Vec, - pub entries_offset: usize, - pub entries_end_offset: usize, - pub trailing_footer_hex: String, - pub footer_progress_word_0: u32, - pub footer_progress_word_0_hex: String, - pub footer_progress_word_1: u32, - pub footer_progress_word_1_hex: String, - pub footer_trailing_byte: u8, - pub footer_trailing_byte_hex: String, - pub footer_grounded_alignments: Vec, - pub entries: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRt3105SaveNamedLocomotiveAvailabilityProbe { - pub profile_family: String, - pub source_kind: String, - pub semantic_family: String, - pub semantic_alignment: Vec, - pub entries_offset: usize, - pub entry_stride: usize, - pub entry_stride_hex: String, - pub observed_entry_count: usize, - pub zero_availability_count: usize, - pub zero_availability_names: Vec, - pub entries_end_offset: usize, - pub entries: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRt3105SaveNameTableEntry { - pub index: usize, - pub offset: usize, - pub text: String, - pub availability_dword: u32, - pub availability_dword_hex: String, - pub trailer_word: u32, - pub trailer_word_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSpecialConditionEntry { - pub slot_index: u8, - pub hidden: bool, - pub label_id: u32, - pub help_id: u32, - pub label: String, - pub value: u32, - pub value_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSpecialConditionsProbe { - pub profile_family: String, - pub source_kind: String, - pub table_offset: usize, - pub table_len: usize, - pub enabled_visible_count: usize, - pub enabled_visible_labels: Vec, - pub hidden_sentinel_slot_index: u8, - pub hidden_sentinel_value: u32, - pub hidden_sentinel_value_hex: String, - pub entries: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpAlignedRuntimeRuleBandLane { - pub band_index: usize, - pub absolute_offset: usize, - pub relative_offset: usize, - pub absolute_offset_hex: String, - pub relative_offset_hex: String, - pub lane_kind: String, - pub known_label: Option, - pub value: u32, - pub value_hex: String, - pub probable_f32_le: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpAlignedRuntimeRuleBandProbe { - pub profile_family: String, - pub source_kind: String, - pub band_offset: usize, - pub band_end_offset: usize, - pub band_len: usize, - pub band_len_hex: String, - pub dword_count: usize, - pub known_editor_rule_dword_count: usize, - pub trailing_scalar_index: usize, - pub trailing_scalar_offset: usize, - pub trailing_scalar_offset_hex: String, - pub post_window_overlap_start_index: usize, - pub post_window_overlap_dword_count: usize, - pub post_window_overlap_end_index: usize, - pub post_window_overlap_post_relative_offset_start_hex: String, - pub post_window_overlap_post_relative_offset_end_hex: String, - pub nonzero_post_window_overlap_band_indices: Vec, - pub nonzero_post_window_overlap_post_relative_offset_hexes: Vec, - pub nonzero_lane_count: usize, - pub nonzero_band_indices: Vec, - pub nonzero_relative_offset_hexes: Vec, - pub nonzero_lanes: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPostSpecialConditionsScalarLane { - pub absolute_offset: usize, - pub relative_offset: usize, - pub absolute_offset_hex: String, - pub relative_offset_hex: String, - pub value: u32, - pub value_hex: String, - pub probable_f32_le: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPostTextGroundedFieldObservation { - pub field_name: String, - pub runtime_object_offset: usize, - pub runtime_object_offset_hex: String, - pub file_offset: usize, - pub file_offset_hex: String, - pub field_width_bytes: usize, - pub field_width_bytes_hex: String, - pub raw_hex: String, - pub value_u8: Option, - pub value_u8_hex: Option, - pub value_u32: Option, - pub value_u32_hex: Option, - pub probable_f32_le: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPostTextFloatAlignmentCandidate { - pub grounded_field_name: String, - pub grounded_field_runtime_object_offset: usize, - pub grounded_field_runtime_object_offset_hex: String, - pub grounded_field_file_offset: usize, - pub grounded_field_file_offset_hex: String, - pub candidate_offset: usize, - pub candidate_offset_hex: String, - pub candidate_value: u32, - pub candidate_value_hex: String, - pub probable_f32_le: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPostTextFieldNeighborhoodProbe { - pub profile_family: String, - pub source_kind: String, - pub window_offset: usize, - pub window_end_offset: usize, - pub window_len: usize, - pub window_len_hex: String, - pub grounded_field_observations: Vec, - pub one_byte_early_float_candidates: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLocomotivePolicyFieldObservation { - pub field_name: String, - pub runtime_object_offset: usize, - pub runtime_object_offset_hex: String, - pub file_offset: usize, - pub file_offset_hex: String, - pub field_width_bytes: usize, - pub field_width_bytes_hex: String, - pub raw_hex: String, - pub value_u8: Option, - pub value_u8_hex: Option, - pub value_u32: Option, - pub value_u32_hex: Option, - pub probable_f32_le: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLocomotivePolicyFloatAlignmentCandidate { - pub grounded_field_name: String, - pub grounded_field_runtime_object_offset: usize, - pub grounded_field_runtime_object_offset_hex: String, - pub grounded_field_file_offset: usize, - pub grounded_field_file_offset_hex: String, - pub candidate_offset: usize, - pub candidate_offset_hex: String, - pub candidate_value: u32, - pub candidate_value_hex: String, - pub probable_f32_le: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLocomotivePolicyNeighborhoodProbe { - pub profile_family: String, - pub source_kind: String, - pub window_offset: usize, - pub window_end_offset: usize, - pub window_len: usize, - pub window_len_hex: String, - pub grounded_field_observations: Vec, - pub three_byte_early_float_candidates: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPreRecipeScalarPlateauLane { - pub absolute_offset: usize, - pub relative_offset: usize, - pub absolute_offset_hex: String, - pub relative_offset_hex: String, - pub value: u32, - pub value_hex: String, - pub probable_f32_le: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPreRecipeScalarPlateauProbe { - pub profile_family: String, - pub source_kind: String, - pub window_offset: usize, - pub window_end_offset: usize, - pub window_len: usize, - pub window_len_hex: String, - pub aligned_dword_count: usize, - pub family_signature: String, - pub nonzero_lanes: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRecipeBookSummaryBook { - pub book_index: usize, - pub book_offset: usize, - pub book_offset_hex: String, - pub head_kind: String, - pub head_nonzero_byte_count: usize, - pub head_cdcd_byte_count: usize, - pub head_first_16_hex: String, - pub max_annual_production_offset: usize, - pub max_annual_production_offset_hex: String, - pub max_annual_production_word: u32, - pub max_annual_production_word_hex: String, - pub max_annual_production_probable_f32_le: Option, - pub line_area_offset: usize, - pub line_area_offset_hex: String, - pub line_area_len: usize, - pub line_area_len_hex: String, - pub line_area_kind: String, - pub line_area_nonzero_byte_count: usize, - pub line_area_cdcd_byte_count: usize, - pub line_area_first_16_hex: String, - pub lines: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRecipeBookLineSummary { - pub line_index: usize, - pub line_offset: usize, - pub line_offset_hex: String, - pub line_kind: String, - pub line_signature_kind: String, - pub imports_to_runtime_descriptor: bool, - pub runtime_import_branch_kind: String, - pub line_nonzero_byte_count: usize, - pub line_cdcd_byte_count: usize, - pub line_first_16_hex: String, - pub mode_word_offset: usize, - pub mode_word_offset_hex: String, - pub mode_word: u32, - pub mode_word_hex: String, - pub annual_amount_offset: usize, - pub annual_amount_offset_hex: String, - pub annual_amount_word: u32, - pub annual_amount_word_hex: String, - pub annual_amount_probable_f32_le: Option, - pub supplied_cargo_token_offset: usize, - pub supplied_cargo_token_offset_hex: String, - pub supplied_cargo_token_word: u32, - pub supplied_cargo_token_word_hex: String, - pub supplied_cargo_token_layout_kind: String, - pub supplied_cargo_token_window_hex: String, - pub supplied_cargo_token_window_ascii: String, - pub supplied_cargo_token_active_in_runtime_import: bool, - pub supplied_cargo_token_probable_high16_ascii_stem: Option, - pub demanded_cargo_token_offset: usize, - pub demanded_cargo_token_offset_hex: String, - pub demanded_cargo_token_word: u32, - pub demanded_cargo_token_word_hex: String, - pub demanded_cargo_token_layout_kind: String, - pub demanded_cargo_token_window_hex: String, - pub demanded_cargo_token_window_ascii: String, - pub demanded_cargo_token_active_in_runtime_import: bool, - pub demanded_cargo_token_probable_high16_ascii_stem: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRecipeBookSummaryProbe { - pub profile_family: String, - pub source_kind: String, - pub root_offset: usize, - pub root_offset_hex: String, - pub runtime_object_root_offset: usize, - pub runtime_object_root_offset_hex: String, - pub book_count: usize, - pub book_stride: usize, - pub book_stride_hex: String, - pub max_annual_production_relative_offset: usize, - pub max_annual_production_relative_offset_hex: String, - pub line_area_relative_offset: usize, - pub line_area_relative_offset_hex: String, - pub line_count: usize, - pub line_stride: usize, - pub line_stride_hex: String, - pub books: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPostSpecialConditionsScalarProbe { - pub profile_family: String, - pub source_kind: String, - pub window_offset: usize, - pub window_end_offset: usize, - pub window_len: usize, - pub window_len_hex: String, - pub dword_count: usize, - pub overlap_end_offset: usize, - pub overlap_end_offset_hex: String, - pub overlap_dword_count: usize, - pub overlap_nonzero_dword_count: usize, - pub overlap_nonzero_relative_offset_hexes: Vec, - pub tail_offset: usize, - pub tail_offset_hex: String, - pub tail_len: usize, - pub tail_len_hex: String, - pub tail_dword_count: usize, - pub tail_runtime_object_offset: usize, - pub tail_runtime_object_offset_hex: String, - pub tail_runtime_object_end_offset: usize, - pub tail_runtime_object_end_offset_hex: String, - pub tail_runtime_object_validated_byte_mirror: bool, - pub tail_grounded_live_field_offset: usize, - pub tail_grounded_live_field_offset_hex: String, - pub tail_grounded_live_field_name: String, - pub tail_grounded_live_field_copy_len: usize, - pub tail_grounded_live_field_copy_len_hex: String, - pub tail_grounded_live_field_copy_end_offset: usize, - pub tail_grounded_live_field_copy_end_offset_hex: String, - pub tail_window_cuts_through_grounded_live_field: bool, - pub tail_grounded_live_field_remaining_file_window_offset: usize, - pub tail_grounded_live_field_remaining_file_window_offset_hex: String, - pub tail_grounded_live_field_remaining_file_window_len: usize, - pub tail_grounded_live_field_remaining_file_window_len_hex: String, - pub tail_grounded_live_field_remaining_file_window_nonzero_byte_count: usize, - pub tail_grounded_live_field_remaining_file_window_first_nonzero_offset: Option, - pub tail_grounded_live_field_remaining_file_window_first_nonzero_offset_hex: Option, - pub tail_grounded_live_field_remaining_file_window_last_nonzero_offset: Option, - pub tail_grounded_live_field_remaining_file_window_last_nonzero_offset_hex: Option, - pub tail_next_grounded_dword_field_offset: usize, - pub tail_next_grounded_dword_field_offset_hex: String, - pub tail_next_grounded_dword_field_file_offset: usize, - pub tail_next_grounded_dword_field_file_offset_hex: String, - pub tail_second_grounded_dword_field_offset: usize, - pub tail_second_grounded_dword_field_offset_hex: String, - pub tail_second_grounded_dword_field_file_offset: usize, - pub tail_second_grounded_dword_field_file_offset_hex: String, - pub post_text_field_file_alignment_matches_grounded_dword_fields: bool, - pub tail_nonzero_dword_count: usize, - pub tail_first_nonzero_offset: Option, - pub tail_first_nonzero_offset_hex: Option, - pub tail_last_nonzero_offset: Option, - pub tail_last_nonzero_offset_hex: Option, - pub tail_nonzero_relative_offset_hexes: Vec, - pub nonzero_dword_count: usize, - pub first_nonzero_offset: Option, - pub first_nonzero_offset_hex: Option, - pub last_nonzero_offset: Option, - pub last_nonzero_offset_hex: Option, - pub nonzero_lanes: Vec, - pub evidence: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpClassicRehydrateProfileProbe { - pub profile_family: String, - pub progress_32dc_offset: usize, - pub progress_3714_offset: usize, - pub progress_3715_offset: usize, - pub packed_profile_offset: usize, - pub packed_profile_len: usize, - pub packed_profile_len_hex: String, - pub packed_profile_block: SmpClassicPackedProfileBlock, - pub ascii_runs: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpClassicPackedProfileBlock { - pub relative_len: usize, - pub relative_len_hex: String, - pub leading_word_0: u32, - pub leading_word_0_hex: String, - pub trailing_zero_word_count_after_leading_word: usize, - pub map_path_offset: usize, - pub map_path: Option, - pub display_name_offset: usize, - pub display_name: Option, - pub profile_byte_0x77: u8, - pub profile_byte_0x77_hex: String, - pub profile_byte_0x82: u8, - pub profile_byte_0x82_hex: String, - pub profile_byte_0x97: u8, - pub profile_byte_0x97_hex: String, - pub profile_byte_0xc5: u8, - pub profile_byte_0xc5_hex: String, - pub stable_nonzero_words: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRt3105PackedProfileProbe { - pub profile_family: String, - pub packed_profile_offset: usize, - pub packed_profile_len: usize, - pub packed_profile_len_hex: String, - pub packed_profile_block: SmpRt3105PackedProfileBlock, - pub ascii_runs: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRt3105PackedProfileBlock { - pub relative_len: usize, - pub relative_len_hex: String, - pub leading_word_0: u32, - pub leading_word_0_hex: String, - pub trailing_zero_word_count_after_leading_word: usize, - pub header_flag_word_3: u32, - pub header_flag_word_3_hex: String, - pub map_path_offset: usize, - pub map_path: Option, - pub display_name_offset: usize, - pub display_name: Option, - pub profile_byte_0x77: u8, - pub profile_byte_0x77_hex: String, - pub profile_byte_0x82: u8, - pub profile_byte_0x82_hex: String, - pub profile_byte_0x97: u8, - pub profile_byte_0x97_hex: String, - pub profile_byte_0xc5: u8, - pub profile_byte_0xc5_hex: String, - pub stable_nonzero_words: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveLoadCandidateTableSummary { - pub source_kind: String, - pub semantic_family: String, - pub observed_entry_count: usize, - pub zero_availability_count: usize, - pub zero_availability_names: Vec, - pub footer_progress_hex_words: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveLoadSummary { - pub file_extension_hint: Option, - pub container_profile_family: Option, - pub mechanism_family: String, - pub mechanism_confidence: String, - pub packed_profile_kind: Option, - pub packed_profile_family: Option, - pub packed_profile_offset: Option, - pub packed_profile_len: Option, - pub map_path: Option, - pub display_name: Option, - pub profile_byte_0x77: Option, - pub profile_byte_0x77_hex: Option, - pub profile_byte_0x82: Option, - pub profile_byte_0x82_hex: Option, - pub profile_byte_0x97: Option, - pub profile_byte_0x97_hex: Option, - pub profile_byte_0xc5: Option, - pub profile_byte_0xc5_hex: Option, - pub trailer_family: Option, - pub bridge_family: Option, - pub candidate_table: Option, - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedProfile { - pub profile_kind: String, - pub profile_family: String, - pub packed_profile_offset: usize, - pub packed_profile_len: usize, - pub packed_profile_len_hex: String, - pub leading_word_0: u32, - pub leading_word_0_hex: String, - pub header_flag_word_3: Option, - pub header_flag_word_3_hex: Option, - pub map_path: Option, - pub display_name: Option, - pub profile_byte_0x77: u8, - pub profile_byte_0x77_hex: String, - pub profile_byte_0x82: u8, - pub profile_byte_0x82_hex: String, - pub profile_byte_0x97: u8, - pub profile_byte_0x97_hex: String, - pub profile_byte_0xc5: u8, - pub profile_byte_0xc5_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedCandidateAvailabilityTable { - pub source_kind: String, - pub semantic_family: String, - pub header_offset: usize, - pub entries_offset: usize, - pub entries_end_offset: usize, - pub observed_entry_count: usize, - pub zero_availability_count: usize, - pub zero_availability_names: Vec, - pub footer_progress_hex_words: Vec, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedNamedLocomotiveAvailabilityTable { - pub source_kind: String, - pub semantic_family: String, - #[serde(default)] - pub header_offset: Option, - #[serde(default)] - pub entries_offset: Option, - #[serde(default)] - pub entries_end_offset: Option, - pub observed_entry_count: usize, - pub zero_availability_count: usize, - pub zero_availability_names: Vec, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedLocomotiveCatalogEntry { - pub locomotive_id: u32, - pub name: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedLocomotiveCatalog { - pub source_kind: String, - pub semantic_family: String, - #[serde(default)] - pub entries_offset: Option, - pub observed_entry_count: usize, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedCargoCatalogEntry { - pub slot_id: u32, - pub label: String, - #[serde(default)] - pub cargo_class: RuntimeCargoClass, - pub book_index: usize, - pub max_annual_production_word: u32, - pub mode_word: u32, - pub runtime_import_branch_kind: String, - pub annual_amount_word: u32, - pub supplied_cargo_token_word: u32, - #[serde(default)] - pub supplied_cargo_token_probable_high16_ascii_stem: Option, - pub demanded_cargo_token_word: u32, - #[serde(default)] - pub demanded_cargo_token_probable_high16_ascii_stem: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedCargoCatalog { - pub source_kind: String, - pub semantic_family: String, - #[serde(default)] - pub root_offset: Option, - pub observed_entry_count: usize, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedWorldIssue37State { - pub source_kind: String, - pub semantic_family: String, - pub issue_value: u32, - pub issue_value_hex: String, - pub issue_38_value: u32, - pub issue_38_value_hex: String, - pub issue_39_value: u32, - pub issue_39_value_hex: String, - pub issue_3a_value: u32, - pub issue_3a_value_hex: String, - pub multiplier_raw_u32: u32, - pub multiplier_raw_hex: String, - pub multiplier_value_f32_text: String, - #[serde(default)] - pub issue_opinion_base_terms_raw_i32: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedWorldEconomicTuningState { - pub source_kind: String, - pub semantic_family: String, - pub mirror_raw_u32: u32, - pub mirror_raw_hex: String, - pub mirror_value_f32_text: String, - pub lane_raw_u32: Vec, - pub lane_raw_hex: Vec, - pub lane_value_f32_text: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedWorldFinanceNeighborhoodState { - pub source_kind: String, - pub semantic_family: String, - pub packed_year_word_raw_u16: u16, - pub packed_year_word_raw_hex: String, - pub partial_year_progress_raw_u8: u8, - pub partial_year_progress_raw_hex: String, - pub current_calendar_tuple_word_raw_u32: u32, - pub current_calendar_tuple_word_raw_hex: String, - pub current_calendar_tuple_word_2_raw_u32: u32, - pub current_calendar_tuple_word_2_raw_hex: String, - pub absolute_counter_raw_u32: u32, - pub absolute_counter_raw_hex: String, - pub absolute_counter_mirror_raw_u32: u32, - pub absolute_counter_mirror_raw_hex: String, - pub stock_policy_raw_u8: u8, - pub stock_policy_raw_hex: String, - pub bond_policy_raw_u8: u8, - pub bond_policy_raw_hex: String, - pub bankruptcy_policy_raw_u8: u8, - pub bankruptcy_policy_raw_hex: String, - pub dividend_policy_raw_u8: u8, - pub dividend_policy_raw_hex: String, - pub building_density_growth_setting_raw_u32: u32, - pub building_density_growth_setting_raw_hex: String, - pub labels: Vec, - pub relative_offsets: Vec, - pub relative_offset_hex: Vec, - pub raw_u32: Vec, - pub raw_hex: Vec, - pub value_i32: Vec, - pub value_f32_text: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedWorldLocomotivePolicyState { - pub source_kind: String, - pub semantic_family: String, - #[serde(default)] - pub selected_year_gap_scalar_raw_u32: Option, - #[serde(default)] - pub selected_year_gap_scalar_raw_hex: Option, - #[serde(default)] - pub selected_year_gap_scalar_value_f32_text: Option, - #[serde(default)] - pub linked_site_removal_follow_on_gate_raw_u8: Option, - #[serde(default)] - pub linked_site_removal_follow_on_gate_raw_hex: Option, - #[serde(default)] - pub auto_show_grade_during_track_lay_raw_u8: Option, - #[serde(default)] - pub auto_show_grade_during_track_lay_raw_hex: Option, - #[serde(default)] - pub starting_building_density_level_raw_u8: Option, - #[serde(default)] - pub starting_building_density_level_raw_hex: Option, - #[serde(default)] - pub building_density_growth_raw_u8: Option, - #[serde(default)] - pub building_density_growth_raw_hex: Option, - #[serde(default)] - pub leftover_simulation_time_accumulator_raw_u32: Option, - #[serde(default)] - pub leftover_simulation_time_accumulator_raw_hex: Option, - #[serde(default)] - pub leftover_simulation_time_accumulator_value_f32_text: Option, - #[serde(default)] - pub selected_year_lane_snapshot_raw_u8: Option, - #[serde(default)] - pub selected_year_lane_snapshot_raw_hex: Option, - #[serde(default)] - pub all_steam_locomotives_available_raw_u8: Option, - #[serde(default)] - pub all_steam_locomotives_available_raw_hex: Option, - #[serde(default)] - pub all_diesel_locomotives_available_raw_u8: Option, - #[serde(default)] - pub all_diesel_locomotives_available_raw_hex: Option, - #[serde(default)] - pub all_electric_locomotives_available_raw_u8: Option, - #[serde(default)] - pub all_electric_locomotives_available_raw_hex: Option, - #[serde(default)] - pub cached_available_locomotive_rating_raw_u32: Option, - #[serde(default)] - pub cached_available_locomotive_rating_raw_hex: Option, - #[serde(default)] - pub cached_available_locomotive_rating_value_f32_text: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedCompanyRosterEntry { - pub company_id: u32, - pub active: bool, - #[serde(default)] - pub controller_kind: RuntimeCompanyControllerKind, - pub current_cash: i64, - pub debt: u64, - #[serde(default)] - pub credit_rating_score: Option, - #[serde(default)] - pub prime_rate: Option, - #[serde(default)] - pub available_track_laying_capacity: Option, - #[serde(default)] - pub track_piece_counts: RuntimeTrackPieceCounts, - #[serde(default)] - pub linked_chairman_profile_id: Option, - #[serde(default)] - pub book_value_per_share: i64, - #[serde(default)] - pub investor_confidence: i64, - #[serde(default)] - pub management_attitude: i64, - #[serde(default)] - pub takeover_cooldown_year: Option, - #[serde(default)] - pub merger_cooldown_year: Option, - #[serde(default)] - pub preferred_locomotive_engine_type_raw_u8: Option, - #[serde(default)] - pub market_state: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedCompanyRoster { - pub source_kind: String, - pub semantic_family: String, - pub observed_entry_count: usize, - #[serde(default)] - pub selected_company_id: Option, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedChairmanProfileEntry { - pub profile_id: u32, - pub name: String, - pub active: bool, - #[serde(default)] - pub current_cash: i64, - #[serde(default)] - pub linked_company_id: Option, - #[serde(default)] - pub company_holdings: BTreeMap, - #[serde(default)] - pub holdings_value_total: i64, - #[serde(default)] - pub net_worth_total: i64, - #[serde(default)] - pub purchasing_power_total: i64, - #[serde(default)] - pub personality_byte_0x291: Option, - #[serde(default)] - pub issue_opinion_terms_raw_i32: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedChairmanProfileTable { - pub source_kind: String, - pub semantic_family: String, - pub observed_entry_count: usize, - #[serde(default)] - pub selected_chairman_profile_id: Option, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveScalarCandidate { - pub relative_offset: usize, - pub relative_offset_hex: String, - pub raw_u64: u64, - pub raw_u64_hex: String, - pub value_i64: i64, - pub value_f64: f64, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveDwordCandidate { - pub label: String, - pub relative_offset: usize, - pub relative_offset_hex: String, - pub raw_u32: u32, - pub raw_u32_hex: String, - pub value_i32: i32, - pub value_f32: f32, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveWorldSelectionRoleAnalysisEntry { - pub slot_index: usize, - pub selector_byte: u8, - pub selector_byte_hex: String, - pub role_gate_byte: u8, - pub role_gate_byte_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveWorldSelectionRoleAnalysis { - pub selected_company_id: u32, - pub selected_chairman_profile_id: u32, - pub campaign_override_flag: u8, - pub campaign_override_flag_hex: String, - pub chairman_slots: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveCompanyRecordAnalysisEntry { - pub company_id: u32, - pub name: String, - pub active: bool, - #[serde(default)] - pub linked_chairman_profile_id: Option, - pub outstanding_shares: u32, - pub debt: u64, - pub bond_count: u8, - #[serde(default)] - pub live_bond_slots: Vec, - #[serde(default)] - pub largest_live_bond_principal: Option, - #[serde(default)] - pub highest_coupon_live_bond_principal: Option, - #[serde(default)] - pub available_track_laying_capacity: Option, - pub company_value_scalar_f32: f32, - pub cached_share_support_scalar_f32: f32, - pub cached_share_price_f32: f32, - pub chairman_salary_baseline: u32, - pub chairman_salary_current: u32, - pub chairman_bonus_year: u32, - pub chairman_bonus_amount: i32, - pub founding_year: u32, - pub last_bankruptcy_year: u32, - pub last_dividend_year: u32, - pub preferred_locomotive_engine_type_raw_u8: u8, - pub preferred_locomotive_engine_type_raw_hex: String, - pub city_connection_latch: bool, - pub linked_transit_latch: bool, - pub linked_transit_autoroute_site_score_cache_refresh_absolute_counter: u32, - pub linked_transit_site_peer_cache_refresh_absolute_counter: u32, - #[serde(default)] - pub linked_transit_route_anchor_entry_id: Option, - #[serde(default)] - pub linked_transit_route_anchor_fallback_counts: Vec, - pub merger_cooldown_year: u32, - pub takeover_cooldown_year: u32, - #[serde(default)] - pub scalar_dword_candidates: Vec, - #[serde(default)] - pub post_capacity_dword_candidates: Vec, - #[serde(default)] - pub stat_band_root_0cfb_candidates: Vec, - #[serde(default)] - pub stat_band_root_0d7f_candidates: Vec, - #[serde(default)] - pub stat_band_root_1c47_candidates: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveChairmanRecordAnalysisEntry { - pub profile_id: u32, - pub name: String, - pub active: bool, - pub current_cash: f64, - #[serde(default)] - pub linked_company_id: Option, - #[serde(default)] - pub holdings_by_company: BTreeMap, - #[serde(default)] - pub derived_holdings_share_price_total: Option, - #[serde(default)] - pub derived_net_worth_share_price_total: Option, - #[serde(default)] - pub derived_cached_purchasing_power_total: Option, - pub personality_byte_0x291: u8, - pub personality_byte_0x291_hex: String, - #[serde(default)] - pub cached_scalar_candidates: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpSaveCompanyChairmanAnalysisReport { - pub profile_family: String, - #[serde(default)] - pub selected_company_id: Option, - #[serde(default)] - pub selected_chairman_profile_id: Option, - #[serde(default)] - pub world_selection_context: Option, - #[serde(default)] - pub world_issue_37: Option, - #[serde(default)] - pub world_economic_tuning: Option, - #[serde(default)] - pub world_finance_neighborhood: Option, - #[serde(default)] - pub train_collection_header: Option, - #[serde(default)] - pub train_collection_directory: Option, - #[serde(default)] - pub region_collection_header: Option, - #[serde(default)] - pub region_record_triplets: Option, - #[serde(default)] - pub region_queued_notice_records: Option, - #[serde(default)] - pub region_fixed_row_run_candidates: Option, - #[serde(default)] - pub placed_structure_collection_header: Option, - #[serde(default)] - pub placed_structure_record_triplets: Option, - #[serde(default)] - pub placed_structure_dynamic_side_buffer: Option, - #[serde(default)] - pub placed_structure_dynamic_side_buffer_alignment: - Option, - #[serde(default)] - pub unclassified_tagged_collection_headers: Vec, - #[serde(default)] - pub company_entries: Vec, - #[serde(default)] - pub chairman_entries: Vec, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpServiceTraceBranchStatus { - pub branch_name: String, - pub status: String, - #[serde(default)] - pub grounded_inputs: Vec, - #[serde(default)] - pub blocking_inputs: Vec, - #[serde(default)] - pub candidate_consumers: Vec, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPeriodicCompanyServiceTraceEntry { - pub company_id: u32, - pub name: String, - pub active: bool, - #[serde(default)] - pub linked_chairman_profile_id: Option, - pub preferred_locomotive_engine_type_raw_u8: u8, - pub city_connection_latch: bool, - pub linked_transit_latch: bool, - pub linked_transit_autoroute_site_score_cache_refresh_absolute_counter: u32, - pub linked_transit_site_peer_cache_refresh_absolute_counter: u32, - #[serde(default)] - pub branches: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureProfilePayloadSummaryEntry { - pub profile_payload_dword_hex: String, - pub profile_status_kind: String, - pub count: usize, - #[serde(default)] - pub sample_primary_names: Vec, - #[serde(default)] - pub sample_secondary_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureProfilePayloadDeltaSummaryEntry { - pub delta_hex: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureProfileFooterPaddingSummaryEntry { - pub padding_len: usize, - pub count: usize, - #[serde(default)] - pub sample_hex_bytes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureProfileCompanionByteSummaryEntry { - pub companion_byte_hex: String, - pub count: usize, - #[serde(default)] - pub sample_primary_names: Vec, - #[serde(default)] - pub sample_secondary_names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { - pub companion_byte_hex: String, - pub primary_name: String, - pub secondary_name: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureNonzeroCompanionBuildingFamilyOverlapSummaryEntry { - pub companion_byte_hex: String, - pub primary_name: String, - pub secondary_name: String, - pub count: usize, - pub primary_matches_nonzero_stock_building_header_family: bool, - pub secondary_matches_nonzero_stock_building_header_family: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructureNonzeroCompanionBuildingFamilyResidueSummaryEntry { - pub companion_byte_hex: String, - pub primary_name: String, - pub secondary_name: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSavePlacedStructurePolicyTrailingWordSummaryEntry { - pub policy_trailing_word_hex: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPeriodicCompanyServiceTraceReport { - pub profile_family: String, - #[serde(default)] - pub selected_company_id: Option, - #[serde(default)] - pub world_issue_37_present: bool, - #[serde(default)] - pub world_finance_neighborhood_present: bool, - #[serde(default)] - pub region_record_body_present: bool, - #[serde(default)] - pub placed_structure_record_body_present: bool, - #[serde(default)] - pub infrastructure_asset_side_buffer_present: bool, - pub peer_site_selector_candidate_owner_strip: String, - pub peer_site_selector_candidate_persisted_tag_hex: String, - pub peer_site_selector_candidate_selector_lane: String, - pub peer_site_selector_candidate_secondary_payload_lane: String, - pub peer_site_selector_candidate_post_secondary_byte_status: String, - pub peer_site_selector_candidate_class_identity_status: String, - #[serde(default)] - pub peer_site_selector_candidate_helper_linkage: Vec, - #[serde(default)] - pub peer_site_selector_candidate_saved_payload_summaries: - Vec, - #[serde(default)] - pub peer_site_selector_candidate_saved_payload_delta_summaries: - Vec, - #[serde(default)] - pub peer_site_selector_candidate_saved_footer_padding_summaries: - Vec, - #[serde(default)] - pub peer_site_selector_candidate_saved_companion_byte_summaries: - Vec, - #[serde(default)] - pub peer_site_selector_candidate_saved_policy_trailing_word_summaries: - Vec, - #[serde(default)] - pub peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries: - Vec, - #[serde(default)] - pub peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries: - Vec, - #[serde(default)] - pub peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries: - Vec, - #[serde(default)] - pub peer_site_persisted_selector_bundle_fields: Vec, - #[serde(default)] - pub peer_site_rebuilt_transient_followon_fields: Vec, - pub peer_site_shellless_minimum_persisted_identity_status: String, - #[serde(default)] - pub peer_site_shellless_minimum_persisted_identity_inputs: Vec, - #[serde(default)] - pub peer_site_restore_input_fields: Vec, - #[serde(default)] - pub peer_site_runtime_input_fields: Vec, - pub peer_site_runtime_reconstruction_status: String, - #[serde(default)] - pub peer_site_runtime_reconstruction_steps: Vec, - #[serde(default)] - pub near_city_acquisition_region_input_fields: Vec, - #[serde(default)] - pub near_city_acquisition_peer_input_fields: Vec, - #[serde(default)] - pub near_city_acquisition_company_input_fields: Vec, - pub near_city_acquisition_shellless_readiness_status: String, - #[serde(default)] - pub near_city_acquisition_runtime_backed_input_families: Vec, - pub near_city_acquisition_site_owner_company_projection_status: String, - pub near_city_acquisition_site_self_id_projection_status: String, - pub near_city_acquisition_site_cached_tri_lane_projection_status: String, - pub near_city_acquisition_tri_lane_live_service_status: String, - pub near_city_acquisition_candidate_subtype_projection_status: String, - pub near_city_acquisition_backing_record_projection_status: String, - pub near_city_acquisition_nontransport_persisted_source_status: String, - #[serde(default)] - pub near_city_acquisition_nontransport_persisted_source_candidates: Vec, - pub near_city_acquisition_tri_lane_save_shape_family_status: String, - #[serde(default)] - pub near_city_acquisition_tri_lane_save_shape_family_candidates: - Vec, - #[serde(default)] - pub near_city_acquisition_tri_lane_live_owner_families: Vec, - #[serde(default)] - pub near_city_acquisition_tri_lane_candidate_gate_fields: Vec, - #[serde(default)] - pub near_city_acquisition_tri_lane_runtime_writer_roles: Vec, - #[serde(default)] - pub near_city_acquisition_tri_lane_direct_caller_families: Vec, - #[serde(default)] - pub near_city_acquisition_tri_lane_formula_input_lanes: Vec, - #[serde(default)] - pub near_city_acquisition_projection_hypotheses: Vec, - #[serde(default)] - pub near_city_acquisition_remaining_owner_gaps: Vec, - #[serde(default)] - pub near_city_acquisition_region_lane_statuses: Vec, - #[serde(default)] - pub atlas_candidate_consumers: Vec, - #[serde(default)] - pub known_bridge_helpers: Vec, - #[serde(default)] - pub next_owner_questions: Vec, - pub linked_transit_shellless_readiness_status: String, - #[serde(default)] - pub linked_transit_minimum_persisted_identity_inputs: Vec, - #[serde(default)] - pub linked_transit_live_rebuilt_cache_lanes: Vec, - #[serde(default)] - pub linked_transit_runtime_backed_input_families: Vec, - #[serde(default)] - pub linked_transit_remaining_owner_gaps: Vec, - #[serde(default)] - pub companies: Vec, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPeriodicTriLaneSaveShapeFamilyCandidateSummaryEntry { - pub rank: usize, - pub shape_family_signature: String, - pub rows_offset_hex: String, - pub row_count: usize, - pub row_stride_hex: String, - #[serde(default)] - pub best_probable_density_lane_relative_offset_hex: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRegionServiceTraceEntry { - pub name: String, - #[serde(default)] - pub profile_collection_count: Option, - pub policy_leading_f32_0_text: String, - pub policy_leading_f32_1_text: String, - pub policy_leading_f32_2_text: String, - #[serde(default)] - pub policy_reserved_dword_hex_words: Vec, - pub policy_trailing_word_hex: String, - #[serde(default)] - pub branches: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpRegionServiceTraceReport { - pub profile_family: String, - #[serde(default)] - pub region_collection_header_present: bool, - #[serde(default)] - pub region_record_triplet_count: usize, - #[serde(default)] - pub queued_notice_record_count: usize, - #[serde(default)] - pub atlas_candidate_consumers: Vec, - #[serde(default)] - pub known_owner_bridge_fields: Vec, - #[serde(default)] - pub known_bridge_helpers: Vec, - #[serde(default)] - pub next_owner_questions: Vec, - #[serde(default)] - pub candidate_consumer_hypotheses: Vec, - #[serde(default)] - pub entries: Vec, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpInfrastructureAssetTraceReport { - pub profile_family: String, - #[serde(default)] - pub placed_structure_collection_header_present: bool, - #[serde(default)] - pub placed_structure_record_triplet_count: usize, - #[serde(default)] - pub side_buffer_present: bool, - #[serde(default)] - pub side_buffer_decoded_embedded_name_row_count: usize, - #[serde(default)] - pub side_buffer_unique_name_pair_count: usize, - #[serde(default)] - pub bridge_like_name_pair_count: usize, - #[serde(default)] - pub tunnel_like_name_pair_count: usize, - #[serde(default)] - pub track_cap_like_name_pair_count: usize, - #[serde(default)] - pub triplet_alignment_overlap_count: usize, - #[serde(default)] - pub atlas_candidate_consumers: Vec, - #[serde(default)] - pub known_owner_bridge_fields: Vec, - #[serde(default)] - pub known_bridge_helpers: Vec, - #[serde(default)] - pub next_owner_questions: Vec, - #[serde(default)] - pub candidate_consumer_hypotheses: Vec, - #[serde(default)] - pub branches: Vec, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpServiceConsumerHypothesis { - pub label: String, - pub status: String, - #[serde(default)] - pub candidate_consumers: Vec, - #[serde(default)] - pub evidence: Vec, - #[serde(default)] - pub blockers: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedSpecialConditionsTable { - pub source_kind: String, - pub table_offset: usize, - pub table_len: usize, - pub enabled_visible_count: usize, - pub enabled_visible_labels: Vec, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedEventRuntimeCollectionSummary { - pub source_kind: String, - pub mechanism_family: String, - pub mechanism_confidence: String, - #[serde(default)] - pub container_profile_family: Option, - pub metadata_tag_offset: usize, - pub records_tag_offset: usize, - pub close_tag_offset: usize, - pub packed_state_version: u32, - pub packed_state_version_hex: String, - pub live_id_bound: u32, - pub live_record_count: usize, - pub live_entry_ids: Vec, - #[serde(default)] - pub decoded_record_count: usize, - #[serde(default)] - pub imported_runtime_record_count: usize, - #[serde(default)] - pub records_with_trigger_kind: usize, - #[serde(default)] - pub records_missing_trigger_kind: usize, - #[serde(default)] - pub nondirect_compact_record_count: usize, - #[serde(default)] - pub nondirect_compact_records_missing_trigger_kind: usize, - #[serde(default)] - pub trigger_kinds_present: Vec, - #[serde(default)] - pub add_building_dispatch_strip_record_indexes: Vec, - #[serde(default)] - pub add_building_dispatch_strip_descriptor_labels: Vec, - #[serde(default)] - pub add_building_dispatch_strip_records_with_trigger_kind: usize, - #[serde(default)] - pub add_building_dispatch_strip_records_missing_trigger_kind: usize, - #[serde(default)] - pub add_building_dispatch_strip_row_shape_families: Vec, - #[serde(default)] - pub add_building_dispatch_strip_signature_families: Vec, - #[serde(default)] - pub add_building_dispatch_strip_condition_tuple_families: Vec, - #[serde(default)] - pub add_building_dispatch_strip_signature_condition_clusters: Vec, - #[serde(default)] - pub control_lane_notes: Vec, - #[serde(default)] - pub records: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPackedEventRecordSummary { - pub record_index: usize, - pub live_entry_id: u32, - #[serde(default)] - pub payload_offset: Option, - #[serde(default)] - pub payload_len: Option, - pub decode_status: String, - #[serde(default)] - pub payload_family: String, - #[serde(default)] - pub trigger_kind: Option, - #[serde(default)] - pub active: Option, - #[serde(default)] - pub marks_collection_dirty: Option, - #[serde(default)] - pub one_shot: Option, - #[serde(default)] - pub compact_control: Option, - #[serde(default)] - pub text_bands: Vec, - #[serde(default)] - pub standalone_condition_row_count: usize, - #[serde(default)] - pub standalone_condition_rows: Vec, - #[serde(default)] - pub negative_sentinel_scope: Option, - #[serde(default)] - pub grouped_effect_row_counts: Vec, - #[serde(default)] - pub grouped_effect_rows: Vec, - #[serde(default)] - pub decoded_conditions: Vec, - #[serde(default)] - pub decoded_actions: Vec, - #[serde(default)] - pub executable_import_ready: bool, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPackedEventNegativeSentinelScopeSummary { - pub company_test_scope: RuntimeCompanyConditionTestScope, - pub player_test_scope: RuntimePlayerConditionTestScope, - pub territory_scope_selector_is_0x63: bool, - #[serde(default)] - pub source_row_indexes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPackedEventCompactControlSummary { - pub mode_byte_0x7ef: u8, - pub primary_selector_0x7f0: u32, - pub grouped_mode_0x7f4: u8, - pub one_shot_header_0x7f5: u32, - pub modifier_flag_0x7f9: u8, - pub modifier_flag_0x7fa: u8, - pub grouped_target_scope_ordinals_0x7fb: Vec, - pub grouped_scope_checkboxes_0x7ff: Vec, - pub summary_toggle_0x800: u8, - pub grouped_territory_selectors_0x80f: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPackedEventTextBandSummary { - pub label: String, - pub packed_len: usize, - pub present: bool, - pub preview: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPackedEventConditionRowSummary { - pub row_index: usize, - pub raw_condition_id: i32, - pub subtype: u8, - #[serde(default)] - pub flag_bytes: Vec, - #[serde(default)] - pub candidate_name: Option, - #[serde(default)] - pub comparator: Option, - #[serde(default)] - pub metric: Option, - #[serde(default)] - pub semantic_family: Option, - #[serde(default)] - pub semantic_preview: Option, - #[serde(default)] - pub recovered_cargo_slot: Option, - #[serde(default)] - pub recovered_cargo_class: Option, - #[serde(default)] - pub requires_candidate_name_binding: bool, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpLoadedPackedEventGroupedEffectRowSummary { - pub group_index: usize, - pub row_index: usize, - pub descriptor_id: u32, - #[serde(default)] - pub descriptor_label: Option, - #[serde(default)] - pub target_mask_bits: Option, - #[serde(default)] - pub parameter_family: Option, - #[serde(default)] - pub grouped_target_subject: Option, - #[serde(default)] - pub grouped_target_scope: Option, - pub opcode: u8, - pub raw_scalar_value: i32, - pub value_byte_0x09: u8, - pub value_dword_0x0d: u32, - pub value_byte_0x11: u8, - pub value_byte_0x12: u8, - pub value_word_0x14: u16, - pub value_word_0x16: u16, - pub row_shape: String, - #[serde(default)] - pub semantic_family: Option, - #[serde(default)] - pub semantic_preview: Option, - #[serde(default)] - pub recovered_cargo_slot: Option, - #[serde(default)] - pub recovered_cargo_class: Option, - #[serde(default)] - pub recovered_cargo_label: Option, - #[serde(default)] - pub recovered_locomotive_id: Option, - #[serde(default)] - pub locomotive_name: Option, - #[serde(default)] - pub notes: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RealGroupedTargetSubject { - Company, - Player, - Chairman, - Territory, - WholeGame, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpLoadedSaveSlice { - pub file_extension_hint: Option, - pub container_profile_family: Option, - pub mechanism_family: String, - pub mechanism_confidence: String, - pub trailer_family: Option, - pub bridge_family: Option, - pub profile: Option, - pub candidate_availability_table: Option, - pub named_locomotive_availability_table: Option, - #[serde(default)] - pub locomotive_catalog: Option, - #[serde(default)] - pub cargo_catalog: Option, - #[serde(default)] - pub world_issue_37_state: Option, - #[serde(default)] - pub world_economic_tuning_state: Option, - #[serde(default)] - pub world_finance_neighborhood_state: Option, - #[serde(default)] - pub world_locomotive_policy_state: Option, - #[serde(default)] - pub company_roster: Option, - #[serde(default)] - pub chairman_profile_table: Option, - #[serde(default)] - pub region_collection: Option, - #[serde(default)] - pub region_fixed_row_run_summary: Option, - #[serde(default)] - pub placed_structure_collection: Option, - #[serde(default)] - pub placed_structure_dynamic_side_buffer_summary: - Option, - pub special_conditions_table: Option, - pub event_runtime_collection: Option, - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpPackedProfileWordLane { - pub relative_offset: usize, - pub relative_offset_hex: String, - pub value: u32, - pub value_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SmpInspectionReport { - pub inspection_mode: String, - pub file_extension_hint: Option, - pub file_size: usize, - pub sha256: String, - pub preamble: SmpPreamble, - pub shared_header: Option, - pub header_variant_probe: Option, - pub first_ascii_run: Option, - pub early_content_probe: Option, - pub secondary_variant_probe: Option, - pub container_profile: Option, - pub save_bootstrap_block: Option, - pub save_anchor_run_block: Option, - pub runtime_anchor_cycle_block: Option, - pub runtime_trailer_block: Option, - pub runtime_post_span_probe: Option, - pub rt3_105_post_span_bridge_probe: Option, - pub rt3_105_save_bridge_payload_probe: Option, - pub save_world_selection_context_probe: Option, - pub save_world_issue_37_probe: Option, - pub save_world_economic_tuning_probe: Option, - pub save_world_finance_neighborhood_probe: Option, - pub save_company_collection_header_probe: Option, - pub save_chairman_profile_collection_header_probe: Option, - pub save_train_collection_header_probe: Option, - pub save_train_collection_directory_probe: Option, - pub save_region_collection_header_probe: Option, - pub save_region_record_triplet_probe: Option, - #[serde(default)] - pub save_region_queued_notice_record_probe: Option, - #[serde(default)] - pub save_region_fixed_row_run_candidate_probe: Option, - pub save_placed_structure_collection_header_probe: Option, - pub save_placed_structure_record_triplet_probe: - Option, - #[serde(default)] - pub save_placed_structure_dynamic_side_buffer_probe: - Option, - #[serde(default)] - pub save_unclassified_tagged_collection_header_probes: - Vec, - #[serde(default)] - pub save_company_roster_probe: Option, - #[serde(default)] - pub save_chairman_profile_table_probe: Option, - #[serde(default)] - pub map_title_hint_probe: Option, - pub rt3_105_save_name_table_probe: Option, - pub rt3_105_save_named_locomotive_availability_probe: - Option, - pub special_conditions_probe: Option, - pub smp_aligned_runtime_rule_band_probe: Option, - pub post_special_conditions_scalar_probe: Option, - pub post_text_field_neighborhood_probe: Option, - pub locomotive_policy_neighborhood_probe: Option, - pub pre_recipe_scalar_plateau_probe: Option, - pub recipe_book_summary_probe: Option, - pub classic_rehydrate_profile_probe: Option, - pub rt3_105_packed_profile_probe: Option, - pub save_load_summary: Option, - pub event_runtime_collection_summary: Option, - pub contains_grounded_runtime_tags: bool, - pub known_tag_hits: Vec, - pub notes: Vec, - pub warnings: Vec, -} - -pub fn inspect_smp_file(path: &Path) -> Result> { - let bytes = fs::read(path)?; - Ok(inspect_bundle_bytes( - &bytes, - path.extension() - .and_then(|extension| extension.to_str()) - .map(|extension| extension.to_ascii_lowercase()), - )) -} - -pub fn inspect_map_title_hint_file( - path: &Path, -) -> Result, Box> { - let bytes = fs::read(path)?; - let file_extension_hint = path - .extension() - .and_then(|extension| extension.to_str()) - .map(|extension| extension.to_ascii_lowercase()); - Ok(inspect_map_title_hint_bytes( - &bytes, - file_extension_hint.as_deref(), - )) -} - -pub fn inspect_map_title_hint_bytes( - bytes: &[u8], - file_extension_hint: Option<&str>, -) -> Option { - let shared_header = parse_shared_header(bytes); - let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); - let first_ascii_run = find_first_ascii_run(bytes); - let early_content_probe = first_ascii_run - .as_ref() - .and_then(|ascii_run| probe_early_content_layout(bytes, ascii_run)); - let secondary_variant_probe = early_content_probe - .as_ref() - .and_then(classify_secondary_variant_probe); - let container_profile = classify_container_profile( - file_extension_hint, - header_variant_probe.as_ref(), - secondary_variant_probe.as_ref(), - ); - parse_map_title_hint_probe(bytes, file_extension_hint, container_profile.as_ref()) -} - -pub fn inspect_unclassified_save_collection_headers_file( - path: &Path, -) -> Result, Box> { - let bytes = fs::read(path)?; - let file_extension_hint = path - .extension() - .and_then(|extension| extension.to_str()) - .map(|extension| extension.to_ascii_lowercase()); - let shared_header = parse_shared_header(&bytes); - let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); - let first_ascii_run = find_first_ascii_run(&bytes); - let early_content_probe = first_ascii_run - .as_ref() - .and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run)); - let secondary_variant_probe = early_content_probe - .as_ref() - .and_then(classify_secondary_variant_probe); - let container_profile = classify_container_profile( - file_extension_hint.as_deref(), - header_variant_probe.as_ref(), - secondary_variant_probe.as_ref(), - ); - let save_company_collection_header_probe = parse_save_company_collection_header_probe( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_chairman_profile_collection_header_probe = - parse_save_chairman_profile_collection_header_probe( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_train_collection_header_probe = parse_save_train_collection_header_probe( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_region_collection_header_probe = parse_save_region_collection_header_probe( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_placed_structure_collection_header_probe = - parse_save_placed_structure_collection_header_probe( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let known_header_probes = [ - save_company_collection_header_probe.as_ref(), - save_chairman_profile_collection_header_probe.as_ref(), - save_train_collection_header_probe.as_ref(), - save_region_collection_header_probe.as_ref(), - save_placed_structure_collection_header_probe.as_ref(), - ]; - let probes = scan_save_unclassified_tagged_collection_header_probes( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - Ok( - filter_unclassified_tagged_collection_header_probes_outside_known_spans( - probes, - &known_header_probes, - ), - ) -} - -pub fn inspect_save_placed_structure_dynamic_side_buffer_file( - path: &Path, -) -> Result, Box> { - let bytes = fs::read(path)?; - let file_extension_hint = path - .extension() - .and_then(|extension| extension.to_str()) - .map(|extension| extension.to_ascii_lowercase()); - let shared_header = parse_shared_header(&bytes); - let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); - let first_ascii_run = find_first_ascii_run(&bytes); - let early_content_probe = first_ascii_run - .as_ref() - .and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run)); - let secondary_variant_probe = early_content_probe - .as_ref() - .and_then(classify_secondary_variant_probe); - let container_profile = classify_container_profile( - file_extension_hint.as_deref(), - header_variant_probe.as_ref(), - secondary_variant_probe.as_ref(), - ); - Ok(parse_save_placed_structure_dynamic_side_buffer_probe( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - )) -} - -pub fn inspect_save_region_queued_notice_records_file( - path: &Path, -) -> Result, Box> { - let bytes = fs::read(path)?; - let file_extension_hint = path - .extension() - .and_then(|extension| extension.to_str()) - .map(|extension| extension.to_ascii_lowercase()); - let shared_header = parse_shared_header(&bytes); - let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); - let first_ascii_run = find_first_ascii_run(&bytes); - let early_content_probe = first_ascii_run - .as_ref() - .and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run)); - let secondary_variant_probe = early_content_probe - .as_ref() - .and_then(classify_secondary_variant_probe); - let container_profile = classify_container_profile( - file_extension_hint.as_deref(), - header_variant_probe.as_ref(), - secondary_variant_probe.as_ref(), - ); - let save_region_collection_header_probe = parse_save_region_collection_header_probe( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - Ok(parse_save_region_queued_notice_record_probe( - &bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - save_region_collection_header_probe.as_ref(), - )) -} - -fn build_service_trace_branch_status( - branch_name: &str, - status: &str, - grounded_inputs: &[&str], - blocking_inputs: &[&str], - candidate_consumers: &[&str], - notes: &[&str], -) -> SmpServiceTraceBranchStatus { - SmpServiceTraceBranchStatus { - branch_name: branch_name.to_string(), - status: status.to_string(), - grounded_inputs: grounded_inputs - .iter() - .map(|value| value.to_string()) - .collect(), - blocking_inputs: blocking_inputs - .iter() - .map(|value| value.to_string()) - .collect(), - candidate_consumers: candidate_consumers - .iter() - .map(|value| value.to_string()) - .collect(), - notes: notes.iter().map(|value| value.to_string()).collect(), - } -} - -pub fn inspect_save_periodic_company_service_trace_file( - path: &Path, -) -> Result> { - let inspection = inspect_smp_file(path)?; - let analysis = inspect_save_company_and_chairman_analysis_file(path)?; - let mut trace = build_periodic_company_service_trace_report(&analysis); - let _ = inspection; - if trace.region_record_body_present || trace.placed_structure_record_body_present { - trace.notes.push( - "The current blockers are no longer collection identity; they are missing higher-layer consumer semantics for the region and infrastructure/placed-structure owner seams.".to_string(), - ); - } - Ok(trace) -} - -fn summarize_peer_site_selector_candidate_saved_payloads( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> Vec { - let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { - return Vec::new(); - }; - let mut grouped = - BTreeMap::<(String, String), (usize, BTreeSet, BTreeSet)>::new(); - for entry in &triplets.entries { - let grouped_entry = grouped - .entry(( - entry.profile_payload_dword_hex.clone(), - entry.profile_status_kind.clone(), - )) - .or_insert_with(|| (0, BTreeSet::new(), BTreeSet::new())); - grouped_entry.0 += 1; - if grouped_entry.1.len() < 4 { - grouped_entry.1.insert(entry.primary_name.clone()); - } - if grouped_entry.2.len() < 4 { - grouped_entry.2.insert(entry.secondary_name.clone()); - } - } - let mut summaries = grouped - .into_iter() - .map( - |( - (profile_payload_dword_hex, profile_status_kind), - (count, primary_names, secondary_names), - )| { - SmpSavePlacedStructureProfilePayloadSummaryEntry { - profile_payload_dword_hex, - profile_status_kind, - count, - sample_primary_names: primary_names.into_iter().collect(), - sample_secondary_names: secondary_names.into_iter().collect(), - } - }, - ) - .collect::>(); - summaries.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| { - left.profile_payload_dword_hex - .cmp(&right.profile_payload_dword_hex) - }) - .then_with(|| left.profile_status_kind.cmp(&right.profile_status_kind)) - }); - summaries.truncate(8); - summaries -} - -fn summarize_peer_site_selector_candidate_saved_payload_deltas( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> Vec { - let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { - return Vec::new(); - }; - let mut payload_values = triplets - .entries - .iter() - .map(|entry| entry.profile_payload_dword) - .collect::>(); - payload_values.sort_unstable(); - payload_values.dedup(); - let mut delta_counts = BTreeMap::::new(); - for window in payload_values.windows(2) { - let delta = window[1].wrapping_sub(window[0]); - *delta_counts.entry(delta).or_insert(0) += 1; - } - let mut summaries = delta_counts - .into_iter() - .map( - |(delta, count)| SmpSavePlacedStructureProfilePayloadDeltaSummaryEntry { - delta_hex: format!("0x{delta:08x}"), - count, - }, - ) - .collect::>(); - summaries.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| left.delta_hex.cmp(&right.delta_hex)) - }); - summaries.truncate(6); - summaries -} - -fn summarize_peer_site_selector_candidate_saved_footer_padding( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> Vec { - let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { - return Vec::new(); - }; - let mut grouped = BTreeMap::)>::new(); - for entry in &triplets.entries { - let grouped_entry = grouped - .entry(entry.profile_pre_footer_padding_len) - .or_insert_with(|| (0, BTreeSet::new())); - grouped_entry.0 += 1; - if grouped_entry.1.len() < 4 { - grouped_entry - .1 - .insert(entry.profile_pre_footer_padding_hex_bytes.join(",")); - } - } - let mut summaries = grouped - .into_iter() - .map(|(padding_len, (count, sample_hex_bytes))| { - SmpSavePlacedStructureProfileFooterPaddingSummaryEntry { - padding_len, - count, - sample_hex_bytes: sample_hex_bytes.into_iter().collect(), - } - }) - .collect::>(); - summaries.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| left.padding_len.cmp(&right.padding_len)) - }); - summaries.truncate(6); - summaries -} - -fn summarize_peer_site_selector_candidate_saved_companion_bytes( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> Vec { - let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { - return Vec::new(); - }; - let mut grouped = BTreeMap::, BTreeSet)>::new(); - for entry in &triplets.entries { - let Some(companion_byte_hex) = entry.profile_companion_byte_hex.as_ref() else { - continue; - }; - let grouped_entry = grouped - .entry(companion_byte_hex.clone()) - .or_insert_with(|| (0, BTreeSet::new(), BTreeSet::new())); - grouped_entry.0 += 1; - if grouped_entry.1.len() < 4 { - grouped_entry.1.insert(entry.primary_name.clone()); - } - if grouped_entry.2.len() < 4 { - grouped_entry.2.insert(entry.secondary_name.clone()); - } - } - let mut summaries = grouped - .into_iter() - .map( - |(companion_byte_hex, (count, primary_names, secondary_names))| { - SmpSavePlacedStructureProfileCompanionByteSummaryEntry { - companion_byte_hex, - count, - sample_primary_names: primary_names.into_iter().collect(), - sample_secondary_names: secondary_names.into_iter().collect(), - } - }, - ) - .collect::>(); - summaries.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| left.companion_byte_hex.cmp(&right.companion_byte_hex)) - }); - summaries.truncate(8); - summaries -} - -fn summarize_peer_site_selector_candidate_saved_policy_trailing_words( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> Vec { - let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { - return Vec::new(); - }; - let mut grouped = BTreeMap::::new(); - for entry in &triplets.entries { - *grouped - .entry(entry.policy_trailing_word_hex.clone()) - .or_insert(0) += 1; - } - let mut summaries = grouped - .into_iter() - .map(|(policy_trailing_word_hex, count)| { - SmpSavePlacedStructurePolicyTrailingWordSummaryEntry { - policy_trailing_word_hex, - count, - } - }) - .collect::>(); - summaries.sort_by(|left, right| { - right.count.cmp(&left.count).then_with(|| { - left.policy_trailing_word_hex - .cmp(&right.policy_trailing_word_hex) - }) - }); - summaries.truncate(8); - summaries -} - -fn summarize_peer_site_selector_candidate_saved_nonzero_companion_name_pairs( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> Vec { - let Some(triplets) = analysis.placed_structure_record_triplets.as_ref() else { - return Vec::new(); - }; - let mut grouped = BTreeMap::<(String, String, String), usize>::new(); - for entry in &triplets.entries { - let Some(companion_byte_hex) = entry.profile_companion_byte_hex.as_ref() else { - continue; - }; - if companion_byte_hex == "0x00" { - continue; - } - *grouped - .entry(( - companion_byte_hex.clone(), - entry.primary_name.clone(), - entry.secondary_name.clone(), - )) - .or_insert(0) += 1; - } - let mut summaries = grouped - .into_iter() - .map( - |((companion_byte_hex, primary_name, secondary_name), count)| { - SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { - companion_byte_hex, - primary_name, - secondary_name, - count, - } - }, - ) - .collect::>(); - summaries.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| left.companion_byte_hex.cmp(&right.companion_byte_hex)) - .then_with(|| left.primary_name.cmp(&right.primary_name)) - .then_with(|| left.secondary_name.cmp(&right.secondary_name)) - }); - summaries.truncate(10); - summaries -} - -fn summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_overlaps( - summaries: &[SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry], -) -> Vec { - let nonzero_stock_family_aliases = nonzero_stock_building_header_family_aliases(); - let mut overlaps = summaries - .iter() - .filter_map(|entry| { - let primary_matches = nonzero_stock_family_aliases - .contains(&canonicalize_building_like_name(&entry.primary_name)); - let secondary_matches = nonzero_stock_family_aliases - .contains(&canonicalize_building_like_name(&entry.secondary_name)); - if !(primary_matches || secondary_matches) { - return None; - } - Some( - SmpSavePlacedStructureNonzeroCompanionBuildingFamilyOverlapSummaryEntry { - companion_byte_hex: entry.companion_byte_hex.clone(), - primary_name: entry.primary_name.clone(), - secondary_name: entry.secondary_name.clone(), - count: entry.count, - primary_matches_nonzero_stock_building_header_family: primary_matches, - secondary_matches_nonzero_stock_building_header_family: secondary_matches, - }, - ) - }) - .collect::>(); - overlaps.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| left.companion_byte_hex.cmp(&right.companion_byte_hex)) - .then_with(|| left.primary_name.cmp(&right.primary_name)) - .then_with(|| left.secondary_name.cmp(&right.secondary_name)) - }); - overlaps.truncate(10); - overlaps -} - -fn summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_residues( - summaries: &[SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry], -) -> Vec { - let nonzero_stock_family_aliases = nonzero_stock_building_header_family_aliases(); - let mut residues = summaries - .iter() - .filter(|entry| { - !nonzero_stock_family_aliases - .contains(&canonicalize_building_like_name(&entry.primary_name)) - && !nonzero_stock_family_aliases - .contains(&canonicalize_building_like_name(&entry.secondary_name)) - }) - .map( - |entry| SmpSavePlacedStructureNonzeroCompanionBuildingFamilyResidueSummaryEntry { - companion_byte_hex: entry.companion_byte_hex.clone(), - primary_name: entry.primary_name.clone(), - secondary_name: entry.secondary_name.clone(), - count: entry.count, - }, - ) - .collect::>(); - residues.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| left.companion_byte_hex.cmp(&right.companion_byte_hex)) - .then_with(|| left.primary_name.cmp(&right.primary_name)) - .then_with(|| left.secondary_name.cmp(&right.secondary_name)) - }); - residues.truncate(10); - residues -} - -fn nonzero_stock_building_header_family_aliases() -> BTreeSet { - [ - "Brewery", - "ConcretePlant", - "ConstructionFirm", - "DairyProcessor", - "Distillery", - "ElectronicsPlant", - "Furnace", - "FurnitureFactory", - "Hospital", - "Lumbermill", - "MachineShop", - "MeatPackingPlant", - "Museum", - "PaperMill", - "PharmaceuticalPlant", - "Port", - "Recycling Plant", - "Steel Mill", - "Textile Mill", - "TextileMill", - "Tire Factory", - "Tool and Die", - "Toolndie", - "Warehouse", - ] - .into_iter() - .map(canonicalize_building_like_name) - .collect() -} - -fn canonicalize_building_like_name(name: &str) -> String { - name.chars() - .filter(|ch| !matches!(ch, ' ' | '_' | '-')) - .flat_map(|ch| ch.to_lowercase()) - .collect() -} - -fn summarize_near_city_acquisition_tri_lane_save_shape_family_candidates( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> Vec { - let Some(probe) = analysis.region_fixed_row_run_candidates.as_ref() else { - return Vec::new(); - }; - probe - .candidates - .iter() - .take(5) - .enumerate() - .map( - |(index, candidate)| SmpPeriodicTriLaneSaveShapeFamilyCandidateSummaryEntry { - rank: index + 1, - shape_family_signature: candidate.shape_family_signature.clone(), - rows_offset_hex: candidate.rows_offset_hex.clone(), - row_count: candidate.row_count, - row_stride_hex: candidate.row_stride_hex.clone(), - best_probable_density_lane_relative_offset_hex: candidate - .best_probable_density_lane_relative_offset_hex - .clone(), - }, - ) - .collect() -} - -fn build_periodic_company_service_trace_report( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> SmpPeriodicCompanyServiceTraceReport { - let profile_family = analysis.profile_family.clone(); - let selected_company_id = analysis.selected_company_id; - let region_record_body_present = analysis.region_record_triplets.is_some(); - let placed_structure_record_body_present = analysis.placed_structure_record_triplets.is_some(); - let infrastructure_asset_side_buffer_present = - analysis.placed_structure_dynamic_side_buffer.is_some(); - let world_issue_37_present = analysis.world_issue_37.is_some(); - let world_finance_neighborhood_present = analysis.world_finance_neighborhood.is_some(); - let peer_site_selector_candidate_owner_strip = - "0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70".to_string(); - let peer_site_selector_candidate_persisted_tag_hex = "0x5dc1".to_string(); - let peer_site_selector_candidate_selector_lane = "[owner+0x23e]".to_string(); - let peer_site_selector_candidate_secondary_payload_lane = "[owner+0x242]".to_string(); - let peer_site_selector_candidate_post_secondary_byte_status = - "unresolved 0x5dc1 post-secondary discriminator byte after the repeated secondary payload string".to_string(); - let peer_site_selector_candidate_class_identity_status = - "grounded_direct_local_helper_strip".to_string(); - let peer_site_selector_candidate_helper_linkage = vec![ - "0x0040ceab -> 0x0045c150".to_string(), - "0x0040d1a1 -> 0x0045c310".to_string(), - "0x0040cd70 seeds [site+0x3cc/+0x3d0] from 0x62b2fc / 0x62b268".to_string(), - "0x0040d1e1 -> 0x0045c3c0 consumes the same owner family's [site+0x246] child lane" - .to_string(), - ]; - let peer_site_selector_candidate_saved_payload_summaries = - summarize_peer_site_selector_candidate_saved_payloads(analysis); - let peer_site_selector_candidate_saved_payload_delta_summaries = - summarize_peer_site_selector_candidate_saved_payload_deltas(analysis); - let peer_site_selector_candidate_saved_footer_padding_summaries = - summarize_peer_site_selector_candidate_saved_footer_padding(analysis); - let peer_site_selector_candidate_saved_companion_byte_summaries = - summarize_peer_site_selector_candidate_saved_companion_bytes(analysis); - let peer_site_selector_candidate_saved_policy_trailing_word_summaries = - summarize_peer_site_selector_candidate_saved_policy_trailing_words(analysis); - let peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries = - summarize_peer_site_selector_candidate_saved_nonzero_companion_name_pairs(analysis); - let peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries = - summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_overlaps( - &peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries, - ); - let peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries = - summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_residues( - &peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries, - ); - let peer_site_persisted_selector_bundle_fields = vec![ - "0x5dc1 payload lane [owner+0x23e] restored by 0x0045c150 and later fed into 0x0045c36e" - .to_string(), - "0x5dc1 payload lane [owner+0x242] restored by 0x0045c150 as the repeated secondary payload string" - .to_string(), - "0x5dc1 post-secondary one-byte residue after the repeated secondary payload string" - .to_string(), - "broader saved child/runtime selector bundle [owner+0x246/+0x24e/+0x252] emitted by 0x0040c980 -> 0x0045b560" - .to_string(), - ]; - let peer_site_rebuilt_transient_followon_fields = vec![ - "[owner+0x246] primary transient handle rebuilt from payload strings by 0x0045c310" - .to_string(), - "[owner+0x24a] ambient transient rebuilt through 0x0045b210 after 0x0045b5f0 refreshes the current derived position scalar" - .to_string(), - "transient roots [owner+0x24e/+0x256/+0x25a/+0x25e] cleared by 0x0045c150 before 0x0045b5f0 / 0x0045b6f0 rebuild follow-on variant state" - .to_string(), - "larger animation/light/random-sound variant family rooted in [owner+0x23e] rebuilt through 0x0045b6f0 / 0x0045b760 / 0x0045baf0" - .to_string(), - ]; - let peer_site_shellless_minimum_persisted_identity_status = - "name_pair_and_post_secondary_byte_minimum_identity_subset_child_runtime_bundle_rebuild_followon".to_string(); - let peer_site_shellless_minimum_persisted_identity_inputs = vec![ - "[site+0x3cc] cached source placed-structure id".to_string(), - "[site+0x3d0] cached companion candidate/profile id".to_string(), - "0x5dc1 payload lane [owner+0x23e]".to_string(), - "0x5dc1 payload lane [owner+0x242]".to_string(), - "0x5dc1 post-secondary one-byte residue".to_string(), - ]; - let peer_site_restore_input_fields = vec![ - "[site+0x3cc] saved placed-structure id feeding 0x62b2fc".to_string(), - "[site+0x3d0] saved companion-region id from [placed+0x173] feeding 0x62b268".to_string(), - "0x5dc1 payload lane [owner+0x23e] feeding 0x0045c36e selector arg 1".to_string(), - "0x5dc1 payload lane [owner+0x242] carrying the restored secondary payload string" - .to_string(), - "0x5dc1 post-secondary one-byte residue after the repeated secondary payload string" - .to_string(), - ]; - let peer_site_runtime_input_fields = vec![ - "[site+0x04] live backing-record selector consumed by 0x0047efe0 / 0x0047fd50".to_string(), - "[site+0x2a8] linked peer-site id consumed by 0x0040d1f0".to_string(), - "[peer+0x08] route-entry anchor id consumed by 0x0047dda0".to_string(), - ]; - let peer_site_runtime_reconstruction_status = - "restore_subset_and_bring_up_reconstruct_runtime_subset".to_string(); - let peer_site_runtime_reconstruction_steps = vec![ - "[site+0x04] restored from 0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70 -> 0x0052edf0" - .to_string(), - "[site+0x2a8] rewritten by 0x0040f6d0 after 0x00481390 returns the linked peer id" - .to_string(), - "[peer+0x08] refreshed during 0x00444690 -> 0x004133b0 -> 0x0040ee10 -> 0x0040edf6 -> 0x00480710" - .to_string(), - "world-cell owner and linked-site chains [cell+0xd4]/[cell+0xd6] republished during 0x00480710 via 0x0042bbf0/0x0042bbb0 and 0x0042c9f0/0x0042c9a0" - .to_string(), - ]; - let near_city_acquisition_region_input_fields = vec![ - "[site+0x276] owner-present gate".to_string(), - "placed-structure subject subtype gate [candidate+0x32] == 4 consumed through 0x0040d360" - .to_string(), - "[site+0x3d5] age/year delta lane".to_string(), - "[site+0x310/+0x338/+0x360] cached tri-lane sampled through 0x0040cac0".to_string(), - "[site+0x2a4] self placed-structure id lane later consumed through 0x004269b0".to_string(), - ]; - let near_city_acquisition_peer_input_fields = vec![ - "center-cell token gate 0x0041f6e0 -> 0x0042b2d0 over the current region".to_string(), - "[site+0x04] live backing-record selector consumed by 0x0047efe0 / 0x0047fd50".to_string(), - "0x0047fd50 linked-peer candidate-class gate over [candidate+0x8c] accepting only 0/1/2".to_string(), - "[site+0x2a8] linked peer-site id consumed by 0x0040d1f0".to_string(), - "[peer+0x08] route-entry anchor id consumed by 0x0047dda0".to_string(), - "world-cell owner chain [cell+0xd4] and linked-site chain [cell+0xd6] republished by 0x00480710".to_string(), - "linked-region status branch 0x0047de00 -> 0x0040c990".to_string(), - ]; - let near_city_acquisition_company_input_fields = vec![ - "company stat-family reader 0x2329/0x0d through 0x0042a5d0".to_string(), - "save-native linked-transit route-anchor entry id [company+0x0d35] through 0x00401860" - .to_string(), - "save-native linked-transit route-anchor fallback counts [company+0x7664/+0x7668/+0x766c] through 0x00401860" - .to_string(), - "current chairman profile byte [profile+0x291] through 0x00426ef0".to_string(), - "company byte [company+0x5b] and indexed lane [company+0x67 + 12*0x0042a0e0()]".to_string(), - "company-root argument [company+0x00] passed into 0x0040d540 and 0x00455f60".to_string(), - ]; - let near_city_acquisition_shellless_readiness_status = - "peer_and_company_inputs_grounded_site_owner_and_tri_restore_gaps_remaining".to_string(); - let near_city_acquisition_runtime_backed_input_families = vec![ - "peer-site restore subset [site+0x3cc/+0x3d0] plus tagged 0x5dc1 [owner+0x23e/+0x242]" - .to_string(), - "peer-site bring-up replay path reconstructing [site+0x04], [site+0x2a8], and [peer+0x08]" - .to_string(), - "linked-site post-load replay republishing world-cell owner and linked-site chains through 0x0042bbf0/0x0042bbb0 and 0x0042c9f0/0x0042c9a0" - .to_string(), - "placed-structure linked-company resolver 0x0047efe0 already grounds the live owner-company meaning of [site+0x276]" - .to_string(), - "placed-structure peer-chain helpers 0x0041f7e0 / 0x0041f810 / 0x0041f850 already ground [site+0x2a4] as the record's own placed-structure id lane" - .to_string(), - "0x004269b0 resolves the chosen site id back through live placed-structure collection 0x0062b26c before mutating [site+0x276], so the [site+0x2a4] self-id lane is reconstructible from collection identity once the chosen live row is known" - .to_string(), - "aux-candidate stream-load and stem-policy chain 0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0 already grounds the subtype byte consumed as [candidate+0x32]" - .to_string(), - "direct site constructor 0x004134d0 allocates through 0x00518900 and seeds broad row state through 0x0040f6d0, including [site+0x2a4], copied name bytes, [site+0x276], [site+0x3d4/+0x3d5], and cleared tri-lane-adjacent caches" - .to_string(), - "shared finalize helper 0x0040ef10 now has both create-side callers 0x00403ef3 / 0x00404489 and data-driven loader callers 0x0046f073 / 0x004707ff, so the [site+0x276] owner lane is grounded under constructor-plus-finalize families rather than only the post-load local replay strip" - .to_string(), - "data-driven loader callers 0x0046f073 / 0x004707ff push tuple fields [+0x00/+0x04/+0x0c] into 0x0040ef10, and that helper's third argument flows into ebx and then [site+0x276] at 0x0040f5d4" - .to_string(), - "at least one of those tuple-backed callers is now classified too: 0x004707ff sits under multiplayer transport selector-0x13 body 0x004706b0 rather than the ordinary save-load restore strip" - .to_string(), - "another tuple-backed 0x004134d0 family is classified too now: 0x00472b40 is the multiplayer transport selector-0x72 counted live-world apply path, and its inner builders 0x00472bef / 0x00472d03 reach 0x004134d0 from counted transport records rather than ordinary save-load restore" - .to_string(), - "non-transport caller 0x00422bb4 also reaches 0x004134d0, but it pushes live args plus literal flags 1/0 and returns the created row id through an out-param instead of feeding the tuple-backed finalize path" - .to_string(), - "the surviving 0x00508fd1 / 0x005098eb family is bounded away from persisted restore too: it caches the created site id in [this+0x7c], re-enters 0x0040eba0 with immediate coords, and later calls 0x0040ef10 with a hard zero third arg" - .to_string(), - "0x00473c20 is a separate live queue-drain family over scratch band 0x006ce808..0x006ce988: it iterates queued site ids and coordinate pairs, re-enters 0x0040eba0 at 0x00473c98, then clears each queued id, so it is a local post-create refresh path rather than a persisted replay owner" - .to_string(), - "the remaining direct [site+0x276] store census is bounded away from persisted replay too: 0x0042128d is broad zero-init in the 0x00421430 constructor neighborhood, 0x00422305 computes a live score/category lane before publishing 0x7, 0x004269c9/0x00426a2a are acquisition commit and clear helpers, and 0x004282a9/0x004300d6 are bulk owner-transfer writes" - .to_string(), - "the paired collection-side loader 0x00413440 is bounded away too: it owns the tagged 0x36b1/0x36b2/0x36b3 triplet load path, dispatches each live record through vtable slot +0x44, and keeps that seam on the already-grounded triplet payload rather than the missing [site+0x276] replay owner" - .to_string(), - "station-detail mutation path 0x0040dc40 already consumes [site+0x276], company stat-family 0x2329/0x0d, and candidate field [candidate+0x22], then commits linked-site side-state rebuild through 0x0040d1f0 / 0x00480710 / 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70" - .to_string(), - "city-connection direct-placement family 0x00402cb0 -> 0x00403ed5/0x0040446b -> 0x004134d0 -> 0x0040ef10 already grounds the shared allocator/finalize path for newly created site rows" - .to_string(), - "opcode-dispatch strip 0x00431b20 routes grouped 0x0061039c opcodes into the same live site-mutation helpers 0x00430040 / 0x00426d60 / 0x0042fc90 rather than the bring-up replay family" - .to_string(), - "direct local writer strip now grounds live cached-tri-lane producers too: 0x0040d450 writes [site+0x310] through owner-company-aware scoring, and the broader candidate-processing loop 0x00410b30..0x004118f4 writes [site+0x310/+0x338/+0x360] after gating rows through 0x00412560" - .to_string(), - "company stat-family lane 0x2329/0x0d already rehosted in runtime company state".to_string(), - "company market state now carries the save-native linked-transit route-anchor tuple [company+0x0d35] and [company+0x7664/+0x7668/+0x766c]" - .to_string(), - "chairman personality byte [profile+0x291] already reconstructed from raw save chairman rows" - .to_string(), - "company-root pointer and linked chairman/company save-native roster identity already imported" - .to_string(), - ]; - let near_city_acquisition_site_owner_company_projection_status = - "ordinary_replay_ruled_down_stream_load_callback_grounded_tuple_finalize_path_grounded_nontransport_restore_source_missing" - .to_string(); - let near_city_acquisition_site_self_id_projection_status = - "live_meaning_grounded_reconstructible_from_collection_identity".to_string(); - let near_city_acquisition_site_cached_tri_lane_projection_status = - "live_writer_family_grounded_semantics_and_persisted_inputs_missing".to_string(); - let near_city_acquisition_tri_lane_live_service_status = - "candidate_gate_and_live_writer_family_grounded_exact_formula_and_persisted_inputs_missing" - .to_string(); - let near_city_acquisition_candidate_subtype_projection_status = - "cached_candidate_id_bridge_grounded_via_stream_load".to_string(); - let near_city_acquisition_backing_record_projection_status = - "stream_load_callback_grounded_via_0x40ce60".to_string(); - let near_city_acquisition_nontransport_persisted_source_status = - "ordinary_runtime_effect_candidate_present_trigger_lane_mapping_missing".to_string(); - let near_city_acquisition_nontransport_persisted_source_candidates = vec![ - "ordinary loaded runtime-effect lane 0x00444d92 -> 0x00432f40(kind 8) -> 0x004323a0 -> 0x00431b20".to_string(), - "non-direct runtime-event bundle 0x4e99/0x4e9a/0x4e9b decodes grouped placed-structure descriptors on checked maps".to_string(), - "restore-side loader 0x00433130 with 0x0042db20 repopulates ordinary live runtime-effect rows in 0x0062be18".to_string(), - "trigger-kind control lane [event+0x7ef] is editor-visible across 0x00..0x0a including kind 8".to_string(), - "remaining gap is which serialized/live rows feed trigger kind 8 into that lane and which loaded ordinary rows actually reach placed-structure mutation opcodes".to_string(), - ]; - let near_city_acquisition_tri_lane_save_shape_family_candidates = - summarize_near_city_acquisition_tri_lane_save_shape_family_candidates(analysis); - let near_city_acquisition_tri_lane_save_shape_family_status = - if near_city_acquisition_tri_lane_save_shape_family_candidates.is_empty() { - "save_shape_family_probe_missing".to_string() - } else { - "save_shape_family_candidates_present_fixed_offset_ruled_down".to_string() - }; - let near_city_acquisition_tri_lane_live_owner_families = vec![ - "0x0040d450 owner-company-aware local scorer producing [site+0x310]".to_string(), - "0x00410b30..0x004118f4 broader candidate-processing loop producing [site+0x310/+0x338/+0x360]" - .to_string(), - "0x00412560 shared candidate/admissibility gate above both scorer paths".to_string(), - "0x0040c9a0 deferred additive accumulator/reset folding the tri-lane into [site+0x2b4/+0x2b8/+0x2bc] and [site+0x2e4..]" - .to_string(), - "0x0040fcc0..0x0040fe28 and 0x00422c62..0x00422d3c weighted scoring/evaluation consumers reading 0x0040cac0" - .to_string(), - ]; - let near_city_acquisition_tri_lane_candidate_gate_fields = vec![ - "0x00412560 gates candidate rows using fields [+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44]".to_string(), - "0x00412560 consumes world date/flags through 0x006cec78".to_string(), - "0x00412560 resolves candidate rows from table 0x0062ba8c".to_string(), - "0x00412560 callers pass the placed-structure subject vtable slot +0x80 result plus owner-present flag from [site+0x246]".to_string(), - "direct callers of 0x00412560 are now bounded at 0x0040fb8d, 0x00410721, 0x00410b71, 0x00412620, and 0x004126d3".to_string(), - ]; - let near_city_acquisition_tri_lane_runtime_writer_roles = vec![ - "0x0040d450 adds one owner-company-aware local score component into [site+0x310]".to_string(), - "0x00410b30..0x004118f4 walks 0xbc-stride candidate rows and adds per-row score components into [site+0x310/+0x338/+0x360]".to_string(), - "0x0041114a7/0x004111572 add into [site+0x310] and 0x0041114b7/0x004111582 add into [site+0x338]".to_string(), - "0x0041118aa/0x0041118f4 add into [site+0x360]".to_string(), - "0x0040c9a0 later folds the tri-lane into [site+0x2b4/+0x2b8/+0x2bc] and clears the transient producer lanes".to_string(), - ]; - let near_city_acquisition_tri_lane_direct_caller_families = vec![ - "0x0040fb70 is a small wrapper passing one candidate row plus the subject vtable slot +0x80 result and owner-present flag into 0x00412560".to_string(), - "0x004b4052 / 0x004b46ec are collection-wide 0x0040fb70 callers iterating 0x0062b26c candidate rows".to_string(), - "0x00401633 is an acquisition-adjacent 0x0040d540 caller that immediately feeds company stat-family 0x2329/0x0d".to_string(), - "0x0044b81a is an owner-company-aware 0x0040d540 caller that also reaches 0x0040cb70 and 0x00436590 news/event id 0x65".to_string(), - "0x004b70f5 / 0x004b7979 are broader 0x0040d540 callers routing through 0x004337a0 and downstream 0x00540120 / 0x00518140 state consumers".to_string(), - ]; - let near_city_acquisition_tri_lane_formula_input_lanes = vec![ - "0x00412560 uses candidate-row time window [+0x20/+0x22], owner/absence booleans [+0x24/+0x28], list count [+0x2c], and membership list [+0x44]".to_string(), - "0x00412560 consumes world date and policy flags through 0x006cec78 fields [world+0x0d] and [world+0x4afb]".to_string(), - "0x0040d450 combines helper outputs 0x00455810 / 0x00455800 / 0x0044ad60 with owner-company lane [site+0x276] and world event sink 0x00436590 ids 0x66/0x68".to_string(), - "0x00410b30..0x004118f4 consumes candidate-row fields [+0x18/+0x1c/+0x2a/+0x2c/+0x44], subject latch [site+0x78c], and personality byte [site+0x391]".to_string(), - "0x00410b30..0x004118f4 also feeds world-side scalar [world+0x4caa], owner-company scalar [company+0x0d5d] through 0x0040d210, and the local cache bands [site+0x2e8], [site+0x310], [site+0x338], and [site+0x360]".to_string(), - ]; - let near_city_acquisition_projection_hypotheses = vec![ - SmpServiceConsumerHypothesis { - label: "site_owner_replay_from_post_load_refresh_self_id_reconstructible".to_string(), - status: "ordinary_replay_and_stream_load_ruled_down_tuple_finalize_positive_path_grounded".to_string(), - candidate_consumers: vec![ - "0x00444690 late world bring-up caller".to_string(), - "0x004133b0 placed-structure local-runtime replay owner".to_string(), - "0x0040ee10 live-site position/scalar refresh helper".to_string(), - "0x00480710 linked-site runtime side-buffer and route-entry refresh".to_string(), - "0x004134d0 allocator plus direct site constructor 0x0040f6d0".to_string(), - "0x00403ef3 / 0x00404489 create-side callers of shared finalize helper 0x0040ef10" - .to_string(), - "0x0046f073 / 0x004707ff data-driven loader callers of shared finalize helper 0x0040ef10" - .to_string(), - "0x004269b0 acquisition commit owner resolving live site rows by id".to_string(), - ], - evidence: vec![ - "[site+0x276] live owner-company meaning is grounded through 0x0047efe0 / 0x0040d210".to_string(), - "[site+0x2a4] self-id meaning is grounded through 0x0041f7e0 / 0x0041f810 / 0x0041f850".to_string(), - "0x004269b0 resolves the chosen site id through placed-structure collection 0x0062b26c before mutating the live row, so [site+0x2a4] is reconstructible from collection identity rather than a separate serializer-owned selector".to_string(), - "0x00444690 -> 0x004133b0 -> 0x0040ee10 is the current checked-in ordinary bring-up replay family above live placed structures".to_string(), - "the ordinary restore-side staging order is tighter now too: world bring-up calls 0x00413280 first for tagged 0x36b1/0x36b2/0x36b3 stream load at 0x00444467, refreshes dynamic side buffers through 0x00481210 at 0x004444d8, and only later enters 0x004133b0 at 0x00444690 for queued local-runtime replay plus 0x0040ee10".to_string(), - "0x004133b0 rebuilds cloned local-runtime records through 0x0040e450 and 0x0040ee10 only republishes local position/scalar triplets before 0x0040e360".to_string(), - "the ordinary bring-up strip stays separate from the constructor/finalize family too: after 0x00444690 -> 0x004133b0 the world restore continues through later route-entry/grid/tagged refresh owners rather than re-entering 0x004134d0 / 0x0040f6d0 / 0x0040ef10 for already-restored rows".to_string(), - "[site+0x27a] is now bounded as a live signed scalar accumulator rather than a second owner-identity mystery: base constructor 0x00421200 zeros it at 0x0042125d, create-side initializer 0x0040f6d0 zeros it again at 0x0040f793, station-detail apply 0x0040dfec accumulates into it before 0x0040d1f0 / 0x00480710, acquisition commit stores it at 0x004269e4, acquisition clear negates and clears it through 0x00426a44..0x00426a90, and acquisition delta helper 0x00426ad8 accumulates into it again".to_string(), - "direct local replay-strip inspection now splits that family more precisely: bounded 0x0040ee10 itself only reads cached source lane [site+0x3cc], and the bounded 0x00480710 neighborhood is working from [site+0x04], [site+0x08], and [site+0x3cc]; the broader immediate continuation 0x0040e360..0x0040edf6 still consumes [site+0x2a8], [site+0x2a4], and [site+0x276] around 0x0040d230 / 0x0040d1f0 / 0x00480710 / 0x00426b10 / 0x00455860, but in the checked range those [site+0x276] uses are still reads/queries rather than a direct rehydrating store".to_string(), - "direct constructor control-flow now shows 0x004134d0 allocating a new placed-structure row through 0x00518900 and then calling 0x0040f6d0, which seeds [site+0x2a4], clears broad row state, copies the display name bytes, and writes [site+0x276] from an incoming argument before any later service logic runs".to_string(), - "0x0040f6d0 immediately zeroes [site+0x2a8/+0x272/+0x27a/+0x29e], stamps [site+0x3d4/+0x3d5], and seeds further local caches, which makes it a create-side initializer rather than a replay-only refresh".to_string(), - "shared finalize helper 0x0040ef10 now has create-side callers 0x00403ef3 / 0x00404489 and data-driven callers 0x0046f073 / 0x004707ff; the latter feed it from tuple-backed loads after 0x0040eba0 / 0x0052eb90 rather than from the checked-in local replay strip".to_string(), - "the loader-side dataflow is narrower now too: 0x0046efbf is the paired constructor call and 0x0046f073 / 0x004707ff are the paired finalize calls in the tuple-backed data path; 0x0046efbf and 0x0047074b both reach 0x004134d0 first, then 0x0046f073 / 0x004707ff push tuple fields [+0x00/+0x04/+0x0c] into 0x0040ef10, that helper reads arg3 into ebx at 0x0040ef1c, and the paired write at 0x0040f5d4 stores ebx into [site+0x276] while 0x0040f5da stores the computed companion word into [site+0x27a]".to_string(), - "the outer owner above 0x0046efbf / 0x0046f073 / 0x0047074b / 0x004707ff is now classified too: atlas-backed recovery ties those calls to multiplayer transport selector-0x13 body 0x004706b0, which attempts the placed-structure apply path through 0x004197e0 / 0x004134d0 / 0x0040eba0 / 0x0052eb90 / 0x0040ef10 rather than ordinary save-load restore".to_string(), - "the neighboring builder strip 0x00472b40 is classified too now: atlas-backed recovery ties it to multiplayer transport selector-0x72, the heavier counted live-world apply path over 0x0062b2fc / 0x0062b26c / 0x0062bae0, and its inner calls 0x00472bef / 0x00472d03 reach 0x004134d0 from counted transport records rather than ordinary save-load restore".to_string(), - "another surviving 0x004134d0 caller is bounded away from persisted restore too: 0x00422bb4 pushes one live 0x0062b2fc record plus local args and literal flags 1/0 into 0x004134d0, then returns the created row id through an out-param rather than re-entering the tuple-backed finalize path".to_string(), - "the remaining 0x00508fd1 / 0x005098eb strip is bounded away from persisted restore too: 0x00508fd1 stores the new row id in [this+0x7c], immediately configures the live row through vtable slot +0x58 plus 0x00507cf0, and 0x005098eb later re-enters 0x0040ef10 with arg3 forced to zero, so this family is another live controller path rather than the missing persisted owner seam".to_string(), - "the adjacent 0x00473c20 family is bounded away too: it drains queued site ids and coordinate pairs from scratch band 0x006ce808..0x006ce988, re-enters 0x0040eba0 at 0x00473c98 for each live row, and then clears the queued id slot, so it is a local post-create refresh path rather than the missing persisted owner seam".to_string(), - "the remaining direct [site+0x276] store census is bounded away too: 0x0042128d is broad zero-init in the 0x00421430 constructor neighborhood, 0x00422305 computes a live score/category lane before publishing event 0x7, 0x004269c9/0x00426a2a are acquisition commit and clear helpers, and 0x004282a9 / 0x004300d6 are bulk owner-transfer writes rather than ordinary restored-row replay".to_string(), - "the paired collection-side serializer 0x00413440 is bounded away too: it opens tagged families 0x36b1 / 0x36b2 / 0x36b3 on the save side, routes each live record through per-entry vtable slot +0x44, and keeps that seam on the already-grounded triplet payload/load-save strip rather than the missing [site+0x276] replay owner".to_string(), - "the actual broader restore-side stream-load owner remains 0x00413280: it opens tagged 0x36b1 / 0x36b2 / 0x36b3 on the load side, dispatches per-entry vtable slot +0x40, and currently only grounds the cached-source bridge 0x0040ce60 -> 0x0040cd70 / 0x0045c150 rather than a direct [site+0x276] republisher".to_string(), - "direct local control flow now rules the ordinary bring-up owner down even further: 0x00444690 immediately calls 0x004133b0 and then continues into later grid/world refresh owners without re-entering 0x004134d0 / 0x0040f6d0 / 0x0040ef10 for already-restored rows".to_string(), - "inside 0x0040ef10 the [site+0x276] write at 0x0040f047 only clears owner-company under a world-flag branch, while the paired [site+0x276]/[site+0x27a] write at 0x0040f5d4 follows a 0x00436590 event/scalar path and is not the generic post-load republisher".to_string(), - "direct local writer census now shows the grounded [site+0x276] write side clustering under live mutation families such as 0x004269b0 / 0x00426a10, the create-side 0x0040ef10 / 0x0040f6d0 strip, and the bulk reassignment families 0x00426dce..0x00426ea1 and 0x00430040..0x004300d6 rather than under the known replay strip".to_string(), - "direct local control-flow reconstruction now shows those same writer families hanging under the 0x00431b20 opcode dispatcher over 0x0061039c: opcodes 0x04..0x07 dispatch to 0x00430040, opcodes 0x08/0x10..0x13 dispatch to 0x00426d60, and opcodes 0x0d/0x16 dispatch to 0x0042fc90".to_string(), - "0x0042fc90 itself iterates the live placed-structure collection 0x0062b26c, filters rows through 0x0040c990 plus optional company match [site+0x276], and then dispatches row vtable slot +0x70, which keeps that branch on the live application side rather than replay".to_string(), - "the trigger-kind field itself is now bounded as an ordinary loaded per-event lane rather than a startup-only special class: restore-side loader 0x00433130 repopulates live event collection 0x0062be18 from packed chunk family 0x4e21/0x4e22, and the event-detail editor strip 0x004d90ba..0x004d91ed writes [event+0x7ef] across the full 0x00..0x0a range through controls 0x4e98..0x4ea2, including kind 8 at 0x004d91b3".to_string(), - "that keeps 0x00444d92 -> 0x00432f40(kind 8) on the ordinary loaded runtime-effect pipeline too: world bring-up is servicing pre-existing rows from 0x0062be18 rather than a one-off startup-only record class synthesized outside the collection".to_string(), - "the event-detail editor family now ties that trigger-kind field to the ordinary runtime-effect builders too: selected-event control family 0x004db02a / 0x004db1b8..0x004db309 mirrors current [event+0x7ef] back into controls 0x4e98..0x4ea2 under root control 0x4e84, while editor-side builder 0x004db9e5..0x004db9f1 allocates a runtime-effect row from compact payload into 0x0062be18 through 0x00432ea0 before rebinding the selected event id".to_string(), - "bundle-side inspection now grounds the ordinary startup collection further too: the non-direct 0x4e99/0x4e9a/0x4e9b runtime-event collection decodes as a compact serializer family recovered from 0x00433060/0x00430d70 plus the paired 0x00433130/0x0042db20 load path rather than an opaque raw blob, and sampled maps such as War Effort/British Isles/Germany/Texas Tea now decode their compact rows into actual condition/grouped summaries instead of signature-only parity".to_string(), - "the adjacent control-lane owner is bounded too now: nearby helper 0x0042e050 copies text bands plus [event+0x7ee..0x80f] between live runtime-event rows, which separates full event-control cloning from the narrower compact row-body loader 0x0042db20".to_string(), - ], - blockers: vec![ - "current atlas evidence now grounds one tuple-backed owner path too: loader tuple field [+0x0c] reaches [site+0x276] through 0x0046f073 / 0x004707ff -> 0x0040ef10, but the classified 0x004707ff caller belongs to multiplayer transport selector-0x13 rather than ordinary save-load restore, so a non-transport persisted source family is still needed for shellless acquisition".to_string(), - "the explicit store census now also rules down the remaining obvious non-transport writes, so the missing ordinary restored-row owner seam likely sits outside the currently bounded direct allocator/finalize/store families".to_string(), - "the paired collection-side triplet serializer 0x00413440 is ruled down too, so the missing ordinary restored-row owner seam likely sits outside the currently bounded direct allocator/finalize/store families and the tagged 0x36b1/0x36b2/0x36b3 load-save strip".to_string(), - "the load-side stream owner 0x00413280 is ruled down to cached-source/candidate replay through vtable slot +0x40 and 0x0040ce60, so the missing ordinary restored-row owner seam still sits beyond the current stream-load bridge too".to_string(), - "the ordinary bring-up caller 0x00444690 is ruled down too: it just enters 0x004133b0 and then proceeds into later refresh owners, so the positive [site+0x276] write side at 0x0040ef10 remains a tuple/create path rather than the checked ordinary restore path".to_string(), - "the checked ordinary restore ordering is ruled down too: 0x00413280 stream load, 0x00481210 dynamic side-buffer refresh, and 0x004133b0 local-runtime replay all sit on the bring-up strip without re-entering 0x004134d0 / 0x0040f6d0 / 0x0040ef10 for already-restored rows".to_string(), - "the grouped opcode dispatcher 0x00431b20 is still not a tagged restore owner, but the remaining uncertainty is now narrower than compact row framing too: restore-side 0x00433130 with 0x0042db20 reloads compact row bodies into ordinary live event rows in 0x0062be18, nearby 0x0042e050 is the separate full-event copy owner for [event+0x7ee..0x80f], the event-detail editor exposes [event+0x7ef] across 0x00..0x0a including kind 8, and sampled map bundles now decode into concrete grouped descriptors, so the open question is which serialized/live rows feed trigger kind 8 into that control lane and which of those loaded rows can actually reach the placed-structure mutation opcodes under 0x00431b20".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "site_cached_tri_lane_payload_or_restore_owner".to_string(), - status: "checked_in_save_seams_ruled_down_live_scoring_family_grounded_exact_semantics_open".to_string(), - candidate_consumers: vec![ - "0x36b1/0x36b2/0x36b3 placed-structure triplet owner".to_string(), - "0x00455fc0 shared tagged payload loader".to_string(), - "0x00444690 -> 0x004133b0 replay strip".to_string(), - "0x0040d450 owner-company-aware local scorer writing [site+0x310]".to_string(), - "0x00410b30..0x004118f4 candidate-processing loop writing [site+0x310/+0x338/+0x360] after 0x00412560".to_string(), - ], - evidence: vec![ - "0x0040cac0 is only the exact raw delta reader over [site+0x310/+0x338/+0x360]".to_string(), - "direct local binary inspection now shows 0x0040c9a0 as the deferred additive accumulator over [site+0x310/+0x338/+0x360], folding that tri-lane into [site+0x2b4/+0x2b8/+0x2bc], mirroring the nine-dword side array rooted at [site+0x2e4], and then clearing the tri-lane".to_string(), - "direct local caller census now shows 0x0040c9a0 only under the broad live-collection sweep 0x0040a3a1..0x0040a4d3, while 0x0040cac0 stays under weighted scoring or evaluation families such as 0x0040fcc0..0x0040fe28 and 0x00422c62..0x00422d3c".to_string(), - "direct local binary inspection now shows concrete live producers too: 0x0040d4aa/0x0040d4b0 add into [site+0x310], 0x0041114a7/0x004111572 add into [site+0x310], 0x0041114b7/0x004111582 add into [site+0x338], and 0x0041118aa/0x0041118f4 add into [site+0x360]".to_string(), - "0x0040d450 is a small owner-company-aware producer over [site+0x276], 0x00455810/0x00455800/0x0044ad60, and 0x00436590 ids 0x66/0x68 that writes directly into [site+0x310]".to_string(), - "0x00410b30..0x004118f4 is a broader candidate-processing loop walking 0xbc-stride rows, gating them through 0x00412560, and then accumulating stack temporaries plus direct writes into [site+0x310/+0x338/+0x360]".to_string(), - "0x00412560 itself is a shared candidate/admissibility gate over candidate-row fields [+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44], world date/flags via 0x006cec78, and the candidate table 0x0062ba8c".to_string(), - "current checked-in save owners still do not serialize those lanes directly, which rules down the known save seams while leaving a later restore family open".to_string(), - "0x00481430 -> 0x0047d8e0 repopulates the dynamic side-buffer route-entry list, three byte arrays, five proximity buckets, and trailing scratch band from stream without claiming the tri-lane".to_string(), - ], - blockers: vec![ - "no checked-in triplet or side-buffer payload field is tied directly to the tri-lane, and the remaining open question is now the exact service semantics and persisted inputs of the grounded live scorer family rather than the existence of runtime writers".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "cached_source_candidate_id_to_subtype_projection".to_string(), - status: "grounded_stream_load_callback_0x40ce60".to_string(), - candidate_consumers: vec![ - "0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70".to_string(), - "0x005c8c50 +0x40 stream-load callback 0x0040ce60".to_string(), - "0x0040cd70 cached source/candidate resolver seeding [site+0x3cc/+0x3d0]".to_string(), - "0x0040cee0 cached candidate resolver through 0x0062b268".to_string(), - "0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0 aux-candidate load chain".to_string(), - ], - evidence: vec![ - "[site+0x04] backing-record selector owner is grounded, but the stronger checked-in bridge is now [site+0x3cc/+0x3d0]".to_string(), - "direct local binary inspection now shows the placed-structure stream-load path 0x00413280 dispatching per-entry vtable slot +0x40 on the 0x005c8c50 specialization table, and that slot resolves to 0x0040ce60".to_string(), - "0x0040ce60 canonicalizes the Radio Station stem and then re-enters 0x0040cd70 plus 0x0045c150 on stream load".to_string(), - "0x0040cd70 rebuilds cached source id [site+0x3cc] and candidate/profile id [site+0x3d0]".to_string(), - "0x0040cee0 resolves cached candidate id [site+0x3d0] back into the live candidate pool 0x0062b268".to_string(), - "0x004138f0 already counts live placed structures by cached candidate id [site+0x3d0], confirming that lane as a real live selector bridge".to_string(), - "candidate subtype ownership is bounded under the aux-candidate load and stem-policy chain".to_string(), - "0x0040d360 only consumes the loaded candidate subtype byte [candidate+0x32] == 4".to_string(), - ], - blockers: Vec::new(), - }, - ]; - let near_city_acquisition_remaining_owner_gaps = vec![ - "non-transport persisted source family outside the currently bounded direct allocator/finalize/store families and tagged 0x36b1/0x36b2/0x36b3 load path, feeding the tuple-to-live projection of placed-structure owner-company field [site+0x276] for the acquisition-side owner-present gate; the live meaning is grounded through 0x0047efe0, the owner family is bounded under 0x004134d0 / 0x0040f6d0 plus shared finalize helper 0x0040ef10, and loader tuple field [+0x0c] is known to seed [site+0x276] through 0x0046f073 / 0x004707ff, but the classified 0x004707ff caller sits under multiplayer transport selector-0x13 rather than ordinary save-load restore".to_string(), - "exact persisted inputs and shellless service semantics for the now-grounded live cached tri-lane writer family over [site+0x310/+0x338/+0x360], especially 0x0040d450 and 0x00410b30..0x004118f4 above 0x00412560".to_string(), - ]; - let near_city_acquisition_region_lane_statuses = vec![ - "[site+0x276] owner-present gate: consumed directly by 0x004014b0, the live owner-company meaning is grounded through 0x0047efe0, the write family is bounded under 0x004134d0 / 0x0040f6d0 plus shared finalize helper 0x0040ef10, loader tuple field [+0x0c] is known to seed that lane through 0x0046f073 / 0x004707ff, and the tagged 0x36b1/0x36b2/0x36b3 collection loader is ruled down; the remaining gap is which non-transport persisted source family and companion restore calls outside those bounded families are sufficient for shellless acquisition".to_string(), - "[site+0x2a4] placed-structure id lane: peer-chain helpers already ground this as the record's own site id, constructor-side 0x00480210 seeds it for new linked-site rows, and 0x004269b0 resolves the chosen site id back through 0x0062b26c before mutating [site+0x276], so this lane is reconstructible from collection identity for the 0x004014b0 commit path".to_string(), - "[site+0x310/+0x338/+0x360] cached tri-lane: exact delta reader grounded at 0x0040cac0, deferred additive accumulator/reset helper grounded at 0x0040c9a0, and direct live producers now grounded at 0x0040d450 plus the broader 0x00410b30..0x004118f4 candidate-processing loop above 0x00412560; the remaining gap is exact service semantics and persisted inputs, not writer existence".to_string(), - "placed-structure subtype filter: 0x0040d360 is the exact test [candidate+0x32] == 4, the owning subtype byte is already bounded under 0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0, and direct local binary inspection now grounds stream-load callback 0x0040ce60 as the restore-side bridge into [site+0x3cc/+0x3d0]".to_string(), - ]; - let atlas_candidate_consumers = vec![ - "0x004019e0 periodic company outer service owner".to_string(), - "0x00406050 city-connection bonus/news owner".to_string(), - "0x00402cb0 city-connection direct-placement builder owner".to_string(), - "0x00409950 linked-transit train-roster balancer".to_string(), - "0x004014b0 near-city industry acquisition and news owner".to_string(), - "0x0040dc40 station-detail linked-site mutation validator/apply owner".to_string(), - "0x00401c50 annual finance-policy owner".to_string(), - "0x00420030 / 0x00420280 peer-site boolean/selector pair over 0x006cec20".to_string(), - "0x004093d0 / 0x00407bd0 linked-transit refresh tails".to_string(), - ]; - let known_bridge_helpers = vec![ - "0x004078a0 preferred-locomotive chooser feeding company byte 0x0d17".to_string(), - "0x0041d550 locomotive-era and engine-type approval gate over scenario opinion lanes" - .to_string(), - "0x004010f0 near-city acquisition region scorer over class-0 region entries".to_string(), - "0x00405920 same-company industry proximity aggregator over linked site peers".to_string(), - "0x0041f6e0 center-cell token gate feeding 0x0042b2d0 over the current region".to_string(), - "0x0042b2d0 packed-u16 contains-key predicate over the region token list".to_string(), - "0x0047de00 linked-region resolver feeding the candidate status branch 0x0040c990" - .to_string(), - "0x004801a0 linked-transit route-anchor reachability gate for one candidate site" - .to_string(), - "0x00425b90 pending-bonus/company-state gate over the [region+0x276] companion object" - .to_string(), - "0x0040cac0 placed-structure cached tri-lane delta sampler over [site+0x310/+0x338/+0x360]" - .to_string(), - "0x0040c9a0 deferred additive accumulator/reset folding [site+0x310/+0x338/+0x360] into [site+0x2b4/+0x2b8/+0x2bc] and the side array rooted at [site+0x2e4]" - .to_string(), - "0x0040d450 owner-company-aware local scorer writing directly into [site+0x310] via 0x00455810/0x00455800/0x0044ad60 and 0x00436590 ids 0x66/0x68" - .to_string(), - "0x0040fb70 small wrapper passing the subject vtable slot +0x80 result plus owner-present flag into 0x00412560" - .to_string(), - "0x00412560 shared candidate/admissibility gate over 0x0062ba8c candidate rows and world date/flags before the tri-lane scoring loop" - .to_string(), - "0x00410b30..0x004118f4 broader candidate-processing loop writing [site+0x310/+0x338/+0x360] after 0x00412560" - .to_string(), - "0x00401633 acquisition-adjacent 0x0040d540 caller immediately feeding company stat-family 0x2329/0x0d" - .to_string(), - "0x0044b81a owner-company-aware 0x0040d540 caller reaching 0x0040cb70 and 0x00436590 news/event id 0x65" - .to_string(), - "0x004b4052 / 0x004b46ec collection-wide 0x0040fb70 callers iterating 0x0062b26c candidate rows" - .to_string(), - "0x004b70f5 / 0x004b7979 broader 0x0040d540 callers routing through 0x004337a0 and downstream 0x00540120 / 0x00518140 state consumers" - .to_string(), - "0x0040d360 placed-structure subtype-4 predicate over [candidate+0x32]" - .to_string(), - "0x0040d540 weighted region-to-company proximity scorer with pending-bonus context" - .to_string(), - "0x0040f6d0 / 0x00481390 / 0x00480210 subtype-1 constructor family seeding linked record own id, anchor-site id, and linked peer id" - .to_string(), - "0x0040d210 owner-side placed-structure resolver from [site+0x276] through 0x0062be10" - .to_string(), - "0x0041f7e0 / 0x0041f810 / 0x0041f850 peer-chain helpers grounding [site+0x2a4] as the record's own site id" - .to_string(), - "0x00481390 / 0x00480210 subtype-1 linked-site allocation and constructor".to_string(), - "0x00444690 late world bring-up caller of 0x004133b0 placed-structure local-runtime replay" - .to_string(), - "0x004133b0 placed-structure local-runtime replay owner draining queued site ids through 0x0040e450 and sweeping live sites through 0x0040ee10".to_string(), - "0x0040e360..0x0040edf6 broader replay continuation consuming [site+0x2a8/+0x2a4/+0x276] around 0x0040d230 / 0x0040d1f0 / 0x00480710 / 0x00426b10 / 0x00455860" - .to_string(), - "0x0040e450 queued site-id cloned local-runtime replay helper".to_string(), - "0x0040ee10 live-site position/scalar refresh helper reaching 0x0040edf6 -> 0x00480710 and 0x0040e360".to_string(), - "0x00480710 linked-site runtime side-buffer and route-entry-anchor refresh owner" - .to_string(), - "0x0042bbf0 / 0x0042bbb0 world-cell owner-chain refresh over [cell+0xd4]".to_string(), - "0x0042c9f0 / 0x0042c9a0 world-cell linked-site-chain refresh over [cell+0xd6]" - .to_string(), - "0x0040df27 / 0x0040e00a / 0x0040edf6 concrete linked-site side-refresh callers of 0x00480710" - .to_string(), - "0x004160aa non-bring-up runtime caller of 0x0040ee10".to_string(), - "0x0048abc0 / 0x00493cf0 route-entry-anchor rebind and synthesis strip".to_string(), - "0x0047dda0 linked-peer route-entry-anchor validator".to_string(), - "0x00420030 / 0x00420280 peer-site boolean/selector pair over the placed-structure collection".to_string(), - "0x0047efe0 placed_structure_query_linked_company_id returning owner company id from [site+0x276]".to_string(), - "0x004269b0 acquisition commit owner resolving one site id through 0x0062b26c and then mutating [site+0x276]/[site+0x27a] on the chosen live row".to_string(), - "0x00426dce..0x00426ea1 bulk placed-structure owner-company reassignment over 0x0062b26c for non-subtype-4 rows matching the current company".to_string(), - "0x00430040..0x004300d6 filtered placed-structure owner-company overwrite over site classes 0x09/0x0b/0x0c".to_string(), - "0x00431b20 grouped opcode dispatcher over 0x0061039c routing opcodes 0x04..0x07 to 0x00430040, 0x08/0x10..0x13 to 0x00426d60, and 0x0d/0x16 to 0x0042fc90".to_string(), - "0x0042fc90 live placed-structure mutator iterating 0x0062b26c through 0x0040c990, optional owner-company match [site+0x276], and row vtable slot +0x70".to_string(), - "0x0047fd50 linked-peer candidate-class gate returning true only for class-byte values 0/1/2 at [candidate+0x8c]".to_string(), - "0x004131f0 / 0x00412fb0 / 0x004120b0 / 0x00412ab0 aux-candidate load and stem-policy chain owning subtype byte [candidate+0x32]".to_string(), - "0x004134d0 direct allocator calling constructor 0x0040f6d0 for new placed-structure rows".to_string(), - "0x0040f6d0 create-side initializer seeding [site+0x2a4], [site+0x276], [site+0x3d4/+0x3d5], and cleared local caches".to_string(), - "0x0040dc40 station-detail linked-site mutation validator/apply path consuming [site+0x276], candidate field [candidate+0x22], and company stat-family 0x2329/0x0d" - .to_string(), - "0x00417840 / 0x004197e0 / 0x004142c0 / 0x004142d0 projected-cell validation and compact-grid replay strip ahead of the linked-site mutation" - .to_string(), - "0x0040d1f0 / 0x00480710 / 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70 linked-site mutation-side side-state rebuild strip" - .to_string(), - "0x00402cb0 / 0x00403ed5 / 0x0040446b city-connection direct-placement commit family" - .to_string(), - "0x00403ef3 / 0x00404489 create-side callers of shared finalize helper 0x0040ef10".to_string(), - "0x0046f073 / 0x004707ff data-driven loader callers of shared finalize helper 0x0040ef10".to_string(), - "0x0046f073 / 0x004707ff tuple field [+0x0c] feeding 0x0040ef10 arg3 and then [site+0x276] at 0x0040f5d4".to_string(), - "0x004706b0 multiplayer transport selector-0x13 body re-entering 0x004197e0 / 0x004134d0 / 0x0040eba0 / 0x0052eb90 / 0x0040ef10 before 0x004707ff".to_string(), - "0x00472b40 multiplayer transport selector-0x72 counted live-world apply path whose inner builders 0x00472bef / 0x00472d03 reach 0x004134d0 from counted transport records".to_string(), - "0x00422bb4 direct non-tuple allocator caller pushing one 0x0062b2fc record plus local args and literal flags 1/0 into 0x004134d0, then returning the created row id through an out-param".to_string(), - "0x00508fd1 / 0x005098eb live controller family caching a created site id in [this+0x7c], re-entering 0x0040eba0 with immediate coords, and later calling 0x0040ef10 with arg3 forced to zero".to_string(), - "0x00473c20 live queued-site refresh draining scratch band 0x006ce808..0x006ce988 and re-entering 0x0040eba0 at 0x00473c98 before clearing each queued id slot".to_string(), - "0x0042128d broad zero-init in the 0x00421430 constructor neighborhood clearing [site+0x276] with the surrounding site reset band".to_string(), - "0x00422305 live score/category publisher writing [site+0x276] before event 0x7 dispatch, not ordinary restore".to_string(), - "0x004269c9 / 0x00426a2a acquisition commit and clear helpers mutating [site+0x276]/[site+0x27a] on chosen live rows".to_string(), - "0x004282a9 / 0x004300d6 bulk owner-transfer writes over existing live placed-structure rows".to_string(), - "0x00413440 paired tagged 0x36b1/0x36b2/0x36b3 collection serializer dispatching each live record through vtable slot +0x44".to_string(), - "0x004134d0 / 0x0040ef10 shared placed-structure allocator and finalize-or-rebuild lane for newly created or tuple-loaded site rows" - .to_string(), - "0x00481430 / 0x0047d8e0 dynamic side-buffer stream-load owner repopulating route-entry lists, three byte arrays, five proximity buckets, and trailing scratch band" - .to_string(), - "0x0040c9a0 deferred additive accumulator/reset helper folding tri-lane [site+0x310/+0x338/+0x360] into [site+0x2b4/+0x2b8/+0x2bc] and mirroring the nine-dword side array rooted at [site+0x2e4]" - .to_string(), - "0x0040a3a1..0x0040a4d3 broad live-collection maintenance sweep calling 0x0040c9a0 once per live placed structure after sibling sweeps over companies, source records, and peer sites".to_string(), - "0x0040fcc0..0x0040fe28 and 0x00422c62..0x00422d3c weighted scoring families consuming 0x0040cac0 without grounding a tri-lane producer".to_string(), - "0x005c8c50 +0x40 stream-load callback 0x0040ce60 canonicalizing the site stem and re-entering 0x0040cd70 / 0x0045c150 for restored rows" - .to_string(), - "0x0052edf0 generic base constructor seeding [this+0x04] from caller arg 1".to_string(), - "0x00455b70 placed-structure specialization constructor feeding 0x0052edf0 arg 3 as the primary selector and arg 1 as fallback" - .to_string(), - "0x00455c62 concrete placed-structure specialization caller of 0x0052edf0".to_string(), - "0x00456100 local wrapper duplicating its first incoming arg across the 0x00455b70 selector/fallback bundle" - .to_string(), - "0x00456072 fixed 0x55f2 callback forwarding three local dwords plus unit scalars into 0x00455b70" - .to_string(), - "0x0045c36e / 0x0045da65 / 0x0045e0fc dense 0x00456100 caller family over stack-backed buffers and default scalar lanes" - .to_string(), - "0x0045c36e feeds 0x00456100 selector arg 1 from [owner+0x23e], 0x0045da65 feeds zero, and 0x0045e0fc feeds [ebp+0x08]" - .to_string(), - "0x0045c150 save-backed loader for [owner+0x23e/+0x242] via tagged payload 0x5dc1 ahead of 0x0045c310" - .to_string(), - "0x0040ceab / 0x0040d1a1 local linked-site helper strip calling 0x0045c150 / 0x0045c310 directly" - .to_string(), - "0x00485819 typed placed-structure caller of 0x0052edf0 via 0x530640-style argument bundle" - .to_string(), - "0x00490a79 chooser-side caller of 0x00455b70 with literal selector 0x005cfd74 and fallback seed 0x005c87a8" - .to_string(), - "0x00406050 city-connection bonus/news sibling owner".to_string(), - "0x00409950 linked-transit roster sibling owner".to_string(), - ]; - let next_owner_questions = vec![ - "Which persisted placed-structure and city-or-region linkage fields are still missing for a shellless 0x004014b0 implementation once the peer-site restore subset, company route-anchor tuple, [site+0x276] live owner meaning, and candidate subtype owner strip are accepted as grounded?".to_string(), - "How much of the linked-peer refresh path is strictly post-load versus recurring runtime maintenance now that 0x004133b0 reaches 0x0040ee10 -> 0x0040edf6 -> 0x00480710 during bring-up and 0x004160aa also re-enters 0x0040ee10 later?".to_string(), - "Which save-native or replay seam repopulates placed-structure owner-company field [site+0x276] for already-restored rows once [site+0x2a4] is treated as reconstructible from collection identity, the replay strip is ruled down, and the 0x00431b20 grouped opcode dispatcher is treated as a live application owner rather than a restore seam?".to_string(), - "Which persisted inputs and exact shellless formulas feed the grounded tri-lane live scorer family 0x0040d450 / 0x00410b30..0x004118f4 above 0x00412560 before 0x0040c9a0 folds the results into [site+0x2b4/+0x2b8/+0x2bc]?".to_string(), - "Which infrastructure consumer above the grounded 0x38a5 seam actually drives the linked-transit branch that 0x00409950 follows?".to_string(), - ]; - let linked_transit_shellless_readiness_status = - "timed_cache_and_train_side_followons_grounded_site_cache_input_owners_missing".to_string(); - let linked_transit_minimum_persisted_identity_inputs = vec![ - "save-backed company identity, current company id, and linked-transit latch [company+0x0d56] selecting one per-company cache cell root beneath [site+0x5bd][company_id]".to_string(), - "save-backed linked-transit route-anchor tuple [company+0x0d35] and fallback count lanes [company+0x7664/+0x7668/+0x766c] feeding the reachable-site strip above 0x00401860 / 0x004801a0".to_string(), - "save-backed placed-structure owner and class identity lanes [site+0x276] and [site+0x04] consumed by 0x0047efe0 / 0x0047fd50 before any linked-transit site is marked eligible".to_string(), - "save-backed placed-structure and peer identity lanes [site+0x2a4], [site+0x2a8], and [peer+0x04/+0x08] giving the live site and linked-peer ids that the cache rebuilds query".to_string(), - "save-backed world calendar lanes [world+0x15] and [world+0x0d] driving refresh cadence, age stamping, and the year-banded policy table used by 0x00408380".to_string(), - ]; - let linked_transit_live_rebuilt_cache_lanes = vec![ - "0x004093d0 stamps [company+0x0d3e], clears each selected [site+0x5bd][company_id] cell, frees and reallocates its peer table at +0x06, repopulates +0x02/+0x06/+0x0a, and marks bytes +0x00/+0x01 from the live site filter".to_string(), - "0x004093d0 fills each 0x0d-stride peer row from 0x004a6630, so peer-table dword +0x05 step count and float +0x09 normalized continuity share are rebuilt scratch from live route-entry tracker results".to_string(), - "0x00407bd0 clears [site+0x0e/+0x12/+0x16] before folding the rebuilt peer rows plus candidate tables back into weighted/raw/final per-site score lanes".to_string(), - "0x00481910 and 0x004819b0 rebuild the local occupancy/count lane [site+0x5c1] from current-site-id resolver 0x004a9340 rather than from any serialized cache blob".to_string(), - "0x004aee2b rewrites [site+0x5c5] from world counter [world+0x15], making the chooser age bonus a live rebuilt lane rather than persisted cache state".to_string(), - ]; - let linked_transit_runtime_backed_input_families = vec![ - "company linked-transit route-anchor tuple [company+0x0d35] and fallback count lanes [company+0x7664/+0x7668/+0x766c] through 0x00401860".to_string(), - "company linked-transit peer-cache refresh absolute counter [company+0x0d3e] driving the shorter 0x00409720 -> 0x004093d0 cadence".to_string(), - "company linked-transit autoroute site-score refresh absolute counter [company+0x0d3a] driving the longer 0x00409720 -> 0x00407bd0 cadence".to_string(), - "company linked-transit latch [company+0x0d56] consumed by 0x00409950 and the annual-finance debt lane".to_string(), - "active-company refresh owner 0x00429c10 walks the live company roster and re-enters 0x004093d0 for each active company when linked-transit site-peer caches need a broader rebuild".to_string(), - "placed-structure-side company cache root [site+0x5bd] allocated by 0x00407780 as a 0x20-entry pointer table of 0x1a-byte per-company cache cells and freed by 0x004077e0".to_string(), - "placed-structure replay strip 0x00444690 -> 0x004133b0 -> 0x0040ee10 -> 0x0040edf6 -> 0x00480710 already republishes linked-peer ids, route-entry anchors, and world-cell owner chains before later recurring call 0x004160aa re-enters 0x0040ee10".to_string(), - "placed-structure-side per-company cache cells addressed through [site+0x5bd][company_id] and peer rows filled by 0x004a6630".to_string(), - "per-company cache-cell layout is bounded too: bytes +0x00/+0x01 gate participation, dword +0x02 is peer-row count, dword +0x06 is peer-row pointer, dword +0x0a is the peer-cache refresh stamp, and floats +0x0e/+0x12/+0x16 are the weighted/raw/final linked-transit score lanes".to_string(), - "linked-transit peer-table row lanes +0x05 step count and +0x09 normalized continuity share under 0x004093d0".to_string(), - "linked-transit site-score cache lanes [site+0x0e/+0x12/+0x16] rebuilt by 0x00407bd0".to_string(), - "linked-transit local site occupancy/count lane [site+0x5c1] reset by 0x00481910, repopulated from current-site-id resolver 0x004a9340, adjusted by 0x004819b0, and consumed by 0x00408280 as a divisor/penalty lane".to_string(), - "linked-transit local site age lane [site+0x5c5] stamped from world counter [world+0x15] at 0x004aee2b and consumed by 0x00408280 as the zero-occupancy age bonus ladder".to_string(), - "structure candidate table root 0x0062ba8c is world-load owned by 0x0041f4e0 -> 0x0041ede0 -> 0x0041e970 before 0x00407bd0 reuses candidate-local bands".to_string(), - "route-entry tracker compatibility and endpoint-fallback chooser 0x004a6360 / 0x004a6630 sit under owner-notify refresh 0x00494fb0, so the peer-table metric source is already bounded above the linked-transit caches".to_string(), - "linked-transit aggregate roster-pressure helper 0x00408f70 consuming raw site-score lane [site+0x12]".to_string(), - "linked-transit ranked-site chooser 0x00408280 and staged autoroute entry builder 0x00408380 above the rebuilt site caches".to_string(), - "linked-transit train-side autoroute append / rotate strip 0x00409770 plus add-train owner 0x00409830 beneath roster balancer 0x00409950".to_string(), - ]; - let linked_transit_remaining_owner_gaps = vec![ - "which earlier restore or service owner feeds [site+0x276] and the live linked-peer rows before replay continuation 0x0040e360..0x0040edf6, now that direct inspection shows the 0x0040ea96..0x0040eb65 owner-company branch only consumes [site+0x276] and subtype-4 follow-on 0x0040eba0 already republishes [site+0x2a4] into the world-cell chains before 0x004093d0 / 0x00407bd0 rebuild their scratch cache lanes".to_string(), - "how much of the live placed-structure collection 0x006cec20 and linked-peer replay strip still has to run shelllessly beside already-grounded candidate-table owner 0x0041f4e0 / 0x0041ede0 and route-entry tracker owner 0x00494fb0 / 0x004a6360 before 0x00408280 / 0x00408380 / 0x00409770 become trustworthy".to_string(), - ]; - - let companies = analysis - .company_entries - .iter() - .map(|entry| { - let mut branches = Vec::new(); - branches.push(build_service_trace_branch_status( - "route_preference_override", - if entry.preferred_locomotive_engine_type_raw_u8 == 2 { - "grounded_electric_override_candidate" - } else { - "grounded_non_electric_or_inactive_override_candidate" - }, - &[ - "company periodic side-latch trio", - "world route-preference byte", - "preferred locomotive engine-type lane", - ], - &[], - &[ - "0x004019e0 periodic company outer owner", - "0x004078a0 preferred-locomotive chooser", - "0x0041d550 locomotive-era and engine-type approval gate", - ], - &[ - "This probe keeps the outer owner at the save-owned input level; the concrete runtime reader/apply/restore seam is already grounded separately.", - ], - )); - branches.push(build_service_trace_branch_status( - "annual_finance_policy", - "runnable_from_grounded_owner_state", - &[ - "company market/cache owner state", - "periodic side-latches", - "world issue/timing owner state", - "derived annual-finance readers", - ], - &[], - &[ - "0x004019e0 periodic company outer owner", - "0x00401c50 annual finance-policy owner", - ], - &[ - "The shellless annual-finance helper is already rehosted on top of runtime-owned state.", - ], - )); - branches.push(build_service_trace_branch_status( - "city_connection_announcement", - "blocked_missing_region_and_infrastructure_asset_owner_seams", - &[ - "company periodic side-latches", - "route-preference override seam", - "annual-finance sequencing owner", - ], - &[ - "region pending/completion/one-shot/severity lanes", - "stable region id or class discriminator", - "placed-structure or infrastructure-asset consumer mapping", - ], - &[ - "0x004019e0 periodic company outer owner", - "0x00406050 city-connection bonus/news owner", - "0x00420030 / 0x00420280 city-connection peer probes", - "0x0047efe0 placed-structure linked-company resolver", - ], - &[ - "Current atlas evidence places this branch above both the region pending-bonus lane and infrastructure/placed-structure consumers.", - ], - )); - branches.push(build_service_trace_branch_status( - "linked_transit_roster_maintenance", - "blocked_missing_site_cache_input_owner_mapping", - &[ - "company linked-transit latch", - "company linked-transit route-anchor tuple", - "company linked-transit peer-cache refresh absolute counter [company+0x0d3e]", - "company linked-transit autoroute-score refresh absolute counter [company+0x0d3a]", - "route-preference override seam", - ], - &[ - "placed-structure site-cache input owners beneath 0x004093d0 / 0x00407bd0", - "persisted site-side inputs behind 0x00408280 / 0x00408380", - ], - &[ - "0x004019e0 periodic company outer owner", - "0x00409720 timed linked-transit cache-service wrapper", - "0x004093d0 linked-transit site-peer cache rebuild", - "0x00407bd0 linked-transit autoroute site-score cache rebuild", - "0x00408280 linked-transit ranked-site chooser", - "0x00408380 linked-transit staged autoroute-entry builder", - "0x00408f70 linked-transit aggregate site-score pressure helper", - "0x00409770 linked-transit autoroute append/rotate owner", - "0x00409830 linked-transit add-train/news owner", - "0x00409950 linked-transit train-roster balancer", - ], - &[ - "The save side now grounds the timed cache-owner seams, ranked-site chooser, staged route builder, and train-side follow-ons; the remaining blocker is the placed-structure site-cache input ownership beneath those bounded consumers, not the train-side strip itself.", - ], - )); - branches.push(build_service_trace_branch_status( - "industry_acquisition_side_branch", - "blocked_missing_near-city_owner_mapping", - &[ - "periodic service outer owner", - "annual-finance ordering", - ], - &[ - "near-city industry candidate owner seam", - "city or region peer linkage", - ], - &[ - "0x004019e0 periodic company outer owner", - "0x004014b0 near-city industry acquisition and news owner", - "0x004010f0 near-city acquisition region scorer", - "0x00405920 same-company industry proximity aggregator", - "0x0041f6e0 center-cell token gate", - "0x0042b2d0 packed-u16 contains-key predicate", - "0x0047de00 linked-region resolver feeding 0x0040c990", - "0x004801a0 linked-transit route-anchor reachability gate", - "0x00425b90 pending-bonus/company-state gate", - "0x0040d360 region type/class filter", - "0x0040d540 weighted region-to-company proximity scorer", - "0x0040f6d0 subtype-1 placed-structure constructor", - "0x00481390 / 0x00480210 subtype-1 linked-site allocation and constructor", - "0x00444690 late world bring-up caller of 0x004133b0", - "0x004133b0 placed-structure local-runtime replay owner", - "0x0040e450 queued site-id cloned local-runtime replay helper", - "0x0040ee10 live-site position/scalar refresh helper", - "0x00480710 linked-site runtime side-buffer and route-entry-anchor refresh owner", - "0x0040df27 / 0x0040e00a / 0x0040edf6 linked-site side-refresh callers", - "0x004160aa non-bring-up runtime caller of 0x0040ee10", - "0x0048abc0 / 0x00493cf0 route-entry-anchor rebind and synthesis strip", - "0x0047dda0 linked-peer route-entry-anchor validator", - "0x00420030 / 0x00420280 peer-site boolean/selector pair", - "0x00406050 city-connection bonus/news sibling owner", - ], - &[ - "Direct disassembly now shows this branch scanning the live placed-structure collection at 0x0062b26c for the best current acquisition target, rejecting sites whose owner field [site+0x276] is already nonzero, reusing the center-cell token gate 0x0041f6e0 -> 0x0042b2d0, reusing the linked-region status branch 0x0047de00 -> 0x0040c990, checking candidate reachability through 0x004801a0, consulting the placed-structure peer-site boolean/selector pair 0x00420030 / 0x00420280 over 0x006cec20, scoring candidate sites against company proximity and age through 0x0040d540 and 0x0040cac0, and then committing the chosen site through 0x004269b0. The peer-site selector seam itself is now grounded through the local helper strip: 0x0040cd70 seeds [site+0x3cc/+0x3d0] from 0x62b2fc / 0x62b268, 0x0040ceab and 0x0040d1a1 reach the save-backed 0x0045c150 / 0x0045c310 owner directly, that owner fills [owner+0x23e/+0x242] from tagged payload 0x5dc1, and 0x0045c36e then feeds [owner+0x23e] through 0x00456100 -> 0x00455b70 -> 0x0052edf0 into the live backing-record selector [site+0x04]. The cached tri-lane is no longer a restore-only mystery either: 0x0040d450 and 0x00410b30..0x004118f4 now bound the live writer family above the shared 0x00412560 candidate/admissibility gate, 0x0040fb70 is the small wrapper into that gate, and direct callers now separate acquisition-adjacent 0x0040d540 users like 0x00401633 / 0x0044b81a from broader sibling sweeps such as 0x004b4052 / 0x004b46ec / 0x004b70f5 / 0x004b7979. The remaining linked-site field work is now about which persisted site/peer lanes are actually sufficient for shellless acquisition and city-connection behavior, not about who owns [site+0x04] or whether the tri-lane has live producers.", - ], - )); - SmpPeriodicCompanyServiceTraceEntry { - company_id: entry.company_id, - name: entry.name.clone(), - active: entry.active, - linked_chairman_profile_id: entry.linked_chairman_profile_id, - preferred_locomotive_engine_type_raw_u8: entry - .preferred_locomotive_engine_type_raw_u8, - city_connection_latch: entry.city_connection_latch, - linked_transit_latch: entry.linked_transit_latch, - linked_transit_autoroute_site_score_cache_refresh_absolute_counter: entry - .linked_transit_autoroute_site_score_cache_refresh_absolute_counter, - linked_transit_site_peer_cache_refresh_absolute_counter: entry - .linked_transit_site_peer_cache_refresh_absolute_counter, - branches, - } - }) - .collect::>(); - - let mut notes = Vec::new(); - notes.push( - "Periodic company service trace is intentionally an outer-owner probe: it reports save-owned branch inputs and blocker seams without serializing the full projected runtime reader state.".to_string(), - ); - notes.push( - "Direct disassembly now narrows the acquisition-side sibling substantially: 0x004014b0 gates on the periodic outer owner, then scans the live placed-structure collection at 0x0062b26c, rejects sites whose owner field [site+0x276] is already nonzero, filters candidates through the subtype-4 predicate 0x0040d360, scores surviving sites against company linkage/age/proximity through 0x0040d540 and 0x0040cac0, and then commits the chosen site through 0x004269b0 before the shared news lane 0x004554e0 formats the headline.".to_string(), - ); - notes.push( - "The shellless acquisition frontier is narrower now too: the peer-site selector/linked-peer seam is grounded enough to plan around, the company/chairman side now includes the save-native route-anchor tuple used by 0x00401860, the live owner-company meaning of [site+0x276] is grounded through 0x0047efe0 / 0x0040d210, the self-id meaning of [site+0x2a4] is grounded through 0x0041f7e0 / 0x0041f810 / 0x0041f850, constructor-side 0x00480210 already seeds that self-id lane for new linked-site rows, 0x004269b0 resolves the chosen site id back through 0x0062b26c before mutating [site+0x276], the cached tri-lane now has grounded live producers at 0x0040d450 and 0x00410b30..0x004118f4 above 0x00412560, and the subtype owner strip is bounded under the aux-candidate load chain 0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0. The remaining blocker is the placed-structure-side restore or replay ownership for [site+0x276], the persisted inputs and exact shellless semantics of the live cached tri-lane scorer family, plus the projection from [site+0x04] back into the loaded candidate subtype row.".to_string(), - ); - notes.push( - "The acquisition-side consumer family is tighter now too. The checked-in station-detail action path 0x0040dc40 already consumes live owner company [site+0x276], candidate field [candidate+0x22], company stat-family 0x2329/0x0d, projected-cell validation 0x00417840 -> 0x004197e0, and compact-grid replay 0x004142c0/0x004142d0 before it commits the linked-site mutation through 0x0040d1f0 -> 0x00480710 -> 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70. That means these lanes are already grounded as live preconditions and mutation-side rebuild inputs, even though the save or replay owner that populates them for shellless acquisition is still the open question.".to_string(), - ); - notes.push( - "The create-side family is grounded separately too. City-connection direct placement already reaches the shared constructor/finalize strip 0x00402cb0 -> 0x00403ed5/0x0040446b -> 0x004134d0 -> 0x0040ef10, and the direct writer census now shows [site+0x276] writes clustering under create-side, acquisition-side, and bulk control-transfer families rather than under the known replay strip. So the remaining shellless gap is no longer 'how are new placed structures finalized?', '[site+0x2a4] mystery', or 'does the tri-lane even have live writers?'; it is specifically how restored existing rows regain the owner-company lane and which persisted inputs feed the grounded tri-lane scorer family before acquisition-style consumers run.".to_string(), - ); - notes.push( - "The tri-lane restore side is narrower now too. The checked-in dynamic side-buffer load owner 0x00481430 -> 0x0047d8e0 repopulates the route-entry list, three per-site byte arrays, five proximity buckets, and the trailing scratch band from stream, so that seam is no longer a plausible hidden owner for [site+0x310/+0x338/+0x360].".to_string(), - ); - notes.push( - "Direct local binary inspection now also gives the tri-lane a concrete live runtime role: 0x0040c9a0 folds [site+0x310/+0x338/+0x360] into the local scalar band [site+0x2b4/+0x2b8/+0x2bc], mirrors the nine-dword side array rooted at [site+0x2e4], and then clears the tri-lane. Caller census keeps that accumulator role narrow too: 0x0040c9a0 only appears under the broad live-collection sweep 0x0040a3a1..0x0040a4d3, while 0x0040cac0 stays under weighted scoring/evaluation families such as 0x0040fcc0..0x0040fe28 and 0x00422c62..0x00422d3c. The direct writer strip is grounded too: 0x0040d450 writes [site+0x310] through owner-company-aware scoring, and the broader 0x00410b30..0x004118f4 candidate-processing loop writes [site+0x310/+0x338/+0x360] after gating rows through 0x00412560.".to_string(), - ); - notes.push( - "Cross-save compare now tightens the save-native side too: `compare_save_region_fixed_row_run_candidates` keys the pre-region-header fixed-row bands by lane-shape fingerprint because grounded saves do not keep one stable top rows_offset, but they do retain shared shape-family matches. The tri-lane-adjacent save seam should therefore be treated as a stable row-family problem, not a fixed-offset problem." - .to_string(), - ); - if let Some(best_shape_family) = - near_city_acquisition_tri_lane_save_shape_family_candidates.first() - { - notes.push(format!( - "The periodic-company trace now also carries the current top tri-lane-adjacent save row family: rank {} shape-family {} at rows {} with stride {} and probable density lane {:?}. That keeps the persisted side on a concrete save-native candidate family while the live tri-lane writers stay grounded separately.", - best_shape_family.rank, - best_shape_family.shape_family_signature, - best_shape_family.rows_offset_hex, - best_shape_family.row_stride_hex, - best_shape_family - .best_probable_density_lane_relative_offset_hex - .as_deref() - )); - } - notes.push( - "The periodic-company trace now also surfaces the strongest non-transport persisted source candidate for [site+0x276]: the ordinary loaded runtime-effect lane 0x00444d92 -> 0x00432f40(kind 8) -> 0x004323a0 -> 0x00431b20 above the non-direct 0x4e99/0x4e9a/0x4e9b bundle. That branch is no longer just a blocker note; the remaining question is the tighter control-lane mapping from loaded rows into trigger kind 8 and then into the placed-structure mutation opcodes." - .to_string(), - ); - notes.push( - "Installed-map cluster counts now sharpen that candidate lane too: the current rt3_105/maps corpus has 41 bundled maps, 38 of them with dispatch-strip rows, and 318 dispatch-strip records total, all still on the non-direct compact family with null trigger kind. The add-building subset inside that corpus is narrower still at 10 grouped occurrences across 7 recovered descriptor keys (Barracks, Bauxite Mine, FarmGrain, Furniture Factory, Logging Camp, Port01, Warehouse05), again all with null trigger kind. So the open question is no longer whether ordinary loaded rows can reach the 0x00431b20 strip at scale; it is specifically how any of those rows acquire or bypass the missing trigger-kind control lane." - .to_string(), - ); - notes.push( - "Direct local binary inspection now grounds the cached-candidate restore bridge too: the placed-structure stream-load owner 0x00413280 dispatches per-entry vtable slot +0x40 on the 0x005c8c50 specialization table, that slot resolves to 0x0040ce60, and 0x0040ce60 immediately re-enters 0x0040cd70 plus 0x0045c150. So the acquisition-side cached source/candidate bridge [site+0x3cc/+0x3d0] is no longer a generic restore mystery for stream-loaded rows; the remaining restored-row gaps are [site+0x276] and the deferred tri-lane.".to_string(), - ); - notes.push( - "Direct disassembly now tightens the remaining placed-structure lanes too: 0x0040cac0 is only the raw tri-lane delta reader over [site+0x310/+0x338/+0x360]; 0x0040d360 is only the exact subtype test [candidate+0x32] == 4; and the direct writer strip now shows [site+0x2a4] staying replay/constructor-owned while [site+0x310/+0x338/+0x360] is service-produced by the grounded 0x0040d450 / 0x00410b30..0x004118f4 family above 0x00412560.".to_string(), - ); - notes.push( - "That branch also reuses the same peer-site helper strip already bounded under the city-connection family: 0x0041f6e0 resolves the current center world-grid cell and checks one packed token through 0x0042b2d0, 0x0047de00 follows the linked region behind the candidate site into the status byte returned by 0x0040c990, and 0x004801a0 checks whether one candidate site is reachable from the cached company route anchor through 0x00401860 -> 0x0048e3c0.".to_string(), - ); - notes.push( - "Direct disassembly now closes the collection identity too: 0x00420030 is the boolean peer gate and 0x00420280 is the first-match selector over the live placed-structure / peer-site collection 0x006cec20, combining 0x0042b2d0, the optional linked-company filter through 0x0047efe0, the station-or-transit gate 0x0047fd50, and the linked-region status branch 0x0047de00 -> 0x0040c990.".to_string(), - ); - notes.push( - "The linked-transit sibling owner is tighter now too: timed wrapper 0x00409720 compares world counter [world+0x15] against the save-owned company refresh counters [company+0x0d3e] and [company+0x0d3a], reruns the shorter peer-cache rebuild 0x004093d0 on the tighter 0x7ff80 cadence, reruns the heavier autoroute score-cache rebuild 0x00407bd0 on the broader 0x31380 cadence, and then feeds the raw site-score total 0x00408f70 into roster balancer 0x00409950. That means the remaining blocker for shellless linked-transit maintenance is no longer the timing seam; it is the higher-layer placed-structure / infrastructure consumer mapping above those grounded cache owners.".to_string(), - ); - notes.push( - "The chooser-and-train strip is tighter now too: 0x00408280 only picks among company-owned linked-transit sites whose cache cells are already live, ranking them off site-cache lane [site+0x16] with one extra boost from local cache words [site+0x5c1/+0x5c5], while 0x00408380 either reuses one explicit site id or falls back to 0x00408280 before building a staged 0x33-byte route entry. Above that, 0x00409770 services the caches through 0x00409720, asks 0x00408380 for one staged entry, and then either appends or rotates it in-place, while 0x00409830 repeats the same builder twice before publishing the add-train/news side. So the remaining blocker is not the train-side consumer strip; it is the placed-structure-side owner seam that makes those site-cache lanes and ready bits trustworthy for shellless runs.".to_string(), - ); - notes.push( - "Those extra chooser-side local lanes are tighter now too: 0x00481910 first clears [site+0x5c1] across the live placed-structure collection and then repopulates it by resolving current site ids through 0x004a9340, while 0x004819b0 decrements one prior site's [site+0x5c1] and increments one new site's copy during later reassignment. 0x004a9340 itself is now bounded as a current-site-id resolver: it returns [this+0xa0] when present and otherwise falls back through 0x004b3360 before returning one dereferenced site id. The companion age lane [site+0x5c5] is stamped directly from world counter [world+0x15] at 0x004aee2b. 0x00408280 then uses [site+0x5c1] as an occupancy divisor/penalty and [site+0x5c5] as a zero-occupancy age bonus ladder, so those lanes are no longer anonymous chooser magic; they are grounded live cache inputs with bounded writer strips.".to_string(), - ); - notes.push( - "The per-company cache root is grounded now too: 0x00407780 allocates [site+0x5bd] as a 0x20-entry pointer table and seeds each entry with one zeroed 0x1a-byte company cache cell, while 0x004077e0 frees that same root and any nested cell payloads. That means the remaining linked-transit gap is no longer cache-root allocation identity; it is which persisted site-side inputs are sufficient to repopulate those per-company cells and the downstream score lanes before the bounded chooser/train strip runs shelllessly.".to_string(), - ); - notes.push( - "The per-company cache-cell layout is bounded now too: 0x004093d0 and 0x00407bd0 use bytes +0x00/+0x01 as the initial participation gates, dword +0x02 as the peer-row count, dword +0x06 as the peer-row pointer, dword +0x0a as the shorter peer-cache refresh stamp, and floats +0x0e/+0x12/+0x16 as the weighted/raw/final linked-transit score lanes. The candidate-table and route-entry-tracker owners are bounded above that too: 0x0062ba8c is constructed through 0x0041f4e0 -> 0x0041ede0 -> 0x0041e970, while 0x004a6360 / 0x004a6630 sit under owner-notify refresh 0x00494fb0. The remaining linked-transit gap is narrower again: subtype-4 follow-on 0x0040eba0 already republishes [site+0x2a4] through 0x004814c0 / 0x00481480 and world-cell chain helpers 0x0042c9f0 / 0x0042c9a0, and direct inspection of 0x0040ea96..0x0040eb65 shows that owner-company branch only consumes [site+0x276] rather than rehydrating it. That pushes the open question one level earlier to whichever restore or service owner feeds [site+0x276] and the live linked-peer rows before this replay continuation runs.".to_string(), - ); - notes.push( - "One nearby live helper strip is narrower now too: 0x004337a0 is exactly the raw selected-company getter over [world+0x21], used in the replay-side sibling around 0x0040e775 only to compare the current selection against [site+0x276]. The adjacent world-side helpers 0x00452d80 / 0x00452db0 / 0x00452fa0 are separate live selected-site or active service-state setters/dispatchers over [world+0x217d/+0x2181] gated by mode byte [world+0x2175]; they can clear or republish currently-selected site ids through 0x00413620 / 0x00413750, but they do not repopulate [site+0x276] for already-restored rows.".to_string(), - ); - notes.push( - "The base placed-structure load callback is narrower now too: local .rdata at 0x005cb4c0 shows that the shared base table, not the 0x005c8c50 specialization table, owns the 0x0045c150 / 0x0045b560 / 0x00455870 / 0x00455930 load-save quartet. Direct disassembly of 0x0045c150 -> 0x00455fc0 then shows that callback only reloads the generic 0x55f1/0x55f2/0x55f3 triplet/scalar bands and re-enters the same base triplet/scalar slots 0x00455870 / 0x00455930, so the missing placed-structure owner-company lane [site+0x276] still lies outside the checked-in base load path.".to_string(), - ); - notes.push( - "The remaining direct [site+0x276] writers are split more cleanly now too: 0x00421200 is the broad late-field constructor/reset zero-fill over the same 0x23a/0x23e/0x25a/0x25e/... row family and clears [+0x276] as part of that initialization; 0x00428270 is a collection-wide live owner remap over 0x0062b26c that rewrites [site+0x276] only for rows matching one caller-supplied old owner id; and 0x00422280 is a subtype-local synthetic scalar writer that buckets float lane [row+0x25e], stores one 100000 * rand(bucket) result into [+0x276], and immediately publishes localized-id 7. Those writes therefore sit under constructor/live mutation or subtype-local scalar families, not the missing restore-time owner-company replay seam.".to_string(), - ); - notes.push( - "Direct disassembly now closes the negative persistence side too: the direct 0x36b1 per-record callbacks serialize the shared base scalar triplets rooted at [this+0x206/+0x20a/+0x20e] plus the subordinate payload callback strip, while the 0x4a9d/0x4a3a/0x4a3b side-buffer owner only persists route-entry lists, three byte arrays, five proximity buckets, and the sampled-cell list. That means neither checked-in save owner seam currently persists the core peer-site identity fields [site+0x04], [site+0x2a8], or [peer+0x08] directly.".to_string(), - ); - if !peer_site_selector_candidate_saved_payload_summaries.is_empty() { - notes.push(format!( - "The periodic-company trace now also carries a compact save-side summary of the tagged 0x5dc1 placed-structure profile payload/status pairs already parsed from the 0x36b1 triplet seam; dominant current pair is {} / {} x{}, dominant adjacent payload delta is {:?}, dominant post-secondary byte is {:?}, dominant fixed-policy trailing word is {:?}, and dominant pre-footer padding len is {:?}.", - peer_site_selector_candidate_saved_payload_summaries[0].profile_payload_dword_hex, - peer_site_selector_candidate_saved_payload_summaries[0].profile_status_kind, - peer_site_selector_candidate_saved_payload_summaries[0].count, - peer_site_selector_candidate_saved_payload_delta_summaries - .first() - .map(|entry| entry.delta_hex.as_str()), - peer_site_selector_candidate_saved_companion_byte_summaries - .first() - .map(|entry| entry.companion_byte_hex.as_str()), - peer_site_selector_candidate_saved_policy_trailing_word_summaries - .first() - .map(|entry| entry.policy_trailing_word_hex.as_str()), - peer_site_selector_candidate_saved_footer_padding_summaries - .first() - .map(|entry| entry.padding_len) - )); - } - if !peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries.is_empty() { - let dominant_nonzero_companion = - &peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries[0]; - notes.push(format!( - "The nonzero 0x5dc1 post-secondary byte residue is now narrower too: the trace exposes exact saved name pairs for nonzero-byte rows, and the current leading pair is {} / {} with byte {} x{}. The same atlas-backed owner strip already restores the repeated primary and secondary payload strings into [owner+0x23e] and [owner+0x242], so this byte should now be treated as a separate discriminator after the secondary string, not as the [owner+0x242] field itself. On grounded saves the exposed nonzero sample set is dominated by industry-like names rather than stations, maintenance, or residential rows, which makes the byte a stronger acquisition-side discriminator candidate than the monotone payload dword.", - dominant_nonzero_companion.primary_name, - dominant_nonzero_companion.secondary_name, - dominant_nonzero_companion.companion_byte_hex, - dominant_nonzero_companion.count - )); - } - if !peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries - .is_empty() - { - let dominant_overlap = - &peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries - [0]; - notes.push(format!( - "The same nonzero 0x5dc1 name-pair residue now bridges directly into the recovered stock Tier-2 building family too: the leading overlapping saved pair is {} / {} with byte {} x{}, and it matches the checked-in nonzero stock `.bty` header family (`dword_0xbb = 0x000001f4`) rather than the zero-valued station or maintenance/service families. That makes the acquisition-side discriminator and the Tier-2 banked-clone frontier a shared narrower industrial/commercial subset question, not two separate broad mysteries.", - dominant_overlap.primary_name, - dominant_overlap.secondary_name, - dominant_overlap.companion_byte_hex, - dominant_overlap.count - )); - } - if !peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries - .is_empty() - { - let dominant_residue = - &peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries - [0]; - notes.push(format!( - "The same trace now keeps the explicit non-overlap residue visible too: the leading saved pair still outside that recovered nonzero stock `.bty` family is {} / {} with byte {} x{}. That keeps the current Tier-2/source-selection queue honest: part of the peer-site nonzero residue now maps cleanly onto the recovered 0x000001f4 industrial/commercial family, but the remaining residue still needs a broader stock-header or later chooser-side explanation rather than being silently folded into the overlap set.", - dominant_residue.primary_name, - dominant_residue.secondary_name, - dominant_residue.companion_byte_hex, - dominant_residue.count - )); - } - notes.push( - "Direct disassembly now also separates the narrower peer-class gate from that payload residue: 0x0047fd50 resolves the linked peer through [site+0x04], reads candidate class byte [candidate+0x8c], and returns true only for values 0/1/2 while rejecting 3/4 and above. That means the newly isolated post-secondary byte is not the already-grounded station-or-transit class gate itself; it remains a separate saved discriminator above the restored name-pair payload.".to_string(), - ); - if let (Some(dominant_companion), Some(dominant_trailing_word)) = ( - peer_site_selector_candidate_saved_companion_byte_summaries.first(), - peer_site_selector_candidate_saved_policy_trailing_word_summaries.first(), - ) { - notes.push(format!( - "The same focused 0x36b1 triplet probe now also keeps the fixed-policy trailing word narrow at {} x{} while the profile side stays dominated by companion byte {} and payload/status pair {} / {}. Together that keeps the checked-in triplet seam looking like local structure/profile state rather than the missing acquisition owner-company lane [site+0x276] or cached tri-lane [site+0x310/+0x338/+0x360].", - dominant_trailing_word.policy_trailing_word_hex, - dominant_trailing_word.count, - dominant_companion.companion_byte_hex, - peer_site_selector_candidate_saved_payload_summaries[0].profile_payload_dword_hex, - peer_site_selector_candidate_saved_payload_summaries[0].profile_status_kind, - )); - } - notes.push( - "The replay strip is tighter now too. 0x00444690 is the current late world bring-up caller of 0x004133b0, that outer owner drains queued site ids through 0x0040e450 and then sweeps every live placed structure through 0x0040ee10, and 0x0040ee10 itself reaches 0x0040edf6 -> 0x00480710 plus the later 0x0040e360 follow-on. A separate runtime path at 0x004160aa also re-enters 0x0040ee10 later. So [peer+0x08] replay is no longer the open question, and [site+0x04] is no longer an owner mystery either: the local linked-site helper strip seeds [site+0x3cc/+0x3d0] from 0x62b2fc / 0x62b268, reaches the save-backed 0x0045c150 / 0x0045c310 owner directly, that owner fills [owner+0x23e/+0x242] from tagged payload 0x5dc1, and 0x0045c36e then feeds [owner+0x23e] through 0x00456100 -> 0x00455b70 -> 0x0052edf0 into [site+0x04]. The remaining non-hook target is now the smaller shellless-simulation question: which subset of those persisted site/peer fields is actually sufficient to run 0x004014b0 and its city-connection sibling without shell state.".to_string(), - ); - notes.push( - "The same persisted selector seam is broader than just the two strings. Atlas-backed recovery now bounds 0x0040c980 -> 0x0045b560 as the derived serializer over [site+0x23e/+0x242/+0x246/+0x24e/+0x252], so the remaining restore-owner search should treat that 0x5dc1/0x5dc2 selector/child/runtime bundle as one persisted field family rather than only [site+0x23e/+0x242]." - .to_string(), - ); - notes.push( - "The loader-side counterpart now narrows the shellless minimum persisted subset too. 0x0045c150 restores [owner+0x23e] and [owner+0x242], clears the transient roots, and then hands off to 0x0045c310 / 0x0045b5f0 / 0x0045b6f0 to rebuild the primary child handle and larger ambient/animation/light/random-sound family. That means the broader 0x5dc1/0x5dc2 bundle should be treated as one persisted owner seam, but current shellless planning can keep the minimum identity subset at the cached ids [site+0x3cc/+0x3d0], the restored name-pair [owner+0x23e/+0x242], and the post-secondary discriminator byte while the child/runtime follow-ons stay on the rebuild side." - .to_string(), - ); - - SmpPeriodicCompanyServiceTraceReport { - profile_family, - selected_company_id, - world_issue_37_present, - world_finance_neighborhood_present, - region_record_body_present, - placed_structure_record_body_present, - infrastructure_asset_side_buffer_present, - peer_site_selector_candidate_owner_strip, - peer_site_selector_candidate_persisted_tag_hex, - peer_site_selector_candidate_selector_lane, - peer_site_selector_candidate_secondary_payload_lane, - peer_site_selector_candidate_post_secondary_byte_status, - peer_site_selector_candidate_class_identity_status, - peer_site_selector_candidate_helper_linkage, - peer_site_selector_candidate_saved_payload_summaries, - peer_site_selector_candidate_saved_payload_delta_summaries, - peer_site_selector_candidate_saved_footer_padding_summaries, - peer_site_selector_candidate_saved_companion_byte_summaries, - peer_site_selector_candidate_saved_policy_trailing_word_summaries, - peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries, - peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries, - peer_site_selector_candidate_saved_nonzero_companion_building_family_residue_summaries, - peer_site_persisted_selector_bundle_fields, - peer_site_rebuilt_transient_followon_fields, - peer_site_shellless_minimum_persisted_identity_status, - peer_site_shellless_minimum_persisted_identity_inputs, - peer_site_restore_input_fields, - peer_site_runtime_input_fields, - peer_site_runtime_reconstruction_status, - peer_site_runtime_reconstruction_steps, - near_city_acquisition_region_input_fields, - near_city_acquisition_peer_input_fields, - near_city_acquisition_company_input_fields, - near_city_acquisition_shellless_readiness_status, - near_city_acquisition_runtime_backed_input_families, - near_city_acquisition_site_owner_company_projection_status, - near_city_acquisition_site_self_id_projection_status, - near_city_acquisition_site_cached_tri_lane_projection_status, - near_city_acquisition_tri_lane_live_service_status, - near_city_acquisition_candidate_subtype_projection_status, - near_city_acquisition_backing_record_projection_status, - near_city_acquisition_nontransport_persisted_source_status, - near_city_acquisition_nontransport_persisted_source_candidates, - near_city_acquisition_tri_lane_save_shape_family_status, - near_city_acquisition_tri_lane_save_shape_family_candidates, - near_city_acquisition_tri_lane_live_owner_families, - near_city_acquisition_tri_lane_candidate_gate_fields, - near_city_acquisition_tri_lane_runtime_writer_roles, - near_city_acquisition_tri_lane_direct_caller_families, - near_city_acquisition_tri_lane_formula_input_lanes, - near_city_acquisition_projection_hypotheses, - near_city_acquisition_remaining_owner_gaps, - near_city_acquisition_region_lane_statuses, - atlas_candidate_consumers, - known_bridge_helpers, - next_owner_questions, - linked_transit_shellless_readiness_status, - linked_transit_minimum_persisted_identity_inputs, - linked_transit_live_rebuilt_cache_lanes, - linked_transit_runtime_backed_input_families, - linked_transit_remaining_owner_gaps, - companies, - notes, - } -} - -pub fn inspect_save_region_service_trace_file( - path: &Path, -) -> Result> { - let analysis = inspect_save_company_and_chairman_analysis_file(path)?; - Ok(build_region_service_trace_report(&analysis)) -} - -pub fn inspect_save_infrastructure_asset_trace_file( - path: &Path, -) -> Result> { - let analysis = inspect_save_company_and_chairman_analysis_file(path)?; - Ok(build_infrastructure_asset_trace_report(&analysis)) -} - -fn build_region_service_trace_report( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> SmpRegionServiceTraceReport { - let atlas_candidate_consumers = vec![ - "0x00422100 periodic class-0 region picker and queue seed owner".to_string(), - "0x004337c0 queued 0x20-byte notice-node append helper".to_string(), - "0x00437c00 queued-kind dispatch owner".to_string(), - "0x004c7520 kind-7 region-focused custom-modal owner".to_string(), - "0x004358d0 pending region bonus service owner".to_string(), - "0x00438710 recurring queued-notice service owner".to_string(), - "0x00420410 world_region_refresh_profile_availability_summary_bytes_0x2f6_0x2fa_0x2fe" - .to_string(), - "0x004204c0 world_region_refresh_profile_availability_display_strings_for_cached_selector_0x2f2" - .to_string(), - "0x00420030 / 0x00420280 city-connection peer probes".to_string(), - "0x0047efe0 placed-structure linked-company resolver".to_string(), - ]; - let known_owner_bridge_fields = vec![ - "[region+0x25e] pending-bonus severity/source lane".to_string(), - "[region+0x276] pending bonus amount".to_string(), - "[region+0x302] completion latch".to_string(), - "[region+0x316] one-shot fallback notice latch".to_string(), - "[region+0x356] localized region name".to_string(), - "[region+0x23a] world-scalar-backed region lane used in notices".to_string(), - ]; - let known_bridge_helpers = vec![ - "0x004207d0 city_site_format_connection_bonus_status_label".to_string(), - "0x00420030 city_connection_bonus_exists_matching_peer_site".to_string(), - "0x00420280 city_connection_bonus_select_first_matching_peer_site".to_string(), - "0x0047efe0 placed_structure_query_linked_company_id".to_string(), - "0x00480bb0 placed_structure_refresh_linked_site_display_name_and_route_anchor".to_string(), - "0x00420650 city-site local scalar refresh release-side companion".to_string(), - "0x00421510 world_region_collection_refresh_records_from_tagged_bundle".to_string(), - "0x0041f5c0 world_region_load_tagged_payload_and_profile_collection_0x37f".to_string(), - "0x00455fc0 shared region tagged-payload reload companion".to_string(), - "0x00455870 region triplet-band tagged restore callback (world-region vtable +0x48)" - .to_string(), - "0x00455930 region triplet-band tagged serializer callback (world-region vtable +0x4c)" - .to_string(), - "0x00420410 region profile availability-summary rebuild helper".to_string(), - "0x004204c0 region profile availability-display rebuild helper".to_string(), - "0x004cc930 selected-region severity/source editor helper".to_string(), - "0x00438150 fixed-region severity/source reseed owner".to_string(), - "0x00442cc0 fixed-region severity/source clamp owner".to_string(), - ]; - let next_owner_questions = vec![ - "Which restore seam re-seeds [region+0x25e] and clears [region+0x302/+0x316] before the grounded 0x00422100 -> 0x004358d0 producer/consumer cycle runs again?".to_string(), - "Which stable region id or class discriminator survives save/load strongly enough to drive 0x004358d0 after the class-0 raster/id rebuilds are ruled out?".to_string(), - "Which later global restore continuation after 0x00444887 rehydrates [region+0x2a4] and [region+0x310/+0x338/+0x360] once the 0x00421510 -> 0x0041f5c0 -> 0x00455fc0 path and the 0x00444b90 -> 0x00420560 per-region follow-on are both ruled down?".to_string(), - "Which post-load generation owner under 0x004384d0 actually republishes acquisition-side region lanes after world_load_saved_runtime_state_bundle returns: the 319 placed-structure replay strip, the 320 building setup strip, or the 321 economy-seeding tail?".to_string(), - "How far can the grounded 0x00420030/0x00420280 plus 0x0047efe0 connection chain be rehosted directly before the transient queued-notice family matters again?".to_string(), - ]; - let candidate_consumer_hypotheses = vec![ - SmpServiceConsumerHypothesis { - label: "pending region bonus service path".to_string(), - status: if analysis.region_record_triplets.is_some() { - "highest_priority_static_mapping_target".to_string() - } else { - "possible_consumer_family".to_string() - }, - candidate_consumers: vec![ - "0x004358d0 pending region bonus service owner".to_string(), - "0x00420030 / 0x00420280 city-connection peer probes".to_string(), - "0x0047efe0 placed-structure linked-company resolver".to_string(), - ], - evidence: vec![ - "atlas already bounds this owner as the direct consumer of [region+0x276], [region+0x302], and [region+0x316]".to_string(), - "the new region trace already proves the record envelope and profile subcollection, so the remaining gap is the separate persisted latch seam rather than the service owner".to_string(), - "the neighboring region-profile summary/display strip is now grounded as rebuild-only follow-on work: 0x00420410 and 0x004204c0 walk the restored profile collection [region+0x37f], resolve backing candidates through 0x00412b70, and then rebuild [region+0x2f6/+0x2fa/+0x2fe] plus cached-selector display strings [region+0x2f2] from candidate bytes [candidate+0xba/+0xbb] rather than reading persisted region-owned copies".to_string(), - "direct disassembly now shows 0x004358d0 calling 0x00420030 twice plus 0x00420280, resolving the linked company through 0x0047efe0, posting company stat slot 4, and then clearing [region+0x276] while stamping [region+0x302] or [region+0x316]".to_string(), - "that same direct disassembly now also tightens the branch meaning: the linked-company branch formats the localized region-name notice from [region+0x356], posts it through 0x004554e0 and 0x0042a080, clears [region+0x276], and stamps [region+0x302]=1, while the fallback branch only runs when [region+0x316]==0 and then flips that one-shot latch to 1 before emitting its alternate notice".to_string(), - "the checked-in constructor owner 0x00421200 now also proves these latches are initialized locally at record construction time, which narrows the remaining gap to post-construction restore or rebuild rather than basic field identity".to_string(), - ], - blockers: vec![ - "restore seam that re-seeds [region+0x25e] and clears [region+0x302/+0x316] between service cycles".to_string(), - "stable region id or class discriminator".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "region tagged-load restore path".to_string(), - status: if analysis.region_record_triplets.is_some() { - "parallel_static_mapping_target".to_string() - } else { - "possible_consumer_family".to_string() - }, - candidate_consumers: vec![ - "0x00421510 world_region_collection_refresh_records_from_tagged_bundle".to_string(), - "0x0041f5c0 world_region_load_tagged_payload_and_profile_collection_0x37f".to_string(), - "0x00455fc0 shared region tagged-payload reload companion".to_string(), - ], - evidence: vec![ - "the checked-in function map already grounds 0x00421510 as the tagged region-collection load owner that dispatches each live record through vtable slot +0x40".to_string(), - "the checked-in function map already grounds 0x0041f5c0 as the per-record load slot that reloads the tagged payload through 0x00455fc0 and then rebuilds profile collection [region+0x37f]".to_string(), - "constructor-side evidence now proves the latches are initialized locally, so the remaining gap can legitimately be framed as post-construction restore or rebuild".to_string(), - "direct disassembly of 0x0041f590/0x0041f5b0 now proves the world-region vtable root is 0x005c9a28, so the 0x00455fc0 dispatch at slot +0x48 lands on 0x00455870 and the serializer sibling at +0x4c lands on 0x00455930".to_string(), - "direct disassembly of 0x00455870/0x00455930 now shows that callback pair only restores and serializes two helper-local three-lane scalar bands: 0x00455870 reads six dwords through 0x531150 and forwards them to 0x530720 -> [helper+0x1e2/+0x1e6/+0x1ea] and 0x52e8b0 -> [helper+0x4b/+0x4f/+0x53], while 0x00455930 writes that same pair back through 0x531030; it still does not touch acquisition-side lanes [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), - "direct disassembly now tightens the rest of 0x00455fc0 too: after the +0x48 callback it only runs 0x0052ebd0 to read two one-byte generic flags through 0x531150 into base object bytes [this+0x20], [this+0x8d], [this+0x5c..+0x61], [this+0x1ee], [this+0x1fa], and [this+0x3e], then opens 0x55f3 only for span accounting before returning, so the missing region latches are not hiding in the remainder of 0x00455fc0 either".to_string(), - "direct disassembly of 0x00421510 now also shows the exact tagged loop shape: it opens 0x5209, reads 0x520a, iterates live entry ordinals through 0x518380/0x518140, seeds a stack-local world-region vtable helper through 0x0041f590/0x0041f5b0, dispatches each loaded record through slot +0x40, and only then closes 0x520b".to_string(), - "direct disassembly of 0x0041f5c0 now also shows its post-0x00455fc0 work is local to the profile collection path: it clamps [region+0x31b] back to 1.0f when needed, zeroes [region+0x317], allocates one 0x88-sized helper through 0x53b070/0x518b90, stores it at [region+0x37f], loads that helper through 0x518680, and clears [region+0x385] before returning".to_string(), - "the first caller-side checkpoint above 0x00421510 is now grounded too: 0x00444887 invokes the region collection refresh inside a broader restore sequence and then immediately advances to territory_collection_refresh_records_from_tagged_bundle 0x00487c20 and support_collection_refresh_records_from_tagged_bundle 0x0040b5d0, which makes the missing latches look like a later global rebuild seam rather than hidden work inside 0x00421510 itself".to_string(), - "direct disassembly now rules down the next 0x00444887 continuation branch too: after the region, territory, and support refresh owners, 0x00433130 only opens 0x4e99/0x4e9a, repopulates live event collection 0x0062be18 through 0x0042db20, and closes 0x4e9b, so that event-side loader is not the missing later region restore handoff for [region+0x2a4] or [region+0x310/+0x338/+0x360] either".to_string(), - "that broader restore strip now also has one grounded later region-local sweep: at 0x00444b08 it re-enters the live region collection through 0x00421ce0, which walks live records via 0x518380/0x518140 and dispatches 0x0041fb00 per record".to_string(), - "the checked-in atlas already grounds 0x0041fb00 as the class-0-only default-region helper under the same family, and 0x00421730 as the later raster finalizer that repopulates [world+0x212d] from class-0 region ids".to_string(), - "direct disassembly now tightens that later sweep too: 0x0041fb00 exits immediately for nonzero [region+0x23e], while 0x00421730 clears the per-cell region word at [world+0x212d]+cell*4+1, seeds cached bounds-like fields [region+0x242/+0x246/+0x24a/+0x24e/+0x252], and only then enters the class-0 path that consumes [region+0x256] and the coordinate helpers 0x00455800/0x00455810".to_string(), - "the companion region-set root is runtime-owned now too: direct disassembly of the broader bring-up strip at 0x00448740..0x0044881f shows 0x006cfc9c being allocated through 0x53b070 and constructed through 0x00487bd0 before later rebuild passes run, so the 0x00487650/0x004881b0 companion path is operating on a runtime-owned helper collection rather than a hidden save-owned latch seam".to_string(), - "the later restore-band siblings are tighter now too: 0x00487de0 clears the prior chunked border queues through 0x00533cf0, builds a small per-region id map from [region+0x00]/[region+0x35] keyed by class [region+0x31], scans the live world raster at [world+0x2131], and appends fresh border-segment rows through 0x00536ea0 without touching [region+0x25e/+0x276/+0x302/+0x316]".to_string(), - "the neighboring world-grid reseed 0x0044c4b0 is tighter now too: it clears bit 0x10 across the live grid byte at cell offset +0xe6, then walks the live region collection at 0x0062bae0, admits only class-0 records via [region+0x23e], resolves one representative center cell through 0x00455f60, and marks that same bit back on, which again reads as raster presentation state rather than pending/completion latch restore".to_string(), - "the companion region-set rebuild above that border pass is narrower now too: 0x00487650 is only a small constructor wrapper over 0x00487540 that seeds [region+0x00] from the caller-supplied id, while 0x004881b0 rebuilds [region+0x3d] from the world raster histogram, zeroes [region+0x41], folds class-0 children back into parent [region+0x41] through [region+0x35], and then tails into the border emitter on 0x006cfc9c via 0x00487de0".to_string(), - "the later class-0 batch at 0x00438087 is narrower now too: it walks live class-0 regions through 0x0062bae0, scales the mirrored severity/source pair [region+0x25a/+0x25e] from the current value using world-side factors, clamps the result, and then hands the whole collection to 0x00421c20; it never reads or writes [region+0x276/+0x302/+0x316]".to_string(), - "the follow-on 0x00421c20 is now bounded as a parameterized region-collection helper rather than a latch owner: it loops the same collection with caller-provided scalar arguments, dispatches each record through 0x004235c0, and never touches the pending/completion/one-shot lanes directly".to_string(), - "the subsequent world follow-ons are narrower too: 0x00437b20 only stages one world-side reentry guard at [world+0x46c38], iterates the live region collection through 0x00423d30, and then tails into 0x00434d40, while 0x00437220 rebuilds broader world byte-set state around [world+0x66be/+0x69db] and other global collections. Those branches are still broader runtime follow-ons, not direct owners of [region+0x276/+0x302/+0x316]".to_string(), - ], - blockers: vec![ - "which later restore or rebuild owner rehydrates [region+0x276/+0x302/+0x316] after the 0x00421510 -> 0x0041f5c0 -> 0x00455fc0 path completes".to_string(), - "whether [region+0x25e] severity/source and any stable region id/class discriminator are serialized elsewhere in the tagged region body or recomputed later by another post-load owner after the 0x00421ce0 -> 0x0041fb00 -> 0x00421730 class-0 raster/id sweep, 0x004881b0 companion cell-count rebuild, 0x00487de0 border rebuild, 0x0044c4b0 center-cell reseed, the 0x00438087 mirrored severity/source batch, and the 0x00421c20 -> 0x004235c0 follow-on helper loop are all ruled out as latch owners".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "periodic producer and queued-notice path".to_string(), - status: "secondary_candidate_after_pending_service".to_string(), - candidate_consumers: vec![ - "0x00422100 periodic class-0 region picker and queue seed owner".to_string(), - "0x004337c0 queued 0x20-byte notice-node append helper".to_string(), - "0x00438710 recurring queued-notice service owner".to_string(), - ], - evidence: vec![ - "atlas ties these owners to the transient kind-7 queue family rooted at [world+0x66a6]".to_string(), - "grounded save probes now show that the ordinary-save queue family is not obviously persisted, so this looks more like runtime rebuild state than a direct save seam".to_string(), - "direct disassembly now shows 0x00422100 itself owning the pending-amount seed: it counts eligible class-0 regions with [region+0x276]==0 and [region+0x302]==0, samples one candidate, buckets [region+0x25e] against three thresholds, writes the resulting amount to [region+0x276], and then appends the kind-7 queued notice through 0x004337c0".to_string(), - ], - blockers: vec![ - "transient queue is not obviously persisted in ordinary saves".to_string(), - "needs the upstream restore seam for [region+0x25e/+0x302/+0x316] rather than more queue-side probing".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "later global restore continuation".to_string(), - status: "next_global_restore_handoff_target".to_string(), - candidate_consumers: vec![ - "0x00444887 broader restore continuation after region refresh".to_string(), - "0x00487c20 territory_collection_refresh_records_from_tagged_bundle".to_string(), - "0x0040b5d0 support_collection_refresh_records_from_tagged_bundle".to_string(), - "0x00444b90 later per-region restore follow-on loop".to_string(), - "0x00420560 region profile-derived scalar refresh helper".to_string(), - ], - evidence: vec![ - "the checked-in region trace already grounds 0x00444887 as the first caller-side checkpoint above 0x00421510: it refreshes the region collection and then immediately advances into the territory and support collection refresh owners".to_string(), - "the neighboring atlas-backed restore symmetry already rules the territory side down somewhat too: 0x00487c20 only restores the live territory collection metadata/id family and its current per-entry slots 0x00487670/0x00487680 are still no-op load/save callbacks, so the territory leg now looks less likely than support or a later region-local rebuild to hide [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), - "the atlas-backed support seam is broader than a direct region payload owner too: 0x0040b5d0 sits over collection 0x0062b244, whose grounded live owners maintain goose-entry counters and neighboring world support lanes [world+0x4c9a/+0x4c9e/+0x4ca6/+0x4caa] plus selected support-entry state rather than an obvious per-region acquisition latch family".to_string(), - "the same grounded evidence already narrows the later per-region follow-on too: 0x00444b90 dispatches 0x00420560 over each live region after the broader restore continuation has already advanced".to_string(), - "direct disassembly already rules that per-region follow-on down as a latch owner: 0x00420560 only zeroes and recomputes [region+0x312] from the embedded profile collection [region+0x37f]/[region+0x383], revisits the linked placed-structure chain for class-mix terms, and lazily seeds the year-driven [region+0x317/+0x31b] band through 0x00420350, not [region+0x276/+0x302/+0x316]".to_string(), - "that leaves the broader 0x00444887 continuation as the next structured restore seam above the ruled-down 0x00421510 -> 0x0041f5c0 -> 0x00455fc0 path when chasing acquisition-side lanes [region+0x2a4] and [region+0x310/+0x338/+0x360]".to_string(), - ], - blockers: vec![ - "the concrete later-global restore owner that rehydrates [region+0x2a4] and [region+0x310/+0x338/+0x360] is still not grounded below the 0x00444887 continuation".to_string(), - "the later region-local rebuild now looks stronger than territory refresh 0x00487c20 or the broader support seam 0x0040b5d0, but the exact restore owner below the 0x00444887 continuation is still not grounded".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "post-load generation pipeline handoff".to_string(), - status: "next_post_load_owner_family".to_string(), - candidate_consumers: vec![ - "0x004384d0 world_run_post_load_generation_pipeline".to_string(), - "0x004133b0 placed_structure_collection_refresh_local_runtime_records_and_position_scalars".to_string(), - "0x00421c20 world_region_collection_run_building_population_pass".to_string(), - "0x004235c0 world_region_balance_structure_demand_and_place_candidates".to_string(), - "0x00423d30 world_region_refresh_cached_category_totals_and_weight_slots".to_string(), - "0x00437b20 simulation_run_chunked_fast_forward_burst".to_string(), - ], - evidence: vec![ - "the checked-in shell-load subgraph and function map now place world_load_saved_runtime_state_bundle 0x00446d40 directly ahead of world_run_post_load_generation_pipeline 0x004384d0, so the first later non-hook owner family after the ruled-down 0x00444887 continuation is the post-load generation strip rather than another tagged region payload callback".to_string(), - "0x004384d0 already exposes the stage ordering tightly enough to subdivide the next search: id 319 refreshes the route-entry collection, auxiliary route trackers, and then 0x004133b0 placed-structure local-runtime replay; id 320 runs 0x00421c20(1.0, 1) for the region-owned building setup strip; id 321 runs 0x00437b20 and then sweeps regions through 0x00423d30".to_string(), - "direct disassembly now tightens the early 0x004384d0 setup strip too: before the conditional 320/321 gates it always runs 0x0044fb70 transport/pricing-grid setup and 0x0041ea50 candidate-local-service setup, and the extra arg-guarded 0x00421b60 -> 0x004882e0 default-region pair sits beside them as the last pre-320/321 setup branch. Those owners are therefore setup-side world-grid, candidate-table, and border-refresh work rather than the missing [region+0x2a4] / [region+0x310/+0x338/+0x360] republisher".to_string(), - "the neighboring 319 helpers are ruled down more tightly now too: 0x004377a0 stays on chairman-slot/profile materialization by normalizing the 16 slot bundles at [world+0x69da], republishing selector bytes into [0x006cec7c+0x87], populating the live chairman-profile collection 0x006ceb9c, and clearing selected company/chairman bytes [world+0x21/+0x25]; 0x004348e0 only gates the transient list at [world+0x66a6]; 0x00437c00 is that list's typed dispatcher over queue-node kind byte [node+0x08]; and the later 0x0044d410 calls are world-rect/grid refresh work. None of those helpers republish [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), - "the 319 placed-structure replay strip is now grounded as more than generic setup glue: 0x004133b0 drains queued site ids through 0x0040e450, sweeps every live placed structure through 0x0040ee10, and then reaches the already-grounded linked-site follow-on 0x00480710, which is a stronger structural bridge into acquisition-side site or peer state than the ruled-down territory/support loaders".to_string(), - "the surrounding 319 helpers are ruled down further now too: 0x00437220 and 0x004377a0 stay on chairman-slot selector/profile materialization over [world+0x69d8/+0x69db] and scenario selector bytes [0x006cec7c+0x87], while 0x00434d40 only seeds the subtype-2 candidate runtime latch [candidate+0x7b0] after the later burst".to_string(), - "the 320 building setup strip is narrower but still relevant: 0x00421c20 dispatches every live region into 0x004235c0, and that worker consults the region profile collection [region+0x37f], the placed-instance registry 0x0062b26c, and demand-balancing helpers like 0x00422900/0x00422be0/0x00422ee0, so current evidence keeps it in the same acquisition-adjacent owner family even though it is not yet a direct writer to [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), - "direct worker recovery now narrows 0x004235c0 further: it stays inside the live region demand-and-placement family by routing through 0x00422900 cached category accumulation, 0x004234e0 projected structure-count scalars, 0x00422be0 placed-count subtraction, and 0x00422ee0 placement attempts over 0x0062b26c rather than any restore-time field republisher".to_string(), - "the 321 economy-seeding tail is also now bounded as a narrower cache refresh rather than generic noise: 0x00437b20 only stages a fast-forward guard and then sweeps 0x0062bae0 through 0x00423d30, which refreshes the cached category band [region+0x27a/+0x27e/+0x282/+0x286], so it remains a weaker but still explicit post-load owner family to rule in or out before returning to the deeper 0x00446d40 continuation".to_string(), - "direct local disassembly now narrows 0x00423d30 as well: it only republishes [region+0x27a/+0x27e/+0x282/+0x286] through 0x00422900 after the 0x00437b20 burst and does not touch [region+0x2a4] or [region+0x310/+0x338/+0x360]".to_string(), - ], - blockers: vec![ - "current grounded evidence still does not show which post-load subphase actually republishes [region+0x2a4] or the cached tri-lane [region+0x310/+0x338/+0x360]".to_string(), - "0x00421c20 -> 0x004235c0 and 0x00437b20 -> 0x00423d30 are still grounded as region-side demand and cache refresh passes rather than direct restore writers, so the next static pass still needs the exact later field bridge".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "queued kind-7 modal dispatch path".to_string(), - status: "shell_adjacent_reference_only".to_string(), - candidate_consumers: vec![ - "0x00437c00 queued-kind dispatch owner".to_string(), - "0x004c7520 kind-7 region-focused custom-modal owner".to_string(), - ], - evidence: vec![ - "atlas already bounds this family as the shell-facing modal dispatch above the queued region id".to_string(), - "it is still useful as a reference owner for field identity, but not the first shellless rehost target".to_string(), - ], - blockers: vec![ - "full shell/dialog ownership remains out of scope".to_string(), - ], - }, - ]; - let entries = analysis - .region_record_triplets - .as_ref() - .map(|probe| { - probe.entries - .iter() - .map(|entry| SmpRegionServiceTraceEntry { - name: entry.name.clone(), - profile_collection_count: entry - .profile_collection - .as_ref() - .map(|collection| collection.live_record_count), - policy_leading_f32_0_text: format!("{:.6}", entry.policy_leading_f32_0), - policy_leading_f32_1_text: format!("{:.6}", entry.policy_leading_f32_1), - policy_leading_f32_2_text: format!("{:.6}", entry.policy_leading_f32_2), - policy_reserved_dword_hex_words: entry - .policy_reserved_dwords - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(), - branches: vec![ - build_service_trace_branch_status( - "pending_bonus_queue_seed", - "blocked_missing_pending_bonus_owner_lane", - &[ - "region triplet envelope", - "embedded profile subcollection", - "policy float lanes", - ], - &[ - "[region+0x276] pending amount lane", - "[region+0x25e] severity/source lane", - ], - &[ - "0x00422100 periodic class-0 region picker and queue seed owner", - "0x004337c0 queued 0x20-byte notice-node append helper", - ], - &["The queued kind-7 notice family is not obviously persisted in ordinary saves, so the pending queue must be treated as transient until a direct owner seam is found."], - ), - build_service_trace_branch_status( - "city_connection_completion", - "blocked_missing_completion_and_one_shot_latches", - &[ - "region triplet envelope", - "region name stem", - ], - &[ - "[region+0x302] completion latch", - "[region+0x316] one-shot notice latch", - "stable region id or class discriminator", - ], - &[ - "0x004358d0 pending region bonus service owner", - "0x00420030 / 0x00420280 city-connection peer probes", - "0x0047efe0 placed-structure linked-company resolver", - ], - &["The remaining region blocker is a separate owner seam for the latches the city-connection branch reads and writes."], - ), - ], - }) - .collect::>() - }) - .unwrap_or_default(); - - let mut notes = Vec::new(); - notes.push( - "Region service trace treats the queued kind-7 notice family as transient runtime state until a persisted owner seam is found.".to_string(), - ); - notes.push( - "Direct disassembly now grounds the core producer/consumer pair itself: 0x00422100 seeds [region+0x276] from the severity/source lane [region+0x25e] and appends the kind-7 notice through 0x004337c0, while 0x004358d0 consumes that amount, runs the city-connection peer probes 0x00420030/0x00420280 plus the linked-company resolver 0x0047efe0, and then stamps [region+0x302] or [region+0x316].".to_string(), - ); - notes.push( - "Direct disassembly now also tightens the severity/source side itself: 0x004cc930 is a selected-region editor helper that writes [region+0x25a] and [region+0x25e] together from one integer input, while 0x00438150 and 0x00442cc0 are fixed-region global reseed/clamp owners over collection 0x0062bae0 that adjust the same mirrored pair for hardcoded region ids.".to_string(), - ); - notes.push( - "Two more apparent offset hits are now ruled out as region false leads: 0x0043a5a0 is a separate constructor under vtable root 0x005ca078 that zeroes its own [this+0x302/+0x316] fields during local object setup, and 0x0045c460/0x0045c8xx is a separate vtable-0x005cb5e8 helper family whose [this+0x316] is a child-array pointer serialized through 0x61a9/0x61aa/0x61ab rather than a region latch.".to_string(), - ); - notes.push( - "A direct-writer census now narrows the remaining literal offset path further: the other `0x302/0x316` writer bands at 0x0043dd45/0x0043de19/0x0043e0a7/0x0043f5bc all hang off the same non-region 0x005ca078 object family as 0x0043a5a0 through helpers 0x0043af60/0x0043b030, so the only grounded region-owned literal writes left are the constructor 0x00421200 plus the producer/consumer pair 0x00422100 and 0x004358d0.".to_string(), - ); - notes.push( - "The later post-load per-region sweep is ruled down further now too: in the 0x00444887 restore strip, the follow-on loop at 0x00444b90 dispatches 0x00420560 over each live region, but that helper only zeroes and recomputes [region+0x312] from the embedded profile collection [region+0x37f]/[region+0x383] and lazily seeds the year-driven [region+0x317/+0x31b] band through 0x00420350, not [region+0x276/+0x302/+0x316].".to_string(), - ); - notes.push( - "The immediate profile-summary/display strip is ruled onto the rebuild side too: 0x00420410 rebuilds summary dwords [region+0x2f6/+0x2fa/+0x2fe] and 0x004204c0 refreshes cached-selector display strings [region+0x2f2] by walking the restored profile collection [region+0x37f], resolving backing candidates through 0x00412b70, and consuming candidate bytes [candidate+0xba/+0xbb]. Those bytes are therefore consumer-side summaries, not hidden persisted region lanes.".to_string(), - ); - notes.push( - "The current region seam is strong enough to prove record-envelope ownership, profile subcollection ownership, and the absence of hidden 0x55f3 tail padding on grounded saves.".to_string(), - ); - if let Some(probe) = analysis.region_record_triplets.as_ref() { - let mut trailing_words = probe - .entries - .iter() - .map(|entry| entry.policy_trailing_word_hex.clone()) - .collect::>(); - trailing_words.sort(); - trailing_words.dedup(); - let preview = trailing_words - .iter() - .take(4) - .cloned() - .collect::>() - .join(", "); - notes.push(format!( - "Region 0x55f2 trailing-word candidates currently collapse to {} distinct value(s): {}.", - trailing_words.len(), - if preview.is_empty() { - "none".to_string() - } else { - preview - } - )); - if probe.entries.iter().all(|entry| { - entry.policy_reserved_dwords.iter().all(|word| *word == 0) - && entry.policy_trailing_word == 1 - }) { - notes.push( - "Grounded region 0x55f2 fixed-policy chunks also keep all three reserved dwords at zero while the trailing word stays 0x0001, so that chunk is not currently carrying the missing latch/id discriminator." - .to_string(), - ); - } - let pre_name_prefix_count = probe - .entries - .iter() - .filter(|entry| entry.pre_name_prefix_len != 0) - .count(); - let unique_pre_name_prefix_lens = probe - .entries - .iter() - .map(|entry| entry.pre_name_prefix_len) - .collect::>() - .into_iter() - .collect::>(); - notes.push(format!( - "Grounded live-entry payload starts now also show {} of {} region records with nonzero bytes before the first 0x55f1 tag; unique pre-name prefix lengths are {:?}.", - pre_name_prefix_count, - probe.entries.len(), - unique_pre_name_prefix_lens - )); - } - SmpRegionServiceTraceReport { - profile_family: analysis.profile_family.clone(), - region_collection_header_present: analysis.region_collection_header.is_some(), - region_record_triplet_count: analysis - .region_record_triplets - .as_ref() - .map(|probe| probe.record_count) - .unwrap_or_default(), - queued_notice_record_count: analysis - .region_queued_notice_records - .as_ref() - .map(|probe| probe.entries.len()) - .unwrap_or_default(), - atlas_candidate_consumers, - known_owner_bridge_fields, - known_bridge_helpers, - next_owner_questions, - candidate_consumer_hypotheses, - entries, - notes, - } -} - -fn build_infrastructure_asset_trace_report( - analysis: &SmpSaveCompanyChairmanAnalysisReport, -) -> SmpInfrastructureAssetTraceReport { - let side_buffer = analysis.placed_structure_dynamic_side_buffer.as_ref(); - let alignment = analysis - .placed_structure_dynamic_side_buffer_alignment - .as_ref(); - let name_pair_summaries = side_buffer - .map(|probe| probe.name_pair_summaries.as_slice()) - .unwrap_or(&[]); - let bridge_like_name_pair_count = name_pair_summaries - .iter() - .filter(|summary| summary.primary_name.contains("Bridge")) - .count(); - let tunnel_like_name_pair_count = name_pair_summaries - .iter() - .filter(|summary| summary.primary_name.contains("Tunnel")) - .count(); - let track_cap_like_name_pair_count = name_pair_summaries - .iter() - .filter(|summary| summary.primary_name.contains("TrackCap")) - .count(); - let st_only_name_pair_corpus = !name_pair_summaries.is_empty() - && name_pair_summaries - .iter() - .all(|summary| summary.primary_name.contains("ST")) - && !name_pair_summaries - .iter() - .any(|summary| summary.primary_name.contains("DT")); - let atlas_candidate_consumers = vec![ - "0x00491c60 infrastructure tagged side-buffer serializer owner".to_string(), - "0x00493be0 infrastructure tagged side-buffer collection load owner".to_string(), - "0x004a2c80 infrastructure composition chooser owner (DT family)".to_string(), - "0x004a34e0 infrastructure composition chooser sibling (ST family)".to_string(), - "0x0048a1e0 infrastructure child attach helper".to_string(), - "0x0048dcf0 infrastructure tagged child-stream restore outer owner".to_string(), - "0x0048dd50 infrastructure child rebuild loop".to_string(), - "0x00490a3c infrastructure payload attach helper".to_string(), - "0x004559d0 infrastructure tagged string-triplet serializer".to_string(), - "0x00455870 infrastructure tagged string-triplet load companion".to_string(), - "0x00455930 infrastructure scalar-triplet serializer sibling".to_string(), - "0x00448a70 / 0x00493660 / 0x0048b660 route and world follow-on family".to_string(), - "0x004133b0 placed-structure local-runtime refresh outer owner".to_string(), - ]; - let known_owner_bridge_fields = vec![ - "outer stream prelude [u16 child count, optional saved primary-child byte]".to_string(), - "[this+0x248] cached primary-child slot".to_string(), - "[this+0x206/+0x20a/+0x20e] route-entry resolver fields".to_string(), - "[this+0x1e2/+0x1e6/+0x1ea] published anchor triplet".to_string(), - "[this+0x4b/+0x4f/+0x53] companion local triplet lane".to_string(), - "child list [this+0x75] under the Infrastructure owner".to_string(), - "non-direct live-entry directory [collection+0x3c] with 12-byte rows (payload pointer, previous live id, next live id)".to_string(), - ]; - let known_bridge_helpers = vec![ - "0x00493be0 tagged 0x38a5/0x38a6/0x38a7 collection load owner".to_string(), - "0x00491c60 tagged 0x38a5/0x38a6/0x38a7 collection serializer owner".to_string(), - "0x004a2c80/0x004a34e0 paired upstream infrastructure composition choosers with decoded DT/ST table families plus BallastCap/Overpass literals".to_string(), - "0x0048a340 infrastructure chooser selector setter for [0x226]/[0x219]/[0x251]/bit 0x20".to_string(), - "0x0048a6c0 per-entry serializer for outer child-count / primary-child prelude and child payload callbacks".to_string(), - "0x00490960 infrastructure child constructor / selector propagator".to_string(), - "0x00455a40 raw vtable slot +0x44 dispatch wrapper".to_string(), - "0x00455a50 raw vtable slot +0x40 dispatch wrapper with global bridge reset".to_string(), - "0x004559d0 tagged child payload serializer for 0x55f1/0x55f2/0x55f3".to_string(), - "0x00490200 infrastructure seeded-lane route/link comparator".to_string(), - "0x00518140 indexed_collection_resolve_live_entry_payload_pointer_by_live_id".to_string(), - "0x005181f0 indexed_collection_unlink_non_direct_live_entry".to_string(), - "0x00518260 indexed_collection_link_non_direct_live_entry".to_string(), - "0x00518380 indexed_collection_find_nth_live_entry_id".to_string(), - "0x00518680 indexed_collection_load_header_bitset_and_non_direct_tables".to_string(), - "0x005395d0 shared child-attach list owner".to_string(), - "0x00539530 shared position-lane seed helper".to_string(), - "0x0053a5b0 shared third position-lane seed helper".to_string(), - "0x0052e8b0 runtime_object_publish_companion_triplet_lane_4b_4f_53".to_string(), - "0x00530720 runtime_object_publish_anchor_triplet_and_optionally_rebind_world_cell_handle" - .to_string(), - "0x0048e140 / 0x0048e160 / 0x0048e180 route-entry resolver helpers".to_string(), - ]; - let next_owner_questions = vec![ - "With 0x00491c60, 0x0048a6c0, 0x00490960, 0x00455a40, and 0x004559d0 now grounded as the full child-construction and write-side dispatch chain for the `0x38a5/0x38a6/0x38a7` family, how do the remaining compact-prefix regimes subdivide the already-mapped save-side mode families (0x0a BallastCap, 0x0b TrackCap, 0x02 Tunnel, 0x01 Bridge, with 0x03 Overpass only grounded statically) before they surface in the seeded lanes [this+0x206/+0x20a/+0x20e], the slot +0x4c serializer, and the trailing 0x52ec50 footer path?".to_string(), - "Inside the grounded overpass/ballast branch ([this+0x226]==3) of the paired chooser siblings, when do the fixed BallastCap and Overpass literals (0x5cb138/0x5cb150 and 0x5cb168/0x5cb180) fire, and does the pure BallastCap class 0x0055/0x00 stay a boundary artifact or become a real outer prelude consumed by 0x0048dcf0?".to_string(), - "Which 0x38a5 embedded name-pair groups survive into the per-child vtable +0x40 payload callbacks dispatched through 0x00455a50?".to_string(), - "After the direct route-entry bridge helpers over [this+0x206/+0x20a/+0x20e] are grounded, which later route/local-runtime owner above 0x00448a70/0x00493660/0x0048b660 still depends on the remaining mixed exact classes once [this+0x248] is demoted to child-list cache/cleanup state?".to_string(), - ]; - let candidate_consumer_hypotheses = vec![ - SmpServiceConsumerHypothesis { - label: "infrastructure child attach/rebuild path".to_string(), - status: if side_buffer.is_some() && (bridge_like_name_pair_count > 0 - || tunnel_like_name_pair_count > 0 - || track_cap_like_name_pair_count > 0) - { - "highest_priority_static_mapping_target".to_string() - } else { - "possible_consumer_family".to_string() - }, - candidate_consumers: vec![ - "0x00493be0 infrastructure tagged side-buffer collection load owner".to_string(), - "0x0048a1e0 infrastructure child attach helper".to_string(), - "0x0048dcf0 infrastructure tagged child-stream restore outer owner".to_string(), - "0x0048dd50 infrastructure child rebuild loop".to_string(), - "0x00490a3c infrastructure payload attach helper".to_string(), - ], - evidence: vec![ - format!( - "real side-buffer name families currently count bridge/tunnel/track-cap pairs as {}/{}/{}", - bridge_like_name_pair_count, - tunnel_like_name_pair_count, - track_cap_like_name_pair_count - ), - "atlas already bounds these helpers under the literal Infrastructure owner".to_string(), - "the side-buffer corpus is disjoint from the placed-structure triplet corpus, so a separate child/rebuild family is more plausible than a compact alias".to_string(), - "direct disassembly now shows 0x00493be0 opening tag family 0x38a5/0x38a6/0x38a7, reading one shared dword into the owner-local 0x90/0x94 lane, iterating each live collection entry, and dispatching every loaded infrastructure record through 0x0048dcf0 before the later follow-on owners run".to_string(), - "direct disassembly now also shows 0x00491c60 as the serializer sibling of 0x00493be0: it writes tags 0x38a5/0x38a6/0x38a7, serializes the shared owner-local dword from [this+0x90], iterates live entries through 0x00518380/0x00518140, and dispatches each entry to 0x0048a6c0".to_string(), - "direct disassembly now also grounds paired upstream chooser siblings: 0x004a2c80 routes the DT family and 0x004a34e0 routes the ST family, with both repeatedly calling 0x0048a1e0 and branching on child type codes at [this+0x226], selector bytes at [this+0x219]/[this+0x251]/[this+0x252], bit 0x20 in [this+0x24c], and follow-on owners 0x0048a340/0x0048f4c0/0x00490200/0x00490960".to_string(), - "direct disassembly now also shows 0x0048a6c0 serializing the outer per-record prelude directly: it writes the current child count as a u16, writes the saved primary-child ordinal byte derived from [this+0x248], and then serializes each child through 0x00455a40".to_string(), - "local .rdata at 0x005cfd00 now also proves the missing write-side slot directly: for Infrastructure children, 0x00455a40 lands on vtable slot +0x44 = 0x004559d0, alongside +0x40 = 0x00455fc0, +0x48 = 0x00455870, and +0x4c = 0x00455930".to_string(), - "direct disassembly now also shows 0x004559d0 writing 0x55f1, serializing the three string lanes [this+0x206/+0x20a/+0x20e], writing 0x55f2, dispatching slot +0x4c, re-entering 0x52ec50, and then writing the closing 0x55f3 tag".to_string(), - "direct disassembly now also shows the paired chooser siblings calling 0x00490960 directly on the child-construction side, alongside 0x0048a340, 0x0048f4c0, and 0x00490200".to_string(), - "direct disassembly now also shows 0x00490960 copying selector fields into the child object ([this+0x219], [this+0x251], bit 0x20 in [this+0x24c], and [this+0x226]), allocating a fresh 0x23a Infrastructure child, seeding it through 0x00455b70 with caller-supplied stem input plus fixed literal Infrastructure at 0x005cfd74, attaching it through 0x005395d0, seeding position lanes through 0x00539530/0x0053a5b0, and optionally caching it as primary child in [this+0x248]".to_string(), - "the currently grounded direct-constructor chooser branches are narrower now too: the repeated calls at 0x004a2eba/0x004a30f9/0x004a339c feed 0x00490960 with mode arg 0x0a and stem arg 0x005cb138 = BallastCapDT_Cap.3dp, so they bypass the selector-copy block at 0x004909e2 and go straight into fresh child allocation/seeding".to_string(), - "the wider direct-calls sweep now also grounds stable 0x00490960 mode families: mode 0x0b pairs with fixed TrackCapDT/ST_Cap literals at 0x0048ed01/0x0048ed20, mode 0x03 with OverpassST_section at 0x00495a44, mode 0x02 with the decoded TunnelST/TunnelDT tables and zero-stem fallbacks across 0x004a17eb/0x004a1995/0x004a1b44/0x004a1b7d/0x004a1b95, and mode 0x01 with the decoded BridgeDT/BridgeST tables plus bridge zero-stem fallbacks across 0x004a1dae/0x004a2043/0x004a2082/0x004a221e/0x004a22a5/0x004a23aa/0x004a23eb/0x004a2409/0x004a24f6".to_string(), - "objdump on 0x00490960 now also sharpens the source-side comparison for the remaining mixed exact-prefix classes: mode lives at [esp+0x10], stem at [esp+0x14], args 3/4 at [esp+0x18]/[esp+0x1c] feed 0x539530, arg 5 at [esp+0x20] feeds 0x53a5b0, arg 10 at [esp+0x34] gates whether the new child is cached into [this+0x248], and the selector-copy block at 0x004909e2..0x00490a32 reads bytes from [esp+0x28]/[esp+0x2c]/[esp+0x30] into [this+0x219]/[this+0x251]/bit0x20 in [this+0x24c]. The fixed TrackCap mode-0x0b branches at 0x0048ed01/0x0048ed20 push literals 0x005cb198/0x005cb1ac after the same pre-seeded 1,-1,-1,0,0 flag bundle, so they reach 0x490960 with arg7/arg8/arg9 = -1/-1/0 and bypass that selector-copy block because mode >= 4. The tunnel mode-0x02 family at 0x004a17eb/0x004a1995/0x004a1b44/0x004a1b7d plus zero-stem fallback 0x004a1b95 necessarily flows through the selector-copy block because mode < 4, and the objdump caller bundles show those branches reaching 0x490960 with arg8 fixed at 1, arg9 fixed at 0, and only arg7 varying through the branch-local register (ebx/ebp) before the table or fallback stem is pushed".to_string(), - "direct disassembly now also makes that tunnel-versus-track-cap residue more exact: 0x004a17eb/0x004a1995 drive mode-0x02 through TunnelDT/TunnelST tables 0x621a94/0x621a64 with arg7 entering as a one-bit selector (0 or 1) after the local sbb/inc pair; 0x004a1b44/0x004a1b7d repeat the same one-bit arg7 pattern through sibling tables 0x621a9c/0x621a6c; and the fallback 0x004a1b95 clears both stem and selector bundle entirely. By contrast, 0x0048ed01/0x0048ed20 reach mode-0x0b with the exact same 1,-1,-1,0,0 bundle and differ only by the pushed stem literal 0x005cb198 versus 0x005cb1ac.".to_string(), - "objdump on 0x00455b70 now also makes the shared child seed strip concrete: after zeroing the same [this+0x206/+0x20a/+0x20e] lanes, it copies stack args 1/2/3 into them through 0x51d820 whenever those args are non-null, so the 0x490960 call pattern seeds [0x206] from fixed payload literal 0x005c87a8, [0x20a] from the caller stem, and [0x20e] from fixed literal 0x005cfd74 = \"Infrastructure\" before 0x004559d0 later serializes those same three lanes".to_string(), - "objdump on 0x51d820 now also shows those seeded lanes are owned heap strings, not encoded ids: it frees any prior pointer through 0x5a1145, counts the incoming NUL-terminated ASCII bytes, allocates a fresh buffer through 0x5a125d, and copies the source string byte-for-byte into the destination slot".to_string(), - "objdump on 0x52ec50 now also makes the short footer bytes literal: it serializes one byte from bit 5 of [this+0x20] and one byte from bit 6 of [this+0x20] through 0x531030, so the residual compact-prefix ambiguity still lives in how those footer bits compose with the next-record prelude rather than in the seeded name lanes themselves".to_string(), - "direct disassembly now also grounds one concrete consumer strip below those footer bits: 0x00528d90 only admits the child when the explicit caller override is set, the surrounding global override byte [owner+0x3692] is set, or bit 0x20 in [child+0x20] is set; the sibling loop at 0x00529730 only takes the later 0x530280 follow-on when bit 0x40 in [child+0x20] is set".to_string(), - "that footer-bit consumer strip is tied to a broader higher-layer owner family now too: the same 0x005295f0..0x005297b7 loop repopulates a candidate cell set through 0x00533ba0, walks candidate child lists through 0x00556ef0/0x00556f00, and honors the same controller mode byte [owner+0x3692] that the checked-in atlas already ties to the world-window presentation dispatcher. So the remaining bit-0x20 question belongs to that nearby-presentation/controller family rather than to a free-floating serializer flag".to_string(), - "the neighboring helpers tighten that owner family further: atlas-backed 0x00533ba0 is the nearby-presentation cell-table helper under the layout/presenter strip, direct disassembly shows 0x00548da0 walking list root [layout+0x2593], and direct disassembly of 0x0054bab0 mutates layout slots [layout+0x2637/+0x263b/+0x2643]. That means the 0x005295f0..0x005297b7 footer-bit consumer is sitting in layout/presentation state, not in a simulation-owned infrastructure service".to_string(), - "objdump on 0x531030/0x5a464d/0x5a44a8 now also shows the infrastructure writer is not hiding another per-owner transform there: 0x531030 just forwards the caller-supplied pointer and byte count into the generic stream backend, and 0x5a44a8 is the shared chunked stream write path keyed by the stream handle rather than an infrastructure-specific encoder".to_string(), - "that caller-matrix split now rules out one easy explanation for the mixed save-side prefixes: the shared 0xff0000ff/0x0001/0xff class cannot come from selector-copy state alone, because its dominant TrackCap rows come from mode-0x0b callers that bypass selector-copy entirely while the tunnel residue comes from mode-0x02 callers that necessarily flow through it".to_string(), - "the current grounded q.gms side-buffer name corpus now maps directly onto those constructor families too: BridgeSTWood_Section.3dp aligns with mode 0x01 Bridge, TunnelSTBrick_Cap/Section.3dp with mode 0x02 Tunnel, BallastCapST_Cap.3dp with mode 0x0a BallastCap, and TrackCapST_Cap.3dp with mode 0x0b TrackCap; only the Overpass mode-0x03 family remains static-only in the current save corpus".to_string(), - "direct disassembly now also shows 0x00490200 reading the seeded lanes [this+0x206/+0x20a/+0x20e] back through the live route collection at 0x006cfca8, classifying peer relationships with [this+0x216/+0x218/+0x201/+0x202], and therefore acting as a route/link comparator above the same child payload fields that 0x004559d0 later serializes".to_string(), - "the direct route-entry bridge is tighter now too: 0x0048e140/0x0048e160/0x0048e180 simply resolve [this+0x206/+0x20a/+0x20e] through the live route collection at 0x006cfca8 and return the pointed route-entry or null, while 0x0048e1a0 walks the first two seeded lanes, resolves each peer route, and compares [peer+0x20e]/[peer+0x201] plus conditional [peer+0x206]/[peer+0x20a] against [this+0x202] before returning a boolean match".to_string(), - "the neighboring cached-primary-child path is narrower now too: 0x0048ed30 reads [this+0x248], walks child list [this+0x08] through 0x556ef0/0x556fa0, clears [this+0x248] when it matches the current child, destroys the child through 0x455d20/0x455650/0x53b080, and tears the list down through 0x556f20/0x5570b0/0x5571d0. That makes [this+0x248] a child-list cache and cleanup lane rather than the first route-entry bridge.".to_string(), - "the chooser tables now decode to concrete asset families too: 0x621a44/0x621a54 feed BridgeST caps/sections, 0x621a64 feeds TunnelST cap/section variants, 0x621a74/0x621a84 feed BridgeDT caps/sections, and 0x621a94 feeds TunnelDT variants; fixed literals 0x5cb138/0x5cb150 are BallastCapDT/ST and 0x5cb168/0x5cb180 are OverpassDT/ST".to_string(), - "the top-level chooser branches are grounded now too: [this+0x226]==1 routes bridge families, [this+0x226]==2 routes tunnel families, [this+0x226]==3 routes overpass/ballast families, and bit 0x20 in [this+0x24c] selects the cap-oriented side over the section-oriented side inside those DT/ST siblings".to_string(), - "direct disassembly now also shows 0x0048a340 as the exact chooser-state setter: its dword argument writes [this+0x226], its next two byte arguments write [this+0x219] and [this+0x251], and its final byte argument toggles bit 0x20 in [this+0x24c]".to_string(), - "the material selectors are grounded now too: in the bridge branch, [this+0x219] indexes Steel/Stone/Wood tables directly while value 2 takes the special suspension-cap path through [this+0x252]; in the tunnel branch, [this+0x251] selects Brick versus Concrete while the cap/section split comes from bit 0x20 choosing the base versus +0x8 table entry".to_string(), - "the [this+0x252] selector is partially grounded now too: when [this+0x219]==2, the chooser jump tables dispatch fixed BridgeDT/BridgeST suspension-cap literals for R10, L10, 12, 14, 16, and 18 variants instead of using the general Bridge* table families".to_string(), - side_buffer - .and_then(|probe| probe.first_record_child_count_after_owner_shared) - .map(|child_count| { - format!( - "grounded q.gms bytes now also show the first 0x38a6 record starting immediately after that shared dword with child_count={}, saved_primary_child_byte={}, and first 0x55f1 at offset +0x{:x}", - child_count, - side_buffer - .and_then(|probe| { - probe.first_record_saved_primary_child_byte_after_owner_shared_hex - .as_deref() - }) - .unwrap_or("0x00"), - side_buffer - .and_then(|probe| { - probe.first_record_first_name_tag_relative_offset_after_owner_shared - }) - .unwrap_or_default() - ) - }) - .unwrap_or_else(|| { - "no grounded first-record prelude summary was available after the shared 0x38a6 owner dword".to_string() - }), - "direct disassembly now shows 0x00518140 resolving a non-direct live entry through the tombstone bitset and then returning the first dword of a 12-byte row from [collection+0x3c] for the 0x38a5 path".to_string(), - "direct disassembly now also shows 0x005181f0/0x00518260 treating the same 12-byte rows as a live-entry directory: dword +0 is the payload pointer, dword +4 is previous live id, and dword +8 is next live id, with collection head/tail caches alongside them".to_string(), - "direct disassembly now also shows 0x00493be0 iterating live-entry ordinals through 0x00518380(ordinal, 0), converting each ordinal to a live id, then resolving that live id through 0x00518140 before handing the resulting payload pointer to 0x0048dcf0".to_string(), - "direct disassembly now shows 0x00518680 loading the non-direct collection header, tombstone bitset, and live-id-bound-scaled 12-byte tables for the non-direct path before 0x00493be0 starts iterating".to_string(), - "direct disassembly now also shows the shared child payload callback 0x00455fc0 opening 0x55f1, parsing three len-prefixed strings through 0x531380, opening 0x55f2, seeding the child through 0x455b70, dispatching slot +0x48, and then opening 0x55f3".to_string(), - "direct disassembly now also shows 0x00455b70 storing those three payload strings into [this+0x206/+0x20a/+0x20e], defaulting the second lane through a fixed literal when absent and defaulting the third lane back to the first string when absent".to_string(), - format!( - "current save-side probe reports {} embedded 0x55f1 rows with a third decoded string", - side_buffer - .map(|probe| probe.decoded_embedded_name_row_with_tertiary_name_count) - .unwrap_or_default() - ), - format!( - "current save-side probe also reports {} complete 0x55f1/0x55f2/0x55f3 envelopes, dominant 0x55f2 chunk len 0x{:x}, and dominant 0x55f3 span 0x{:x}", - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .map(|summary| summary.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope) - .unwrap_or_default(), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.dominant_policy_chunk_len) - .unwrap_or_default(), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.dominant_profile_chunk_len) - .unwrap_or_default() - ), - "direct disassembly now also shows the post-+0x48 helper pair 0x52ebd0/0x52ec50 loading and serializing two single-byte lanes around the trailing 0x55f3 tag while folding them into bits 0x20 and 0x40 of [this+0x20]".to_string(), - format!( - "current save-side probe reports {} rows with the short 0x06-byte trailing span; dominant short flag pair is {}/{} x{}", - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()) - .map(|summary| summary.row_count_with_0x06_profile_span) - .unwrap_or_default(), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()) - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.first_flag_byte_hex.as_str()) - .unwrap_or("0x00"), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()) - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.second_flag_byte_hex.as_str()) - .unwrap_or("0x00"), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()) - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.count) - .unwrap_or_default() - ), - "direct disassembly now also shows 0x455870 consuming six 4-byte lanes from the fixed 0x55f2 chunk and forwarding them into 0x530720 then 0x52e8b0, while 0x455930 serializes the same six dword lanes back through 0x531030".to_string(), - format!( - "current save-side probe reports {} fixed 0x1a policy rows; dominant trailing word is {} x{}", - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.fixed_policy_summary.as_ref()) - .map(|summary| summary.row_count_with_0x1a_policy_chunk) - .unwrap_or_default(), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.fixed_policy_summary.as_ref()) - .and_then(|summary| summary.dominant_trailing_word_hex.as_deref()) - .unwrap_or("0x0000"), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.fixed_policy_summary.as_ref()) - .map(|summary| summary.dominant_trailing_word_count) - .unwrap_or_default() - ), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.fixed_policy_summary.as_ref()) - .and_then(|summary| { - summary.compact_prefix_correlations.iter().find(|entry| { - entry.prefix_leading_dword == 0xff00_00ff - && entry.prefix_trailing_word == 0x0001 - && entry.prefix_separator_byte == 0xff - }) - }) - .map(|correlation| { - format!( - "the fixed 0x55f2 lane for exact 0xff0000ff/0x0001/0xff is now explicit too: unique policy tuples={}, dominant mode={:?} x{}, dominant tuple=({:?} | {:?} | {:?}) x{}, and sample rows={:?}", - correlation.unique_policy_tuple_count, - correlation.dominant_mode_family, - correlation.dominant_mode_family_count, - correlation.dominant_first_triplet_dwords_hex, - correlation.dominant_second_triplet_dwords_hex, - correlation.dominant_trailing_word_hex, - correlation.dominant_policy_tuple_count, - correlation - .sample_rows - .iter() - .map(|sample| format!( - "{}:{:?}/{:?}", - sample.name_tag_relative_offset, - sample.primary_name, - sample.secondary_name - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no fixed-policy compact-prefix correlation was available for 0xff0000ff/0x0001/0xff".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.fixed_policy_summary.as_ref()) - .and_then(|summary| { - summary.compact_prefix_correlations.iter().find(|entry| { - entry.prefix_leading_dword == 0x0000_55f3 - && entry.prefix_trailing_word == 0x0001 - && entry.prefix_separator_byte == 0xff - }) - }) - .map(|correlation| { - format!( - "the fixed 0x55f2 lane for exact 0x000055f3/0x0001/0xff is now explicit too: unique policy tuples={}, dominant mode={:?} x{}, dominant tuple=({:?} | {:?} | {:?}) x{}, and sample rows={:?}", - correlation.unique_policy_tuple_count, - correlation.dominant_mode_family, - correlation.dominant_mode_family_count, - correlation.dominant_first_triplet_dwords_hex, - correlation.dominant_second_triplet_dwords_hex, - correlation.dominant_trailing_word_hex, - correlation.dominant_policy_tuple_count, - correlation - .sample_rows - .iter() - .map(|sample| format!( - "{}:{:?}/{:?}", - sample.name_tag_relative_offset, - sample.primary_name, - sample.secondary_name - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no fixed-policy compact-prefix correlation was available for 0x000055f3/0x0001/0xff".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.dominant_profile_span_class_summary.as_ref()) - .map(|summary| { - format!( - "the dominant 0x{:x}-byte post-profile class is now narrowed too: dominant name pair is {:?}/{:?} x{}, dominant compact prefix is {}/{}/{} x{}, and dominant prelude candidate is {}/{} x{} across {} rows", - summary.profile_chunk_len_to_next_name_or_end, - summary.dominant_primary_name, - summary.dominant_secondary_name, - summary.dominant_name_pair_count, - summary - .dominant_prefix_leading_dword_hex - .as_deref() - .unwrap_or("0x00000000"), - summary - .dominant_prefix_trailing_word_hex - .as_deref() - .unwrap_or("0x0000"), - summary - .dominant_prefix_separator_byte_hex - .as_deref() - .unwrap_or("0x00"), - summary.dominant_prefix_count, - summary - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.child_count_candidate_hex.as_str()) - .unwrap_or("0x0000"), - summary - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) - .unwrap_or("0x00"), - summary - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.count) - .unwrap_or_default(), - summary.row_count - ) - }) - .unwrap_or_else(|| { - "no dominant post-profile class summary was available for the embedded 0x55f3 spans".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.dominant_profile_span_class_summary.as_ref()) - .map(|summary| { - format!( - "the dominant post-profile outliers are now explicit too: name-pair counts={:?}, compact-prefix counts={:?}, candidate-pattern counts={:?}", - summary - .name_pair_summaries - .iter() - .map(|entry| format!( - "{:?}/{:?}:{}", - entry.primary_name, entry.secondary_name, entry.count - )) - .collect::>(), - summary - .compact_prefix_pattern_summaries - .iter() - .map(|entry| format!( - "{}/{}/{}:{}", - entry.prefix_leading_dword_hex, - entry.prefix_trailing_word_hex, - entry.prefix_separator_byte_hex, - entry.count - )) - .collect::>(), - summary - .candidate_pattern_summaries - .iter() - .map(|entry| format!( - "{}/{}:{}", - entry.child_count_candidate_hex, - entry.saved_primary_child_byte_candidate_hex, - entry.count - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no dominant post-profile outlier breakdown was available".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .map(|summary| { - format!( - "candidate-pattern correlations now split the remaining prelude classes cleanly too: {:?}", - summary - .candidate_pattern_correlations - .iter() - .map(|entry| format!( - "{}/{} rows={} dominant-name={:?}/{:?} x{} dominant-prev-span={:?} x{}", - entry.child_count_candidate_hex, - entry.saved_primary_child_byte_candidate_hex, - entry.row_count, - entry.dominant_primary_name, - entry.dominant_secondary_name, - entry.dominant_name_pair_count, - entry.dominant_profile_span, - entry.dominant_profile_span_count - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no candidate-pattern correlation summary was available".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .map(|summary| { - format!( - "mode-family correlations now also split the candidate patterns directly: {:?}", - summary - .candidate_pattern_correlations - .iter() - .map(|entry| format!( - "{}/{} rows={} dominant-mode={:?} x{} mode-counts={:?}", - entry.child_count_candidate_hex, - entry.saved_primary_child_byte_candidate_hex, - entry.row_count, - entry.dominant_mode_family, - entry.dominant_mode_family_count, - entry - .mode_family_counts - .iter() - .map(|mode| format!( - "{}:{}", - mode.mode_family, mode.count - )) - .collect::>() - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no mode-family correlation summary was available for the prelude candidates".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .map(|summary| { - format!( - "profile-span mode-family correlations now also split the previous 0x55f3 spans directly: {:?}", - summary - .profile_span_correlations - .iter() - .map(|entry| format!( - "span=0x{:x} rows={} dominant-mode={:?} x{} mode-counts={:?}", - entry.previous_profile_chunk_len_to_next_name_or_end, - entry.row_count, - entry.dominant_mode_family, - entry.dominant_mode_family_count, - entry - .mode_family_counts - .iter() - .map(|mode| format!( - "{}:{}", - mode.mode_family, mode.count - )) - .collect::>() - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no profile-span mode-family correlation summary was available".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .map(|summary| { - format!( - "exact compact-prefix correlations now split the residual prelude classes directly: {:?}", - summary - .compact_prefix_correlations - .iter() - .map(|entry| format!( - "{}/{}/{} rows={} dominant-name={:?}/{:?} x{} dominant-span={:?} x{} dominant-candidate={:?} dominant-mode={:?} x{}", - entry.prefix_leading_dword_hex, - entry.prefix_trailing_word_hex, - entry.prefix_separator_byte_hex, - entry.row_count, - entry.dominant_primary_name, - entry.dominant_secondary_name, - entry.dominant_name_pair_count, - entry.dominant_profile_span, - entry.dominant_profile_span_count, - entry - .dominant_candidate_pattern - .as_ref() - .map(|pattern| format!( - "{}/{}:{}", - pattern.child_count_candidate_hex, - pattern.saved_primary_child_byte_candidate_hex, - pattern.count - )), - entry.dominant_mode_family, - entry.dominant_mode_family_count - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no compact-prefix correlation summary was available for the prelude candidates".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .and_then(|summary| { - summary.candidate_pattern_correlations.iter().find(|entry| { - entry.child_count_candidate == 2 - && entry.saved_primary_child_byte_candidate == 0xff - && entry.dominant_primary_name.as_deref() - == Some("BridgeSTWood_Section.3dp") - && entry.dominant_secondary_name.as_deref() - == Some("Infrastructure") - }) - }) - .map(|correlation| { - format!( - "the bridge-only two-child class is now grounded save-side too: candidate pattern {}/{} spans {} rows, stays pure {:?}/{:?}, and the dominant prior profile span is {:?} x{}", - correlation.child_count_candidate_hex, - correlation.saved_primary_child_byte_candidate_hex, - correlation.row_count, - correlation.dominant_primary_name, - correlation.dominant_secondary_name, - correlation.dominant_profile_span, - correlation.dominant_profile_span_count - ) - }) - .unwrap_or_else(|| { - "no grounded pure bridge-only two-child candidate class was available in the prelude correlations".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .and_then(|summary| { - summary - .profile_span_correlations - .iter() - .find(|row| row.previous_profile_chunk_len_to_next_name_or_end == 3) - }) - .map(|correlation| { - format!( - "current save-side probe now also shows the short 0x03-byte post-profile gaps collapsing cleanly to the next-record prelude: dominant candidate pattern is {}/{} x{} across {} rows, mode counts={:?}, prefix counts={:?}", - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.child_count_candidate_hex.as_str()) - .unwrap_or("0x0000"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) - .unwrap_or("0x00"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.count) - .unwrap_or_default(), - correlation.row_count, - correlation - .mode_family_counts - .iter() - .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) - .collect::>(), - correlation - .compact_prefix_pattern_summaries - .iter() - .map(|prefix| format!( - "{}/{}/{}:{}", - prefix.prefix_leading_dword_hex, - prefix.prefix_trailing_word_hex, - prefix.prefix_separator_byte_hex, - prefix.count - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no grounded 0x03-byte post-profile span correlation was available for the prelude candidates".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .and_then(|summary| { - summary.compact_prefix_correlations.iter().find(|entry| { - entry.prefix_leading_dword == 0xff00_00ff - && entry.prefix_trailing_word == 0x0001 - && entry.prefix_separator_byte == 0xff - }) - }) - .map(|correlation| { - format!( - "the exact 0xff0000ff/0x0001/0xff compact-prefix class is now explicit: dominant name={:?}/{:?} x{}, dominant span={:?} x{}, dominant prelude={}/{} x{}, mode counts={:?}, name-pair counts={:?}, span counts={:?}, previous short-flag pairs={:?} across {} rows, and sample rows={:?}", - correlation.dominant_primary_name, - correlation.dominant_secondary_name, - correlation.dominant_name_pair_count, - correlation.dominant_profile_span, - correlation.dominant_profile_span_count, - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.child_count_candidate_hex.as_str()) - .unwrap_or("0x0000"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) - .unwrap_or("0x00"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.count) - .unwrap_or_default(), - correlation - .mode_family_counts - .iter() - .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) - .collect::>(), - correlation - .name_pair_summaries - .iter() - .map(|entry| format!( - "{:?}/{:?}:{}", - entry.primary_name, entry.secondary_name, entry.count - )) - .collect::>(), - correlation - .profile_span_counts - .iter() - .map(|entry| format!( - "0x{:x}:{}", - entry.previous_profile_chunk_len_to_next_name_or_end, - entry.count - )) - .collect::>(), - correlation - .previous_short_profile_flag_pair_counts - .iter() - .map(|entry| format!( - "{}/{}:{}", - entry.first_flag_byte_hex, - entry.second_flag_byte_hex, - entry.count - )) - .collect::>(), - correlation.rows_with_previous_short_profile_flag_pair, - correlation - .sample_rows - .iter() - .map(|sample| format!( - "{}:{:?}/{:?}", - sample.name_tag_relative_offset, - sample.primary_name, - sample.secondary_name - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no grounded 0xff0000ff/0x0001/0xff compact-prefix correlation was available".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .and_then(|summary| { - summary.compact_prefix_correlations.iter().find(|entry| { - entry.prefix_leading_dword == 0x0000_55f3 - && entry.prefix_trailing_word == 0x0001 - && entry.prefix_separator_byte == 0xff - }) - }) - .map(|correlation| { - format!( - "the exact 0x000055f3/0x0001/0xff compact-prefix class is now explicit too: dominant name={:?}/{:?} x{}, dominant span={:?} x{}, dominant prelude={}/{} x{}, mode counts={:?}, name-pair counts={:?}, previous short-flag pairs={:?} across {} rows, and sample rows={:?}", - correlation.dominant_primary_name, - correlation.dominant_secondary_name, - correlation.dominant_name_pair_count, - correlation.dominant_profile_span, - correlation.dominant_profile_span_count, - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.child_count_candidate_hex.as_str()) - .unwrap_or("0x0000"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) - .unwrap_or("0x00"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.count) - .unwrap_or_default(), - correlation - .mode_family_counts - .iter() - .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) - .collect::>(), - correlation - .name_pair_summaries - .iter() - .map(|entry| format!( - "{:?}/{:?}:{}", - entry.primary_name, entry.secondary_name, entry.count - )) - .collect::>(), - correlation - .previous_short_profile_flag_pair_counts - .iter() - .map(|entry| format!( - "{}/{}:{}", - entry.first_flag_byte_hex, - entry.second_flag_byte_hex, - entry.count - )) - .collect::>(), - correlation.rows_with_previous_short_profile_flag_pair, - correlation - .sample_rows - .iter() - .map(|sample| format!( - "{}:{:?}/{:?}", - sample.name_tag_relative_offset, - sample.primary_name, - sample.secondary_name - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no grounded 0x000055f3/0x0001/0xff compact-prefix correlation was available".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .and_then(|summary| { - summary.compact_prefix_correlations.iter().find(|entry| { - entry.prefix_leading_dword == 0xff00_00ff - && entry.prefix_trailing_word == 0x0002 - && entry.prefix_separator_byte == 0xff - }) - }) - .map(|correlation| { - format!( - "the exact 0xff0000ff/0x0002/0xff compact-prefix class is now explicit too: dominant name={:?}/{:?} x{}, dominant span={:?} x{}, dominant prelude={}/{} x{}, mode counts={:?}, span counts={:?}, and sample rows={:?}", - correlation.dominant_primary_name, - correlation.dominant_secondary_name, - correlation.dominant_name_pair_count, - correlation.dominant_profile_span, - correlation.dominant_profile_span_count, - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.child_count_candidate_hex.as_str()) - .unwrap_or("0x0000"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) - .unwrap_or("0x00"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.count) - .unwrap_or_default(), - correlation - .mode_family_counts - .iter() - .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) - .collect::>(), - correlation - .profile_span_counts - .iter() - .map(|entry| format!( - "0x{:x}:{}", - entry.previous_profile_chunk_len_to_next_name_or_end, - entry.count - )) - .collect::>(), - correlation - .sample_rows - .iter() - .map(|sample| format!( - "{}:{:?}/{:?}", - sample.name_tag_relative_offset, - sample.primary_name, - sample.secondary_name - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no grounded 0xff0000ff/0x0002/0xff compact-prefix correlation was available".to_string() - }), - "cross-save q/p corpus checks now sharpen the mixed exact classes further without changing their identity: 0x000055f3/0x0001/0xff stays on prelude 0x0001/0xff with fixed short-flag pair 0x01/0x00 and fixed post-profile span 0x03 in both saves, while 0xff0000ff/0x0001/0xff stays on prelude 0x0001/0xff with fixed short-flag pair 0x00/0x00 and widely scattered post-profile spans in both saves".to_string(), - "that cross-save split keeps the semantic mix stable too: 0x000055f3/0x0001/0xff remains tunnel-dominant with only a small TrackCap residue, while 0xff0000ff/0x0001/0xff remains TrackCap-dominant with the same small tunnel-cap/section residue. So the next infrastructure question is no longer broad class identity; it is what the two short-flag families mean and why a minority of tunnel rows take the sparse 0xff0000ff outlier class instead of the stable span-0x03 tunnel family".to_string(), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .and_then(|summary| { - summary - .profile_span_correlations - .iter() - .find(|row| row.previous_profile_chunk_len_to_next_name_or_end == 0x27) - }) - .map(|correlation| { - format!( - "the sparse 0x27 post-profile outlier is now explicit too: mode counts={:?}, prefix counts={:?}, sample rows={:?}", - correlation - .mode_family_counts - .iter() - .map(|mode| format!("{}:{}", mode.mode_family, mode.count)) - .collect::>(), - correlation - .compact_prefix_pattern_summaries - .iter() - .map(|prefix| format!( - "{}/{}/{}:{}", - prefix.prefix_leading_dword_hex, - prefix.prefix_trailing_word_hex, - prefix.prefix_separator_byte_hex, - prefix.count - )) - .collect::>(), - correlation - .sample_rows - .iter() - .map(|sample| format!( - "{}:{:?}/{:?}@{}/{}/{}", - sample.name_tag_relative_offset, - sample.primary_name, - sample.secondary_name, - sample.prefix_leading_dword_hex, - sample.prefix_trailing_word_hex, - sample.prefix_separator_byte_hex - )) - .collect::>() - ) - }) - .unwrap_or_else(|| { - "no grounded 0x27 post-profile outlier correlation was available for the prelude candidates".to_string() - }), - side_buffer - .and_then(|probe| probe.payload_envelope_summary.as_ref()) - .and_then(|summary| summary.name_prelude_candidate_summary.as_ref()) - .and_then(|summary| { - summary - .profile_span_correlations - .iter() - .find(|row| row.previous_profile_chunk_len_to_next_name_or_end == 0) - }) - .map(|correlation| { - format!( - "the zero-length post-profile class is now a separate grounded outlier: dominant candidate pattern is {}/{} x{} across {} rows", - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.child_count_candidate_hex.as_str()) - .unwrap_or("0x0000"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.saved_primary_child_byte_candidate_hex.as_str()) - .unwrap_or("0x00"), - correlation - .dominant_candidate_pattern - .as_ref() - .map(|pattern| pattern.count) - .unwrap_or_default(), - correlation.row_count - ) - }) - .unwrap_or_else(|| { - "no grounded zero-length post-profile span correlation was available for the prelude candidates".to_string() - }), - "direct disassembly now also shows 0x530720 publishing the first fixed-triplet lane into [this+0x1e2/+0x1e6/+0x1ea], while 0x52e8b0 publishes the second fixed-triplet lane into [this+0x4b/+0x4f/+0x53] and sets bit 0x02".to_string(), - "direct disassembly now also shows the outer owner at 0x0048dcf0 reading one u16 child count through 0x531150 into the stream prelude, zeroing [this+0x08], and conditionally reading one saved primary-child byte before the per-child callback loop runs".to_string(), - side_buffer - .and_then(|probe| probe.live_entry_prelude_summary.as_ref()) - .map(|summary| { - format!( - "the widened save-side probe now also decodes {} live-entry payload starts inside the records span; dominant child count={} x{}, dominant saved primary-child byte={} x{}, and {} payloads reach the first 0x55f1 child callback at offset +0x3", - summary.rows_with_payload_pointer_inside_records_span, - summary.dominant_child_count.unwrap_or_default(), - summary.dominant_child_count_count, - summary - .dominant_saved_primary_child_byte_hex - .as_deref() - .unwrap_or("0x00"), - summary.dominant_saved_primary_child_byte_count, - summary.rows_with_first_name_tag_at_offset_3 - ) - }) - .unwrap_or_else(|| { - "no live-entry payload-start summary was available for the outer prelude".to_string() - }), - "local .rdata at 0x005cfd00 now also proves the infrastructure child table uses the shared tagged callback strip directly: slot +0x40 = 0x455fc0, slot +0x44 = 0x4559d0, slot +0x48 = 0x455870, and slot +0x4c = 0x455930".to_string(), - "direct disassembly now shows 0x0048a1e0 cloning the first child triplet bands through 0x52e880/0x52e720, destroying the prior child, seeding a new literal Infrastructure child through 0x455b70 with payload seed 0x5c87a8, attaching through 0x5395d0 or 0x53a5d0, and republishing the two bands through 0x52e8b0/0x530720".to_string(), - "direct disassembly now also shows the outer owner at 0x0048dcf0 reading a child count plus optional primary-child ordinal from the tagged stream through 0x531150, zeroing [this+0x08], dispatching each fresh child through 0x455a50 -> vtable slot +0x40, culling ordinals above 5, and restoring cached primary-child slot [this+0x248] from the saved ordinal".to_string(), - "the smaller attach primitive 0x00490a3c no longer looks like the semantic fork by itself: it just allocates one literal Infrastructure child, seeds it through 0x455b70 with caller-provided stem input, attaches it through 0x5395d0, seeds position lanes through 0x539530/0x53a5b0, and optionally caches it as primary child".to_string(), - ], - blockers: vec![ - "how the remaining mixed exact compact-prefix classes map back onto constructor semantics now that the whole prelude corpus is split directly: 0xff0000ff/0x0002/0xff is pure bridge, 0xff000000/{0x0001,0x0002}/0xff are pure bridge, 0xf3010100/0x0055/0x00 is pure ballast-cap, and 0x0005d368/0x0001/0xff is pure track-cap, leaving only 0xff0000ff/0x0001/0xff and 0x000055f3/0x0001/0xff as the mixed residual classes".to_string(), - "how the payload streams reached through 0x00518380 -> 0x00518140 align with the embedded 0x55f1 name-pair groups and compact-prefix regimes surfaced by the save-side probe".to_string(), - "how the observed 0x55f3-to-next-0x55f1 gaps partition between the two 0x52ebd0 flag bytes and the next-record prelude now that 0x0048a6c0 is grounded as the writer for the outer child-count / primary-child prelude".to_string(), - "which fields written through the grounded 0x00490960 -> 0x004559d0 -> slot +0x4c -> 0x52ec50 chain retain the 0x38a5 embedded name-pair semantics before route/local-runtime follow-ons take over".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "infrastructure serializer/load companion path".to_string(), - status: if side_buffer.is_some() { - "strong_static_mapping_candidate".to_string() - } else { - "possible_consumer_family".to_string() - }, - candidate_consumers: vec![ - "0x004559d0 infrastructure tagged string-triplet serializer".to_string(), - "0x00455870 infrastructure tagged string-triplet load companion".to_string(), - "0x00455930 infrastructure scalar-triplet serializer sibling".to_string(), - ], - evidence: vec![ - "atlas already bounds the serializer/load strip around the Infrastructure owner and the same 0x55f1/0x55f2/0x55f3 tag family".to_string(), - "local .rdata at 0x005cfd00 now proves the infrastructure child vtable points straight at 0x455fc0/0x4559d0/0x455870/0x455930 for the load, tagged serializer, triplet-restore, and scalar serializer slots".to_string(), - "the save-side side-buffer carries embedded dual-name rows plus compact prefixes, which is compatible with a serializer-side bridge".to_string(), - ], - blockers: vec![ - "which exact 0x38a5 rows belong to shared 0x55f1/0x55f2/0x55f3 child records versus outer collection metadata, now that the concrete write-side slot +0x44 serializer is grounded as 0x004559d0".to_string(), - ], - }, - SmpServiceConsumerHypothesis { - label: "route/local-runtime follow-on path".to_string(), - status: if side_buffer.is_some() { - "secondary_candidate_after_attach_rebuild".to_string() - } else { - "possible_consumer_family".to_string() - }, - candidate_consumers: vec![ - "0x00448a70 / 0x00493660 / 0x0048b660 route and world follow-on family".to_string(), - "0x004133b0 placed-structure local-runtime refresh outer owner".to_string(), - ], - evidence: vec![ - "atlas ties the Infrastructure rebuild loop to later route-side and local-runtime follow-on owners".to_string(), - "current side-buffer trace shows separate infrastructure state and the direct seeded-lane bridge is now grounded before these later owners run".to_string(), - "direct disassembly now shows 0x00448a70 as a world-overlay byte write helper over [world+0x15e1/+0x162d], with the neighboring 0x00448af0 reading three world bitsets at [world+0x2139/+0x213d/+0x2141] rather than any infrastructure child fields".to_string(), - "direct disassembly now shows 0x00493660 as a counter-and-follow-on owner over one infrastructure child: it updates local counters by [child+0x218], [child+0x226], and [child+0x44], optionally resolves a peer through 0x48dcb0 and 0x426c20, then maps one world-raster byte back into the companion region collection 0x006cfc9c and calls 0x487960".to_string(), - "direct disassembly now shows 0x0048b660 as a presentation-color/style owner: it gates on global shell state, then branches on [child+0x216], [child+0x218], [child+0x226], [child+0x44], and bit 0x40 in [child+0x201] before publishing fixed RGBA tuples through 0x53a350".to_string(), - "the remaining local-runtime helpers are tighter now too: 0x0048e2c0 flips bit 0x20 in [child+0x201] and, when enabling that bit, reruns 0x53a3a0 plus 0x48a9e0; 0x0048e330 similarly flips bit 0x40 in [child+0x201] and tail-calls 0x48b660; and 0x0048e3c0 updates [child+0x22e] through the auxiliary route tracker 0x006cfcb4 after resolving one route entry via [child+0x212], then scans class-0 regions through 0x455800/0x455810 and 0x51dbb0 before setting bit 0x02 in [child+0x24c] only when the region test fails".to_string(), - ], - blockers: vec![ - "which mixed exact compact-prefix classes still survive into these later owners after the seeded-lane bridge and earlier child-stream restore semantics are accounted for".to_string(), - "no direct save-side correlation yet between the remaining mixed exact classes and the later 0x00493660 counter buckets, 0x0048b660 style branches, or the 0x0048e2c0/0x0048e3c0 flag-and-region follow-ons".to_string(), - ], - }, - ]; - let branches = vec![ - build_service_trace_branch_status( - "infrastructure_asset_owner_seam", - if side_buffer.is_some() { - "grounded_separate_owner_seam" - } else { - "blocked_missing_side_buffer_owner_seam" - }, - &[ - "0x38a5/0x38a6/0x38a7 tagged family", - "embedded 0x55f1 dual-name rows", - "compact 6-byte prefix regimes", - ], - if side_buffer.is_some() { - &[] - } else { - &["0x38a5 owner seam"] - }, - &[ - "0x00493be0 infrastructure tagged side-buffer collection load owner", - "0x0048dcf0 infrastructure tagged child-stream restore outer owner", - ], - &[ - "This seam should be treated as infrastructure-asset state rather than as a compact alias of placed-structure triplets.", - ], - ), - build_service_trace_branch_status( - "placed_structure_triplet_alias", - if alignment.is_some_and(|probe| probe.overlapping_name_pair_count == 0) { - "disproved_by_grounded_probe" - } else { - "unresolved" - }, - &[ - "0x36b1 placed-structure triplet corpus", - "0x38a5 side-buffer name-pair corpus", - ], - &[], - &[], - &[ - "Grounded q.gms evidence currently shows zero overlap between the side-buffer name-pair corpus and the placed-structure triplet name-pair corpus.", - ], - ), - build_service_trace_branch_status( - "city_connection_consumer_mapping", - "blocked_missing_infrastructure_asset_consumer_mapping", - &[ - "grounded 0x38a5 owner seam", - "placed-structure triplet seam", - ], - &[ - "higher-layer consumer dispatch mapping", - "compact prefix regime semantics", - ], - &[ - "0x0048a1e0 infrastructure child attach helper", - "0x0048dd50 infrastructure child rebuild loop", - "0x00490a3c infrastructure payload attach helper", - ], - &[ - "The remaining problem is how higher-layer service code consumes this separate seam, not whether the seam exists.", - ], - ), - build_service_trace_branch_status( - "linked_transit_consumer_mapping", - "blocked_missing_infrastructure_asset_consumer_mapping", - &["grounded 0x38a5 owner seam", "company linked-transit latch"], - &[ - "side-buffer consumer mapping", - "route or roster rebuild owner path", - ], - &[ - "0x00448a70 / 0x00493660 / 0x0048b660 route and world follow-on family", - "0x004133b0 placed-structure local-runtime refresh outer owner", - ], - &[ - "The next slice should target the consumer path above the side-buffer seam rather than another raw save scan.", - ], - ), - ]; - let notes = vec![ - "Infrastructure asset trace now makes the side-buffer-versus-triplet split explicit: owner seam identity is grounded, the pure bridge-only 0x0002/0xff candidate class is grounded save-side, the upstream chooser above the child attach path is grounded as paired DT/ST siblings at 0x004a2c80 and 0x004a34e0 with decoded Bridge/Tunnel/BallastCap/Overpass families, grounded top-level branch meaning, grounded bridge/tunnel material selector roles, a concrete child-construction/write-side chain through 0x00490960, 0x00491c60, 0x0048a6c0, 0x00455a40, and 0x004559d0, and stable 0x00490960 mode families for BallastCap, TrackCap, Overpass, Tunnel, and Bridge branches. The current save-side name corpus already maps BallastCap, TrackCap, Tunnel, and Bridge rows onto those families directly, the candidate-pattern correlation narrows the dominant mixed 0x0001/0xff class to bridge:62 / track_cap:21 / tunnel:19, and the exact compact-prefix correlation now splits the full prelude corpus into mostly pure classes: 0xff0000ff/0x0002/0xff is pure bridge, 0xff000000/{0x0001,0x0002}/0xff are pure bridge, 0xf3010100/0x0055/0x00 is pure ballast-cap, and 0x0005d368/0x0001/0xff is pure track-cap, leaving only 0xff0000ff/0x0001/0xff and 0x000055f3/0x0001/0xff as the mixed residual classes.".to_string(), - "Cross-save q/p traces now also split those two mixed residual classes by footer and span behavior: 0x000055f3/0x0001/0xff always carries short-flag pair 0x01/0x00 on a fixed span-0x03 tunnel-dominant family, while 0xff0000ff/0x0001/0xff always carries short-flag pair 0x00/0x00 on the scattered-span TrackCap-dominant outlier family. The remaining unknown is therefore the meaning of those short-flag families and the sparse branch that routes a minority of tunnel rows into the 0xff0000ff outlier class.".to_string(), - "Direct consumers of those footer bits are grounded now too: bit 0x20 of [child+0x20] is the admission gate into the 0x00528d90 branch when no caller/global override is present, while bit 0x40 only feeds the later 0x00529730 -> 0x530280 follow-on. Since both mixed residual classes keep the second footer byte at zero in q/p, the remaining split is now specifically the first footer byte / bit-0x20 gate rather than both footer bytes.".to_string(), - "That bit-0x20 gate is no longer floating without context either: the 0x005295f0..0x005297b7 consumer strip repopulates candidate cells through 0x00533ba0, walks child lists through 0x00556ef0/0x00556f00, and honors the same controller mode byte [owner+0x3692] that the atlas already places under the world-window presentation dispatcher. The next infrastructure pass should therefore treat the remaining bit-0x20 question as a nearby-presentation/controller owner problem, not as a serializer-only problem.".to_string(), - "That owner family is layout-state specific now too: the surrounding helpers sit on atlas-backed layout/presenter roots, with 0x00548da0 walking layout list root [layout+0x2593] and 0x0054bab0 mutating layout slots [layout+0x2637/+0x263b/+0x2643]. So the remaining bit-0x20 split is increasingly a layout/presentation admission question above the infrastructure seam, not a simulation-owned route or rebuild question.".to_string(), - if st_only_name_pair_corpus { - "The current save-side side-buffer corpus is ST-only, so this trace directly exercises the ST chooser sibling while the DT sibling remains grounded statically but unexercised in this save.".to_string() - } else { - "The current save-side side-buffer corpus is not ST-only, so both chooser siblings or a DT-facing save-side class may be in play.".to_string() - }, - ]; - SmpInfrastructureAssetTraceReport { - profile_family: analysis.profile_family.clone(), - placed_structure_collection_header_present: analysis - .placed_structure_collection_header - .is_some(), - placed_structure_record_triplet_count: analysis - .placed_structure_record_triplets - .as_ref() - .map(|probe| probe.record_count) - .unwrap_or_default(), - side_buffer_present: side_buffer.is_some(), - side_buffer_decoded_embedded_name_row_count: side_buffer - .map(|probe| probe.decoded_embedded_name_row_count) - .unwrap_or_default(), - side_buffer_unique_name_pair_count: side_buffer - .map(|probe| probe.unique_embedded_name_pair_count) - .unwrap_or_default(), - bridge_like_name_pair_count, - tunnel_like_name_pair_count, - track_cap_like_name_pair_count, - triplet_alignment_overlap_count: alignment - .map(|probe| probe.overlapping_name_pair_count) - .unwrap_or_default(), - atlas_candidate_consumers, - known_owner_bridge_fields, - known_bridge_helpers, - next_owner_questions, - candidate_consumer_hypotheses, - branches, - notes, - } -} - -pub fn inspect_smp_bytes(bytes: &[u8]) -> SmpInspectionReport { - inspect_bundle_bytes(bytes, None) -} - -fn derive_loaded_placed_structure_collection_from_probe( - probe: &SmpSavePlacedStructureRecordTripletProbe, -) -> SmpLoadedPlacedStructureCollection { - SmpLoadedPlacedStructureCollection { - source_kind: probe.source_kind.clone(), - semantic_family: "scenario-save-placed-structure-triplet-collection".to_string(), - observed_entry_count: probe.record_count, - entries: probe - .entries - .iter() - .map(|entry| SmpLoadedPlacedStructureEntry { - record_index: entry.record_index, - primary_name: entry.primary_name.clone(), - secondary_name: entry.secondary_name.clone(), - policy_trailing_word: entry.policy_trailing_word, - policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(), - profile_payload_dword: entry.profile_payload_dword, - profile_payload_dword_hex: entry.profile_payload_dword_hex.clone(), - profile_status_kind: entry.profile_status_kind.clone(), - farm_growth_stage_index: entry.farm_growth_stage_index, - profile_companion_byte_u8: entry.profile_companion_byte_u8, - profile_companion_byte_hex: entry.profile_companion_byte_hex.clone(), - }) - .collect(), - } -} - -fn derive_loaded_region_collection_from_probe( - probe: &SmpSaveRegionRecordTripletProbe, -) -> SmpLoadedRegionCollection { - SmpLoadedRegionCollection { - source_kind: probe.source_kind.clone(), - semantic_family: "scenario-save-region-triplet-collection".to_string(), - observed_entry_count: probe.record_count, - entries: probe - .entries - .iter() - .map(|entry| SmpLoadedRegionEntry { - record_index: entry.record_index, - name: entry.name.clone(), - pre_name_prefix_len: entry.pre_name_prefix_len, - policy_leading_f32_0: entry.policy_leading_f32_0, - policy_leading_f32_1: entry.policy_leading_f32_1, - policy_leading_f32_2: entry.policy_leading_f32_2, - policy_reserved_dwords: entry.policy_reserved_dwords.clone(), - policy_trailing_word: entry.policy_trailing_word, - policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(), - profile_collection: entry.profile_collection.as_ref().map(|collection| { - SmpLoadedRegionProfileCollection { - direct_collection_flag: collection.direct_collection_flag, - entry_stride: collection.entry_stride, - live_id_bound: collection.live_id_bound, - live_record_count: collection.live_record_count, - trailing_padding_len: collection.trailing_padding_len, - entries: collection - .entries - .iter() - .map(|entry| SmpLoadedRegionProfileEntry { - entry_index: entry.entry_index, - name: entry.name.clone(), - trailing_weight_f32: entry.trailing_weight_f32, - }) - .collect(), - } - }), - }) - .collect(), - } -} - -fn derive_loaded_region_fixed_row_run_summary_from_probe( - probe: &SmpSaveRegionFixedRowRunCandidateProbe, -) -> SmpLoadedRegionFixedRowRunSummary { - SmpLoadedRegionFixedRowRunSummary { - source_kind: probe.source_kind.clone(), - semantic_family: "scenario-save-region-fixed-row-run-summary".to_string(), - target_row_count: probe.target_row_count, - target_row_stride: probe.target_row_stride, - target_row_stride_hex: probe.target_row_stride_hex.clone(), - candidates: probe.candidates.clone(), - } -} - -fn derive_loaded_placed_structure_dynamic_side_buffer_summary( - probe: &SmpSavePlacedStructureDynamicSideBufferProbe, - alignment: Option<&SmpSavePlacedStructureDynamicSideBufferAlignmentProbe>, -) -> SmpLoadedPlacedStructureDynamicSideBufferSummary { - SmpLoadedPlacedStructureDynamicSideBufferSummary { - source_kind: probe.source_kind.clone(), - semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-summary".to_string(), - observed_entry_count: probe.live_record_count, - owner_shared_dword_hex: probe.owner_shared_dword_hex.clone(), - unique_embedded_name_pair_count: probe.unique_embedded_name_pair_count, - decoded_embedded_name_row_count: probe.decoded_embedded_name_row_count, - first_prefix_leading_dword_hex: probe.prefix_leading_dword_hex.clone(), - first_prefix_trailing_word_hex: probe.prefix_trailing_word_hex.clone(), - first_prefix_separator_byte_hex: probe.prefix_separator_byte_hex.clone(), - triplet_alignment_overlap_count: alignment - .map(|alignment| alignment.overlapping_name_pair_count) - .unwrap_or_default(), - triplet_alignment_side_buffer_only_name_pair_count: alignment - .map(|alignment| { - alignment - .unique_side_buffer_name_pair_count - .saturating_sub(alignment.overlapping_name_pair_count) - }) - .unwrap_or_default(), - compact_prefix_pattern_summaries: probe.compact_prefix_pattern_summaries.clone(), - name_pair_summaries: probe.name_pair_summaries.clone(), - } -} - -pub fn load_save_slice_file(path: &Path) -> Result> { - let inspection = inspect_smp_file(path)?; - load_save_slice_from_report(&inspection) - .map_err(|err| -> Box { err.into() }) -} - -pub fn load_save_slice_from_report( - report: &SmpInspectionReport, -) -> Result { - let summary = report - .save_load_summary - .as_ref() - .ok_or_else(|| "inspection did not expose a recognizable save-load summary".to_string())?; - let profile = if let Some(probe) = &report.classic_rehydrate_profile_probe { - Some(SmpLoadedProfile { - profile_kind: "classic-rehydrate-profile".to_string(), - profile_family: probe.profile_family.clone(), - packed_profile_offset: probe.packed_profile_offset, - packed_profile_len: probe.packed_profile_len, - packed_profile_len_hex: probe.packed_profile_len_hex.clone(), - leading_word_0: probe.packed_profile_block.leading_word_0, - leading_word_0_hex: probe.packed_profile_block.leading_word_0_hex.clone(), - header_flag_word_3: None, - header_flag_word_3_hex: None, - map_path: probe.packed_profile_block.map_path.clone(), - display_name: probe.packed_profile_block.display_name.clone(), - profile_byte_0x77: probe.packed_profile_block.profile_byte_0x77, - profile_byte_0x77_hex: probe.packed_profile_block.profile_byte_0x77_hex.clone(), - profile_byte_0x82: probe.packed_profile_block.profile_byte_0x82, - profile_byte_0x82_hex: probe.packed_profile_block.profile_byte_0x82_hex.clone(), - profile_byte_0x97: probe.packed_profile_block.profile_byte_0x97, - profile_byte_0x97_hex: probe.packed_profile_block.profile_byte_0x97_hex.clone(), - profile_byte_0xc5: probe.packed_profile_block.profile_byte_0xc5, - profile_byte_0xc5_hex: probe.packed_profile_block.profile_byte_0xc5_hex.clone(), - }) - } else { - report - .rt3_105_packed_profile_probe - .as_ref() - .map(|probe| SmpLoadedProfile { - profile_kind: "rt3-105-packed-profile".to_string(), - profile_family: probe.profile_family.clone(), - packed_profile_offset: probe.packed_profile_offset, - packed_profile_len: probe.packed_profile_len, - packed_profile_len_hex: probe.packed_profile_len_hex.clone(), - leading_word_0: probe.packed_profile_block.leading_word_0, - leading_word_0_hex: probe.packed_profile_block.leading_word_0_hex.clone(), - header_flag_word_3: Some(probe.packed_profile_block.header_flag_word_3), - header_flag_word_3_hex: Some( - probe.packed_profile_block.header_flag_word_3_hex.clone(), - ), - map_path: probe.packed_profile_block.map_path.clone(), - display_name: probe.packed_profile_block.display_name.clone(), - profile_byte_0x77: probe.packed_profile_block.profile_byte_0x77, - profile_byte_0x77_hex: probe.packed_profile_block.profile_byte_0x77_hex.clone(), - profile_byte_0x82: probe.packed_profile_block.profile_byte_0x82, - profile_byte_0x82_hex: probe.packed_profile_block.profile_byte_0x82_hex.clone(), - profile_byte_0x97: probe.packed_profile_block.profile_byte_0x97, - profile_byte_0x97_hex: probe.packed_profile_block.profile_byte_0x97_hex.clone(), - profile_byte_0xc5: probe.packed_profile_block.profile_byte_0xc5, - profile_byte_0xc5_hex: probe.packed_profile_block.profile_byte_0xc5_hex.clone(), - }) - }; - let candidate_availability_table = report.rt3_105_save_name_table_probe.as_ref().map(|probe| { - SmpLoadedCandidateAvailabilityTable { - source_kind: probe.source_kind.clone(), - semantic_family: probe.semantic_family.clone(), - header_offset: probe.header_offset, - entries_offset: probe.entries_offset, - entries_end_offset: probe.entries_end_offset, - observed_entry_count: probe.observed_entry_count, - zero_availability_count: probe.zero_trailer_entry_count, - zero_availability_names: probe.zero_trailer_entry_names.clone(), - footer_progress_hex_words: vec![ - probe.footer_progress_word_0_hex.clone(), - probe.footer_progress_word_1_hex.clone(), - ], - entries: probe.entries.clone(), - } - }); - let named_locomotive_availability_table = report - .rt3_105_save_named_locomotive_availability_probe - .as_ref() - .map(|probe| SmpLoadedNamedLocomotiveAvailabilityTable { - source_kind: probe.source_kind.clone(), - semantic_family: probe.semantic_family.clone(), - header_offset: None, - entries_offset: Some(probe.entries_offset), - entries_end_offset: Some(probe.entries_end_offset), - observed_entry_count: probe.observed_entry_count, - zero_availability_count: probe.zero_availability_count, - zero_availability_names: probe.zero_availability_names.clone(), - entries: probe.entries.clone(), - }); - let locomotive_catalog = named_locomotive_availability_table - .as_ref() - .and_then(derive_locomotive_catalog_from_named_availability_table); - let cargo_catalog = report - .recipe_book_summary_probe - .as_ref() - .and_then(derive_cargo_catalog_from_recipe_book_probe); - let world_issue_37_state = report - .save_world_issue_37_probe - .as_ref() - .map(derive_loaded_world_issue_37_state_from_probe); - let world_economic_tuning_state = report - .save_world_economic_tuning_probe - .as_ref() - .map(derive_loaded_world_economic_tuning_state_from_probe); - let world_finance_neighborhood_state = report - .save_world_finance_neighborhood_probe - .as_ref() - .map(derive_loaded_world_finance_neighborhood_state_from_probe); - let world_locomotive_policy_state = derive_loaded_world_locomotive_policy_state_from_probes( - report.post_text_field_neighborhood_probe.as_ref(), - report.locomotive_policy_neighborhood_probe.as_ref(), - ); - let company_roster = report.save_company_roster_probe.clone().or_else(|| { - report - .save_world_selection_context_probe - .as_ref() - .and_then(|probe| { - derive_selection_only_company_roster_from_save_world_probe( - probe, - report.save_company_collection_header_probe.as_ref(), - ) - }) - }); - let chairman_profile_table = report - .save_chairman_profile_table_probe - .clone() - .or_else(|| { - report - .save_world_selection_context_probe - .as_ref() - .and_then(|probe| { - derive_selection_only_chairman_profile_table_from_save_world_probe( - probe, - report - .save_chairman_profile_collection_header_probe - .as_ref(), - ) - }) - }); - let region_collection = report - .save_region_record_triplet_probe - .as_ref() - .map(derive_loaded_region_collection_from_probe); - let region_fixed_row_run_summary = report - .save_region_fixed_row_run_candidate_probe - .as_ref() - .map(derive_loaded_region_fixed_row_run_summary_from_probe); - let placed_structure_collection = report - .save_placed_structure_record_triplet_probe - .as_ref() - .map(derive_loaded_placed_structure_collection_from_probe); - let special_conditions_table = - report - .special_conditions_probe - .as_ref() - .map(|probe| SmpLoadedSpecialConditionsTable { - source_kind: probe.source_kind.clone(), - table_offset: probe.table_offset, - table_len: probe.table_len, - enabled_visible_count: probe.enabled_visible_count, - enabled_visible_labels: probe.enabled_visible_labels.clone(), - entries: probe.entries.clone(), - }); - let placed_structure_dynamic_side_buffer_probe = report - .save_placed_structure_dynamic_side_buffer_probe - .clone(); - let placed_structure_dynamic_side_buffer_alignment = report - .save_placed_structure_dynamic_side_buffer_probe - .as_ref() - .zip(report.save_placed_structure_record_triplet_probe.as_ref()) - .map(|(side_buffer, triplets)| { - summarize_placed_structure_dynamic_side_buffer_alignment(side_buffer, triplets) - }); - let placed_structure_dynamic_side_buffer_summary = report - .save_placed_structure_dynamic_side_buffer_probe - .as_ref() - .map(|probe| { - derive_loaded_placed_structure_dynamic_side_buffer_summary( - probe, - placed_structure_dynamic_side_buffer_alignment.as_ref(), - ) - }); - let mut notes = summary.notes.clone(); - if let Some(probe) = &report.save_world_selection_context_probe { - notes.push(format!( - "Raw save fixed world block exposes selected_company_id={} at file offset 0x{:x}.", - probe.selected_company_id, probe.selected_company_id_offset - )); - notes.push(format!( - "Raw save fixed world block exposes selected_chairman_profile_id={} at file offset 0x{:x}.", - probe.selected_chairman_profile_id, probe.selected_chairman_profile_id_offset - )); - notes.push(format!( - "Raw save fixed world block also exposes {} chairman slot selector bytes at file offset 0x{:x} and campaign_override_flag={} at file offset 0x{:x}.", - probe.chairman_slot_selectors.len(), - probe.chairman_slot_selector_offset, - probe.campaign_override_flag, - probe.campaign_override_flag_offset - )); - if report.save_company_roster_probe.is_none() - || report.save_chairman_profile_table_probe.is_none() - { - notes.push( - "Raw save inspection still does not reconstruct every company_roster or chairman_profile_table scalar lane; the grounded package-save path prefers direct-record reconstruction where it can and falls back to selection/header-only context otherwise." - .to_string(), - ); - } - } - if let Some(probe) = &report.save_world_issue_37_probe { - notes.push(format!( - "Raw save fixed world block also exposes the grounded issue-0x37 pair: value={} at file offset 0x{:x} and companion multiplier {:.6} at file offset 0x{:x}.", - probe.issue_value_lane.value_i32, - probe.payload_offset + probe.issue_value_lane.relative_offset, - probe.multiplier_lane.value_f32, - probe.payload_offset + probe.multiplier_lane.relative_offset - )); - } - if let Some(probe) = &report.save_world_economic_tuning_probe { - notes.push(format!( - "Raw save fixed world block also exposes the six-lane economic tuning float band at file offset 0x{:x} (mirror lane at 0x{:x}).", - probe.tuning_lanes - .first() - .map(|lane| probe.payload_offset + lane.relative_offset) - .unwrap_or(probe.payload_offset), - probe.payload_offset + probe.mirror_lane.relative_offset - )); - notes.push( - "Current atlas evidence treats that fixed six-float world tuning band as the editor economic-cost family, not as the company-governance issue table behind investor confidence." - .to_string(), - ); - } - if let Some(probe) = &report.save_company_collection_header_probe { - notes.push(format!( - "Raw save tagged company header reports live_record_count={} and live_id_bound={} at file offsets 0x{:x}/0x{:x}/0x{:x}.", - probe.live_record_count, - probe.live_id_bound, - probe.metadata_tag_offset, - probe.records_tag_offset, - probe.close_tag_offset - )); - } - if let Some(probe) = &report.save_chairman_profile_collection_header_probe { - notes.push(format!( - "Raw save tagged chairman/profile header reports live_record_count={} and live_id_bound={} at file offsets 0x{:x}/0x{:x}/0x{:x}.", - probe.live_record_count, - probe.live_id_bound, - probe.metadata_tag_offset, - probe.records_tag_offset, - probe.close_tag_offset - )); - } - if let Some(probe) = &report.save_train_collection_header_probe { - notes.push(format!( - "Raw save tagged train header reports live_record_count={} and live_id_bound={} with direct_record_stride=0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", - probe.live_record_count, - probe.live_id_bound, - probe.direct_record_stride, - probe.metadata_tag_offset, - probe.records_tag_offset, - probe.close_tag_offset - )); - } - if let Some(probe) = &report.save_train_collection_directory_probe { - notes.push(format!( - "Raw save tagged train metadata also exposes a live-entry directory with {} entries rooted at metadata dword {} (head={:?}, tail={:?}).", - probe.entries.len(), - probe.directory_root_dword_index, - probe.chain_head_live_entry_id, - probe.chain_tail_live_entry_id - )); - } - if let Some(probe) = &report.save_region_collection_header_probe { - notes.push(format!( - "Raw save tagged region header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", - probe.live_record_count, - probe.live_id_bound, - probe.direct_record_stride, - probe.metadata_tag_offset, - probe.records_tag_offset, - probe.close_tag_offset - )); - } - if let Some(probe) = &report.save_region_record_triplet_probe { - notes.push(format!( - "Raw save tagged region records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets in the records span; first name={:?}, first policy lanes=({:.3}, {:.3}, {:.3}), trailing_word={}, first profile collection count={:?}, first profile collection trailing_padding_len={:?}.", - probe.record_count, - probe.entries.first().map(|entry| entry.name.as_str()), - probe.entries - .first() - .map(|entry| entry.policy_leading_f32_0) - .unwrap_or_default(), - probe.entries - .first() - .map(|entry| entry.policy_leading_f32_1) - .unwrap_or_default(), - probe.entries - .first() - .map(|entry| entry.policy_leading_f32_2) - .unwrap_or_default(), - probe.entries - .first() - .map(|entry| entry.policy_trailing_word_hex.as_str()) - .unwrap_or("0x0000"), - probe.entries.first().and_then(|entry| { - entry.profile_collection.as_ref().map(|collection| collection.live_record_count) - }), - probe.entries.first().and_then(|entry| { - entry.profile_collection.as_ref().map(|collection| collection.trailing_padding_len) - }) - )); - } - if let Some(collection) = ®ion_collection { - let total_profile_rows = collection - .entries - .iter() - .map(|entry| { - entry - .profile_collection - .as_ref() - .map(|collection| collection.entries.len()) - .unwrap_or_default() - }) - .sum::(); - let nonzero_prefix_count = collection - .entries - .iter() - .filter(|entry| entry.pre_name_prefix_len != 0) - .count(); - let nonzero_reserved_count = collection - .entries - .iter() - .filter(|entry| entry.policy_reserved_dwords.iter().any(|raw| *raw != 0)) - .count(); - notes.push(format!( - "Save-slice projection now carries {} loaded region triplet rows as first-class context, with {} embedded profile rows, {} rows with nonzero pre-name prefixes, and {} rows with nonzero reserved policy dwords.", - collection.observed_entry_count, - total_profile_rows, - nonzero_prefix_count, - nonzero_reserved_count - )); - } - if let Some(summary) = ®ion_fixed_row_run_summary { - notes.push(format!( - "Save-slice projection now also carries the region fixed-row run summary with {} candidate row bands at target stride {}, best rows offset {:?}, and best shape signature {:?}.", - summary.candidates.len(), - summary.target_row_stride_hex, - summary - .candidates - .first() - .map(|candidate| candidate.rows_offset_hex.as_str()), - summary - .candidates - .first() - .map(|candidate| candidate.shape_signature.as_str()) - )); - } - if let Some(probe) = &report.save_placed_structure_collection_header_probe { - notes.push(format!( - "Raw save tagged placed-structure header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", - probe.live_record_count, - probe.live_id_bound, - probe.direct_record_stride, - probe.metadata_tag_offset, - probe.records_tag_offset, - probe.close_tag_offset - )); - } - if let Some(probe) = &report.save_placed_structure_record_triplet_probe { - notes.push(format!( - "Raw save tagged placed-structure records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets; first stems={:?}/{:?}, first policy lanes=({:.3}, {:.3}, {:.3}, {:.3}, {:.3}), first footer payload={}, first footer status kind={:?}.", - probe.record_count, - probe.entries.first().map(|entry| entry.primary_name.as_str()), - probe.entries.first().map(|entry| entry.secondary_name.as_str()), - probe.entries.first().map(|entry| entry.policy_f32_lane_0).unwrap_or_default(), - probe.entries.first().map(|entry| entry.policy_f32_lane_1).unwrap_or_default(), - probe.entries.first().map(|entry| entry.policy_f32_lane_2).unwrap_or_default(), - probe.entries.first().map(|entry| entry.policy_f32_lane_3).unwrap_or_default(), - probe.entries.first().map(|entry| entry.policy_f32_lane_4).unwrap_or_default(), - probe.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000"), - probe.entries.first().map(|entry| entry.profile_status_kind.as_str()) - )); - } - if let Some(collection) = &placed_structure_collection { - let farm_growth_stage_count = collection - .entries - .iter() - .filter(|entry| entry.farm_growth_stage_index.is_some()) - .count(); - let opaque_status_count = collection - .entries - .iter() - .filter(|entry| entry.profile_status_kind != "unset") - .count(); - notes.push(format!( - "Save-slice projection now carries {} loaded placed-structure triplet rows as first-class context, with {} farm growth-stage rows and {} non-default footer-status rows.", - collection.observed_entry_count, farm_growth_stage_count, opaque_status_count - )); - } - if let Some(probe) = &placed_structure_dynamic_side_buffer_probe { - let dominant_pattern = probe.compact_prefix_pattern_summaries.first(); - let payload_envelope_summary = probe.payload_envelope_summary.as_ref(); - let short_profile_flag_pair_summary = payload_envelope_summary - .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()); - notes.push(format!( - "Raw save also exposes the separate placed-structure dynamic-side-buffer candidate 0x38a5/0x38a6/0x38a7: live_record_count={}, owner-shared 0x38a6 dword={} at relative offset 0x{:x}, first compact prefix=({},{},{}), first embedded names={:?}/{:?}/{:?}, embedded 0x55f1 row count={}, rows with tertiary 0x55f1 string={}, unique compact prefix patterns={}, 0x55f3-leading rows={}, complete 0x55f1/0x55f2/0x55f3 envelopes={}, dominant 0x55f2 chunk len=0x{:x} x{}, dominant 0x55f3 span=0x{:x} x{}, dominant short 0x55f3 flag pair={}/{} x{}, dominant compact pattern={}/{}/{} x{}.", - probe.live_record_count, - probe.owner_shared_dword_hex, - probe.owner_shared_dword_relative_offset, - probe.prefix_leading_dword_hex, - probe.prefix_trailing_word_hex, - probe.prefix_separator_byte_hex, - probe.first_embedded_primary_name.as_deref(), - probe.first_embedded_secondary_name.as_deref(), - probe.first_embedded_tertiary_name.as_deref(), - probe.embedded_name_tag_count, - probe.decoded_embedded_name_row_with_tertiary_name_count, - probe.unique_compact_prefix_pattern_count, - probe.prefix_leading_dword_matching_embedded_profile_tag_count, - payload_envelope_summary - .map(|summary| summary.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope) - .unwrap_or_default(), - payload_envelope_summary - .and_then(|summary| summary.dominant_policy_chunk_len) - .unwrap_or_default(), - payload_envelope_summary - .map(|summary| summary.dominant_policy_chunk_len_count) - .unwrap_or_default(), - payload_envelope_summary - .and_then(|summary| summary.dominant_profile_chunk_len) - .unwrap_or_default(), - payload_envelope_summary - .map(|summary| summary.dominant_profile_chunk_len_count) - .unwrap_or_default(), - short_profile_flag_pair_summary - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.first_flag_byte_hex.as_str()) - .unwrap_or("0x00"), - short_profile_flag_pair_summary - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.second_flag_byte_hex.as_str()) - .unwrap_or("0x00"), - short_profile_flag_pair_summary - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.count) - .unwrap_or_default(), - dominant_pattern - .map(|pattern| pattern.prefix_leading_dword_hex.as_str()) - .unwrap_or("0x00000000"), - dominant_pattern - .map(|pattern| pattern.prefix_trailing_word_hex.as_str()) - .unwrap_or("0x0000"), - dominant_pattern - .map(|pattern| pattern.prefix_separator_byte_hex.as_str()) - .unwrap_or("0x00"), - dominant_pattern.map(|pattern| pattern.count).unwrap_or_default() - )); - if probe.owner_shared_dword_matches_first_compact_prefix_leading_dword { - notes.push( - "Direct disassembly now shows 0x00493be0 consuming one shared 0x38a6 owner-local dword before iterating records; the first compact-prefix leading dword currently reuses that same lane." - .to_string(), - ); - } - } - if let Some(summary) = &placed_structure_dynamic_side_buffer_summary { - notes.push(format!( - "Save-slice projection now also carries the placed-structure dynamic side-buffer summary with {} decoded name rows, {} unique name pairs, and {} overlapping triplet name pairs.", - summary.decoded_embedded_name_row_count, - summary.unique_embedded_name_pair_count, - summary.triplet_alignment_overlap_count - )); - } - if let Some(roster) = &report.save_company_roster_probe { - notes.push(format!( - "Raw save inspection reconstructed {} company direct records from the tagged company collection.", - roster.entries.len() - )); - } - if let Some(table) = &report.save_chairman_profile_table_probe { - notes.push(format!( - "Raw save inspection reconstructed {} chairman/profile direct records from the tagged chairman collection.", - table.entries.len() - )); - } - - Ok(SmpLoadedSaveSlice { - file_extension_hint: summary.file_extension_hint.clone(), - container_profile_family: summary.container_profile_family.clone(), - mechanism_family: summary.mechanism_family.clone(), - mechanism_confidence: summary.mechanism_confidence.clone(), - trailer_family: summary.trailer_family.clone(), - bridge_family: summary.bridge_family.clone(), - profile, - candidate_availability_table, - named_locomotive_availability_table, - locomotive_catalog, - cargo_catalog, - world_issue_37_state, - world_economic_tuning_state, - world_finance_neighborhood_state, - world_locomotive_policy_state, - company_roster, - chairman_profile_table, - region_collection, - region_fixed_row_run_summary, - placed_structure_collection, - placed_structure_dynamic_side_buffer_summary, - special_conditions_table, - event_runtime_collection: report.event_runtime_collection_summary.clone(), - notes, - }) -} - -pub fn inspect_save_company_and_chairman_analysis_file( - path: &Path, -) -> Result> { - let bytes = fs::read(path)?; - let report = inspect_bundle_bytes( - &bytes, - path.extension() - .and_then(|extension| extension.to_str()) - .map(|extension| extension.to_ascii_lowercase()), - ); - inspect_save_company_and_chairman_analysis_bytes(&bytes, &report) - .ok_or_else(|| "save inspection did not expose grounded company/chairman analysis".into()) -} - -pub fn inspect_save_company_and_chairman_analysis_bytes( - bytes: &[u8], - report: &SmpInspectionReport, -) -> Option { - let selection_probe = report.save_world_selection_context_probe.as_ref(); - let world_selection_context = selection_probe.map(build_save_world_selection_role_analysis); - let world_issue_37 = report.save_world_issue_37_probe.clone(); - let world_economic_tuning = report.save_world_economic_tuning_probe.clone(); - let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone(); - let train_collection_directory = report.save_train_collection_directory_probe.clone(); - let region_record_triplets = report.save_region_record_triplet_probe.clone(); - let region_queued_notice_records = report - .save_region_queued_notice_record_probe - .clone() - .or_else(|| { - parse_save_region_queued_notice_record_probe( - bytes, - report.file_extension_hint.as_deref(), - report.container_profile.as_ref(), - report.save_region_collection_header_probe.as_ref(), - ) - }); - let region_fixed_row_run_candidates = report - .save_region_fixed_row_run_candidate_probe - .clone() - .or_else(|| { - parse_save_region_fixed_row_run_candidate_probe( - bytes, - report.file_extension_hint.as_deref(), - report.container_profile.as_ref(), - report.save_region_collection_header_probe.as_ref(), - ) - }); - let placed_structure_record_triplets = - report.save_placed_structure_record_triplet_probe.clone(); - let placed_structure_dynamic_side_buffer = report - .save_placed_structure_dynamic_side_buffer_probe - .clone() - .or_else(|| { - parse_save_placed_structure_dynamic_side_buffer_probe( - bytes, - report.file_extension_hint.as_deref(), - report.container_profile.as_ref(), - ) - }); - let placed_structure_dynamic_side_buffer_alignment = placed_structure_dynamic_side_buffer - .as_ref() - .zip(placed_structure_record_triplets.as_ref()) - .map(|(side_buffer, triplets)| { - summarize_placed_structure_dynamic_side_buffer_alignment(side_buffer, triplets) - }); - let unclassified_tagged_collection_headers = report - .save_unclassified_tagged_collection_header_probes - .clone(); - let company_header_probe = report.save_company_collection_header_probe.as_ref(); - let chairman_header_probe = report - .save_chairman_profile_collection_header_probe - .as_ref(); - - let company_entries = if let Some(header_probe) = company_header_probe { - let record_start_offset = detect_save_company_record_start_offset(&bytes, header_probe)?; - let record_stride = header_probe.direct_record_stride as usize; - let base_offset = header_probe - .metadata_tag_offset - .checked_add(4)? - .checked_add(record_start_offset)?; - let mut entries = Vec::with_capacity(header_probe.live_record_count as usize); - for index in 0..header_probe.live_record_count as usize { - let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?; - let company_id = read_u32_at(&bytes, record_offset)?; - let name = read_ascii_c_string_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_NAME_OFFSET, - SAVE_COMPANY_RECORD_NAME_MAX_LEN, - )?; - let active = - read_u8_at(&bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET)? != 0; - let linked_chairman_profile_id = parse_nonzero_u32( - &bytes, - record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET, - )?; - let outstanding_shares = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET, - )?; - let debt = parse_save_company_total_debt(&bytes, record_offset)?; - let bond_count = read_u8_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET, - )?; - let live_bond_slots = parse_save_company_live_bond_slots(&bytes, record_offset)?; - let largest_live_bond_principal = - parse_save_company_largest_live_bond_principal(&bytes, record_offset)?; - let highest_coupon_live_bond_principal = - parse_save_company_highest_coupon_live_bond_principal(&bytes, record_offset)?; - let available_track_laying_capacity = - parse_save_company_available_track_laying_capacity(&bytes, record_offset)?; - let company_value_scalar_f32 = read_f32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET, - )?; - let cached_share_support_scalar_f32 = read_f32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET, - )?; - let cached_share_price_f32 = read_f32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET, - )?; - let chairman_salary_baseline = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET, - )?; - let chairman_salary_current = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET, - )?; - let chairman_bonus_year = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET, - )?; - let chairman_bonus_amount = read_i32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET, - )?; - let founding_year = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET, - )?; - let last_bankruptcy_year = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET, - )?; - let last_dividend_year = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET, - )?; - let preferred_locomotive_engine_type_raw_u8 = read_u8_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET, - )?; - let city_connection_latch = read_u8_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET, - )? != 0; - let linked_transit_latch = read_u8_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET, - )? != 0; - let linked_transit_autoroute_site_score_cache_refresh_absolute_counter = read_u32_at( - &bytes, - record_offset - + SAVE_COMPANY_RECORD_LINKED_TRANSIT_AUTOROUTE_SITE_SCORE_CACHE_REFRESH_ABSOLUTE_COUNTER_OFFSET, - )?; - let linked_transit_site_peer_cache_refresh_absolute_counter = read_u32_at( - &bytes, - record_offset - + SAVE_COMPANY_RECORD_LINKED_TRANSIT_SITE_PEER_CACHE_REFRESH_ABSOLUTE_COUNTER_OFFSET, - )?; - let linked_transit_route_anchor_entry_id = parse_nonzero_u32( - &bytes, - record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET, - )?; - let linked_transit_route_anchor_fallback_counts = - SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_FALLBACK_COUNT_OFFSETS - .iter() - .map(|relative_offset| read_u32_at(&bytes, record_offset + *relative_offset)) - .collect::>>()?; - let merger_cooldown_year = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET, - )?; - let takeover_cooldown_year = read_u32_at( - &bytes, - record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET, - )?; - let scalar_dword_candidates = SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS - .iter() - .map(|(label, relative_offset)| { - build_save_dword_candidate(&bytes, record_offset, label, *relative_offset) - }) - .collect::>>()?; - let post_capacity_dword_candidates = SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS - .iter() - .map(|(label, relative_offset)| { - build_save_dword_candidate(&bytes, record_offset, label, *relative_offset) - }) - .collect::>>()?; - let stat_band_root_0cfb_candidates = build_save_company_stat_band_candidates( - &bytes, - record_offset, - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET, - "stat_band_0cfb", - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, - )?; - let stat_band_root_0d7f_candidates = build_save_company_stat_band_candidates( - &bytes, - record_offset, - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET, - "stat_band_0d7f", - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, - )?; - let stat_band_root_1c47_candidates = build_save_company_stat_band_candidates( - &bytes, - record_offset, - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET, - "stat_band_1c47", - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, - )?; - entries.push(SmpSaveCompanyRecordAnalysisEntry { - company_id, - name, - active, - linked_chairman_profile_id, - outstanding_shares, - debt, - bond_count, - live_bond_slots, - largest_live_bond_principal, - highest_coupon_live_bond_principal, - available_track_laying_capacity, - company_value_scalar_f32, - cached_share_support_scalar_f32, - cached_share_price_f32, - chairman_salary_baseline, - chairman_salary_current, - chairman_bonus_year, - chairman_bonus_amount, - founding_year, - last_bankruptcy_year, - last_dividend_year, - preferred_locomotive_engine_type_raw_u8, - preferred_locomotive_engine_type_raw_hex: format!( - "0x{preferred_locomotive_engine_type_raw_u8:02x}" - ), - city_connection_latch, - linked_transit_latch, - linked_transit_autoroute_site_score_cache_refresh_absolute_counter, - linked_transit_site_peer_cache_refresh_absolute_counter, - linked_transit_route_anchor_entry_id, - linked_transit_route_anchor_fallback_counts, - merger_cooldown_year, - takeover_cooldown_year, - scalar_dword_candidates, - post_capacity_dword_candidates, - stat_band_root_0cfb_candidates, - stat_band_root_0d7f_candidates, - stat_band_root_1c47_candidates, - }); - } - entries - } else { - Vec::new() - }; - let company_share_prices = company_entries - .iter() - .filter_map(|entry| { - round_f64_to_i64(entry.cached_share_price_f32 as f64) - .map(|share_price| (entry.company_id, share_price)) - }) - .collect::>(); - - let chairman_entries = if let Some(header_probe) = chairman_header_probe { - let record_start_offset = - detect_save_chairman_profile_record_start_offset(&bytes, header_probe)?; - let record_stride = header_probe.direct_record_stride as usize; - let base_offset = header_probe - .metadata_tag_offset - .checked_add(4)? - .checked_add(record_start_offset)?; - let company_id_bound = company_header_probe - .map(|probe| probe.live_id_bound) - .unwrap_or(0); - let mut entries = Vec::with_capacity(header_probe.live_record_count as usize); - for index in 0..header_probe.live_record_count as usize { - let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?; - let profile_id = read_u32_at(&bytes, record_offset)?; - let active = read_u32_at(&bytes, record_offset + 4)? != 0; - let name = read_ascii_c_string_at( - &bytes, - record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET, - SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN, - )?; - let current_cash = - read_f64_at(&bytes, record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET)?; - let linked_company_id = parse_nonzero_u32( - &bytes, - record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, - )?; - let personality_byte_0x291 = read_u8_at( - &bytes, - record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET, - )?; - let mut holdings_by_company = BTreeMap::new(); - for company_id in 1..=company_id_bound { - let slot_offset = record_offset - .checked_add(SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET)? - .checked_add((company_id as usize).checked_mul(4)?)?; - let units = read_u32_at(&bytes, slot_offset)?; - if units != 0 { - holdings_by_company.insert(company_id, units); - } - } - let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS - .iter() - .map(|relative_offset| { - build_save_qword_candidate(&bytes, record_offset, *relative_offset) - }) - .collect::>>()?; - let rounded_current_cash = round_f64_to_i64(current_cash)?; - let derived_holdings_share_price_total = derive_chairman_holdings_share_price_total( - &holdings_by_company, - &company_share_prices, - ); - let derived_net_worth_share_price_total = derived_holdings_share_price_total - .and_then(|holdings_total| rounded_current_cash.checked_add(holdings_total)); - let derived_cached_purchasing_power_total = - derive_chairman_cached_purchasing_power_total( - rounded_current_cash, - &cached_scalar_candidates, - ); - entries.push(SmpSaveChairmanRecordAnalysisEntry { - profile_id, - name, - active, - current_cash, - linked_company_id, - holdings_by_company, - derived_holdings_share_price_total, - derived_net_worth_share_price_total, - derived_cached_purchasing_power_total, - personality_byte_0x291, - personality_byte_0x291_hex: format!("0x{personality_byte_0x291:02x}"), - cached_scalar_candidates, - }); - } - entries - } else { - Vec::new() - }; - - let mut notes = Vec::new(); - if world_selection_context.is_some() { - notes.push( - "World selection context now exports the grounded chairman-slot selector bytes and per-slot role-gate bytes from the fixed save-side 0x32c8 world block." - .to_string(), - ); - } - if world_issue_37.is_some() { - notes.push( - "World analysis now also exports the grounded issue-0x37 pair from the same 0x32c8 world payload: the clamped small issue value at [world+0x2d] and its companion multiplier lane at [world+0x29]." - .to_string(), - ); - } - if world_economic_tuning.is_some() { - notes.push( - "World analysis now also exports the fixed six-lane economic tuning float block from the same 0x32c8 world payload; current atlas evidence still treats that band as distinct from the issue-0x37 investor-confidence family." - .to_string(), - ); - } - if world_finance_neighborhood.is_some() { - notes.push( - "World analysis now also exports one fixed dword finance neighborhood around the grounded issue/calendar lanes, so future issue-0x38/0x39 closure can build on rehosted owner-state candidates instead of ad hoc byte guesses." - .to_string(), - ); - } - if let Some(header) = report.save_train_collection_header_probe.as_ref() { - notes.push(format!( - "Train analysis now also exports the tagged train collection header: live_record_count={} live_id_bound={} direct_record_stride=0x{:x}.", - header.live_record_count, header.live_id_bound, header.direct_record_stride - )); - } - if let Some(directory) = train_collection_directory.as_ref() { - notes.push(format!( - "Train analysis now also exports the tagged live-entry directory rooted at metadata dword {}: {} entries chained from {:?} to {:?}.", - directory.directory_root_dword_index, - directory.entries.len(), - directory.chain_head_live_entry_id, - directory.chain_tail_live_entry_id - )); - } - if let Some(header) = report.save_region_collection_header_probe.as_ref() { - notes.push(format!( - "Region analysis now also exports the non-direct tagged region collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.", - header.live_record_count, header.live_id_bound, header.direct_record_stride - )); - } - if let Some(triplets) = region_record_triplets.as_ref() { - notes.push(format!( - "Region analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first serialized region name={:?}, first policy lanes=({:.3}, {:.3}, {:.3}), first profile collection count={:?}, first profile collection trailing_padding_len={:?}.", - triplets.record_count, - triplets.entries.first().map(|entry| entry.name.as_str()), - triplets - .entries - .first() - .map(|entry| entry.policy_leading_f32_0) - .unwrap_or_default(), - triplets - .entries - .first() - .map(|entry| entry.policy_leading_f32_1) - .unwrap_or_default(), - triplets - .entries - .first() - .map(|entry| entry.policy_leading_f32_2) - .unwrap_or_default(), - triplets.entries.first().and_then(|entry| { - entry.profile_collection.as_ref().map(|collection| collection.live_record_count) - }), - triplets.entries.first().and_then(|entry| { - entry.profile_collection.as_ref().map(|collection| collection.trailing_padding_len) - }) - )); - } - if let Some(queue_probe) = region_queued_notice_records.as_ref() { - notes.push(format!( - "Region analysis now also exports {} queued kind-7 notice nodes with payload seed {}: first region id={} amount={} promotion={} tails={}/{}.", - queue_probe.entries.len(), - queue_probe.payload_seed_dword_hex, - queue_probe.entries[0].region_id, - queue_probe.entries[0].amount, - queue_probe.entries[0].promotion_latch_dword_hex, - queue_probe.entries[0].trailing_sentinel_i32_0_hex, - queue_probe.entries[0].trailing_sentinel_i32_1_hex - )); - } - if let Some(fixed_row_candidates) = region_fixed_row_run_candidates.as_ref() { - notes.push(format!( - "Region analysis now also exports {} fixed-row run candidates keyed to live_record_count={} and stride {} before the tagged region metadata; best candidate rows offset is {:?} with shape signature {:?}.", - fixed_row_candidates.candidates.len(), - fixed_row_candidates.target_row_count, - fixed_row_candidates.target_row_stride_hex, - fixed_row_candidates - .candidates - .first() - .map(|candidate| candidate.rows_offset_hex.as_str()), - fixed_row_candidates - .candidates - .first() - .map(|candidate| candidate.shape_signature.as_str()) - )); - } - if let Some(header) = report - .save_placed_structure_collection_header_probe - .as_ref() - { - notes.push(format!( - "Placed-structure analysis now also exports the tagged collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.", - header.live_record_count, header.live_id_bound, header.direct_record_stride - )); - } - if let Some(triplets) = placed_structure_record_triplets.as_ref() { - notes.push(format!( - "Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}, first footer payload={}, first footer status kind={:?}.", - triplets.record_count, - triplets.entries.first().map(|entry| entry.primary_name.as_str()), - triplets.entries.first().map(|entry| entry.secondary_name.as_str()), - triplets.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000"), - triplets.entries.first().map(|entry| entry.profile_status_kind.as_str()) - )); - } - if let Some(side_buffer) = placed_structure_dynamic_side_buffer.as_ref() { - let dominant_pattern = side_buffer.compact_prefix_pattern_summaries.first(); - let payload_envelope_summary = side_buffer.payload_envelope_summary.as_ref(); - let short_profile_flag_pair_summary = payload_envelope_summary - .and_then(|summary| summary.short_profile_flag_pair_summary.as_ref()); - notes.push(format!( - "Placed-structure analysis now also exports the separate 0x38a5 dynamic side-buffer owner seam with {} embedded name rows, {} decoded rows across {} unique name pairs, {} rows with a tertiary 0x55f1 string, {} unique compact prefix patterns, {} rows whose leading dword matches 0x55f3, {} complete 0x55f1/0x55f2/0x55f3 envelopes, dominant 0x55f2 chunk len=0x{:x} x{}, dominant 0x55f3 span=0x{:x} x{}, dominant short 0x55f3 flag pair={}/{} x{}, and dominant compact pattern={}/{}/{} x{}.", - side_buffer.embedded_name_tag_count, - side_buffer.decoded_embedded_name_row_count, - side_buffer.unique_embedded_name_pair_count, - side_buffer.decoded_embedded_name_row_with_tertiary_name_count, - side_buffer.unique_compact_prefix_pattern_count, - side_buffer.prefix_leading_dword_matching_embedded_profile_tag_count, - payload_envelope_summary - .map(|summary| summary.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope) - .unwrap_or_default(), - payload_envelope_summary - .and_then(|summary| summary.dominant_policy_chunk_len) - .unwrap_or_default(), - payload_envelope_summary - .map(|summary| summary.dominant_policy_chunk_len_count) - .unwrap_or_default(), - payload_envelope_summary - .and_then(|summary| summary.dominant_profile_chunk_len) - .unwrap_or_default(), - payload_envelope_summary - .map(|summary| summary.dominant_profile_chunk_len_count) - .unwrap_or_default(), - short_profile_flag_pair_summary - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.first_flag_byte_hex.as_str()) - .unwrap_or("0x00"), - short_profile_flag_pair_summary - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.second_flag_byte_hex.as_str()) - .unwrap_or("0x00"), - short_profile_flag_pair_summary - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| pair.count) - .unwrap_or_default(), - dominant_pattern - .map(|pattern| pattern.prefix_leading_dword_hex.as_str()) - .unwrap_or("0x00000000"), - dominant_pattern - .map(|pattern| pattern.prefix_trailing_word_hex.as_str()) - .unwrap_or("0x0000"), - dominant_pattern - .map(|pattern| pattern.prefix_separator_byte_hex.as_str()) - .unwrap_or("0x00"), - dominant_pattern.map(|pattern| pattern.count).unwrap_or_default() - )); - } - if let Some(alignment) = placed_structure_dynamic_side_buffer_alignment.as_ref() { - notes.push(format!( - "Placed-structure analysis now also compares the 0x38a5 side-buffer against the grounded 0x36b1 triplet corpus: {} of {} decoded side-buffer rows reuse {} overlapping placed-structure name pairs, leaving {} unmatched side-buffer rows and {} triplet-only name pairs.", - alignment.side_buffer_rows_with_matching_triplet_name_pair_count, - alignment.side_buffer_row_count, - alignment.overlapping_name_pair_count, - alignment.side_buffer_rows_without_matching_triplet_name_pair_count, - alignment.triplet_name_pairs_without_side_buffer_match_count - )); - } - if let Some(candidate) = unclassified_tagged_collection_headers.first() { - notes.push(format!( - "Generic save-side tagged collection scan also found {} unclassified candidate families; largest current candidate uses tags {}/{}/{} with live_record_count={} stride=0x{:x} records_span_len=0x{:x}.", - unclassified_tagged_collection_headers.len(), - candidate.metadata_tag_hex, - candidate.records_tag_hex, - candidate.close_tag_hex, - candidate.live_record_count, - candidate.direct_record_stride, - candidate.records_span_len - )); - } - if !company_entries.is_empty() { - notes.push( - "Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string(), - ); - notes.push( - "Company available_track_laying_capacity is derived from the grounded tail dword [company+0x7680], with negative values treated as the unlimited sentinel.".to_string(), - ); - notes.push( - "Company scalar_dword_candidates expose the current checked-in raw save windows around support/share-price/calendar lanes, and post_capacity_dword_candidates expose the immediate dwords after [company+0x7680] for deeper track-count and record-tail analysis.".to_string(), - ); - notes.push( - "Company stat-band root candidates now also expose the first dword windows rooted at [company+0x0cfb], [company+0x0d7f], and [company+0x1c47], the same broader stat bands the grounded cheat reset branch clears before later finance/detail readers rebuild them.".to_string(), - ); - notes.push( - "Current atlas evidence ties company current_cash and book_value_per_share to stat-family 0x2329 slots 0x0d and 0x1d, so the remaining save-native company finance/governance closure likely needs a structured company-stat family reconstruction instead of more isolated raw offsets." - .to_string(), - ); - } - if !chairman_entries.is_empty() { - notes.push( - "Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9], now including raw qword hex and signed/f64 views for further purchasing-power analysis.".to_string(), - ); - notes.push( - "Chairman analysis now also derives one holdings-at-cached-share-price total from the grounded company cached_share_price lane and one strongest-cached purchasing-power total from the nonnegative qword cache band." - .to_string(), - ); - } - - Some(SmpSaveCompanyChairmanAnalysisReport { - profile_family: report - .container_profile - .as_ref() - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()), - selected_company_id: selection_probe.map(|probe| probe.selected_company_id), - selected_chairman_profile_id: selection_probe - .map(|probe| probe.selected_chairman_profile_id), - world_selection_context, - world_issue_37, - world_economic_tuning, - world_finance_neighborhood, - train_collection_header: report.save_train_collection_header_probe.clone(), - train_collection_directory, - region_collection_header: report.save_region_collection_header_probe.clone(), - region_record_triplets, - region_queued_notice_records, - region_fixed_row_run_candidates, - placed_structure_collection_header: report - .save_placed_structure_collection_header_probe - .clone(), - placed_structure_record_triplets, - placed_structure_dynamic_side_buffer, - placed_structure_dynamic_side_buffer_alignment, - unclassified_tagged_collection_headers, - company_entries, - chairman_entries, - notes, - }) -} - -pub fn compare_save_region_fixed_row_run_candidates( - left: &SmpSaveCompanyChairmanAnalysisReport, - right: &SmpSaveCompanyChairmanAnalysisReport, -) -> Option { - let left_probe = left.region_fixed_row_run_candidates.as_ref()?; - let right_probe = right.region_fixed_row_run_candidates.as_ref()?; - - let left_by_shape = left_probe - .candidates - .iter() - .enumerate() - .map(|(index, candidate)| (candidate.shape_signature.clone(), (index, candidate))) - .collect::>(); - let right_by_shape = right_probe - .candidates - .iter() - .enumerate() - .map(|(index, candidate)| (candidate.shape_signature.clone(), (index, candidate))) - .collect::>(); - - let mut shared_shape_matches = Vec::new(); - let mut shared_shape_family_matches = Vec::new(); - let mut left_only_shape_signatures = Vec::new(); - let mut right_only_shape_signatures = Vec::new(); - let mut left_only_shape_family_signatures = Vec::new(); - let mut right_only_shape_family_signatures = Vec::new(); - let left_family_by_shape = left_probe - .candidates - .iter() - .enumerate() - .map(|(index, candidate)| (candidate.shape_family_signature.clone(), (index, candidate))) - .collect::>(); - let right_family_by_shape = right_probe - .candidates - .iter() - .enumerate() - .map(|(index, candidate)| (candidate.shape_family_signature.clone(), (index, candidate))) - .collect::>(); - - for (shape_signature, (left_index, left_candidate)) in &left_by_shape { - if let Some((right_index, right_candidate)) = right_by_shape.get(shape_signature) { - shared_shape_matches.push(SmpSaveRegionFixedRowRunSharedShapeMatch { - shape_signature: shape_signature.clone(), - left_rank: left_index + 1, - left_rows_offset_hex: left_candidate.rows_offset_hex.clone(), - left_best_probable_density_lane_relative_offset_hex: left_candidate - .best_probable_density_lane_relative_offset_hex - .clone(), - right_rank: right_index + 1, - right_rows_offset_hex: right_candidate.rows_offset_hex.clone(), - right_best_probable_density_lane_relative_offset_hex: right_candidate - .best_probable_density_lane_relative_offset_hex - .clone(), - }); - } else { - left_only_shape_signatures.push(shape_signature.clone()); - } - } - - for shape_signature in right_by_shape.keys() { - if !left_by_shape.contains_key(shape_signature) { - right_only_shape_signatures.push(shape_signature.clone()); - } - } - - for (shape_family_signature, (left_index, left_candidate)) in &left_family_by_shape { - if let Some((right_index, right_candidate)) = - right_family_by_shape.get(shape_family_signature) - { - shared_shape_family_matches.push(SmpSaveRegionFixedRowRunSharedShapeMatch { - shape_signature: shape_family_signature.clone(), - left_rank: left_index + 1, - left_rows_offset_hex: left_candidate.rows_offset_hex.clone(), - left_best_probable_density_lane_relative_offset_hex: left_candidate - .best_probable_density_lane_relative_offset_hex - .clone(), - right_rank: right_index + 1, - right_rows_offset_hex: right_candidate.rows_offset_hex.clone(), - right_best_probable_density_lane_relative_offset_hex: right_candidate - .best_probable_density_lane_relative_offset_hex - .clone(), - }); - } else { - left_only_shape_family_signatures.push(shape_family_signature.clone()); - } - } - - for shape_family_signature in right_family_by_shape.keys() { - if !left_family_by_shape.contains_key(shape_family_signature) { - right_only_shape_family_signatures.push(shape_family_signature.clone()); - } - } - - Some(SmpSaveRegionFixedRowRunComparisonReport { - left_profile_family: left.profile_family.clone(), - right_profile_family: right.profile_family.clone(), - left_best_rows_offset_hex: left_probe - .candidates - .first() - .map(|candidate| candidate.rows_offset_hex.clone()), - right_best_rows_offset_hex: right_probe - .candidates - .first() - .map(|candidate| candidate.rows_offset_hex.clone()), - left_best_shape_signature: left_probe - .candidates - .first() - .map(|candidate| candidate.shape_signature.clone()), - right_best_shape_signature: right_probe - .candidates - .first() - .map(|candidate| candidate.shape_signature.clone()), - left_best_shape_family_signature: left_probe - .candidates - .first() - .map(|candidate| candidate.shape_family_signature.clone()), - right_best_shape_family_signature: right_probe - .candidates - .first() - .map(|candidate| candidate.shape_family_signature.clone()), - shared_shape_matches, - shared_shape_family_matches, - left_only_shape_signatures, - right_only_shape_signatures, - left_only_shape_family_signatures, - right_only_shape_family_signatures, - evidence: vec![ - format!( - "comparison keys the pre-region-header fixed-row candidates by derived lane-shape fingerprint instead of raw offset, because current grounded saves do not keep the same top rows_offset across files ({:?} vs {:?})", - left_probe.candidates.first().map(|candidate| candidate.rows_offset_hex.as_str()), - right_probe.candidates.first().map(|candidate| candidate.rows_offset_hex.as_str()) - ), - "shared shape matches mean two saves surfaced at least one candidate with the same exact probable-f32/small-unsigned/partial-zero/trailing-byte profile, while shared shape-family matches allow mild count drift inside the same dense lane family".to_string(), - ], - }) -} - -fn derive_locomotive_catalog_from_named_availability_table( - table: &SmpLoadedNamedLocomotiveAvailabilityTable, -) -> Option { - if table.entries.is_empty() { - return None; - } - - let entries = table - .entries - .iter() - .enumerate() - .map(|(index, entry)| SmpLoadedLocomotiveCatalogEntry { - locomotive_id: (index + 1) as u32, - name: entry.text.clone(), - }) - .collect::>(); - - Some(SmpLoadedLocomotiveCatalog { - source_kind: format!("{}-ordinal-catalog", table.source_kind), - semantic_family: "scenario-save-derived-locomotive-catalog".to_string(), - entries_offset: table.entries_offset, - observed_entry_count: entries.len(), - entries, - }) -} - -fn derive_cargo_catalog_from_recipe_book_probe( - probe: &SmpRecipeBookSummaryProbe, -) -> Option { - if probe.books.is_empty() { - return None; - } - - let entries = probe - .books - .iter() - .filter(|book| book.book_index < 11) - .filter_map(|book| { - let line = book - .lines - .iter() - .find(|line| line.imports_to_runtime_descriptor) - .or_else(|| book.lines.first())?; - let slot_id = (book.book_index + 1) as u32; - let definition = known_cargo_slot_definition(slot_id)?; - Some(SmpLoadedCargoCatalogEntry { - slot_id, - label: definition.label.to_string(), - cargo_class: definition.cargo_class, - book_index: book.book_index, - max_annual_production_word: book.max_annual_production_word, - mode_word: line.mode_word, - runtime_import_branch_kind: line.runtime_import_branch_kind.clone(), - annual_amount_word: line.annual_amount_word, - supplied_cargo_token_word: line.supplied_cargo_token_word, - supplied_cargo_token_probable_high16_ascii_stem: line - .supplied_cargo_token_probable_high16_ascii_stem - .clone(), - demanded_cargo_token_word: line.demanded_cargo_token_word, - demanded_cargo_token_probable_high16_ascii_stem: line - .demanded_cargo_token_probable_high16_ascii_stem - .clone(), - }) - }) - .collect::>(); - - if entries.is_empty() { - return None; - } - - Some(SmpLoadedCargoCatalog { - source_kind: format!("{}-slot-catalog", probe.source_kind), - semantic_family: "scenario-save-derived-cargo-catalog".to_string(), - root_offset: Some(probe.root_offset), - observed_entry_count: entries.len(), - entries, - }) -} - -fn derive_loaded_world_issue_37_state_from_probe( - probe: &SmpSaveWorldIssue37Probe, -) -> SmpLoadedWorldIssue37State { - SmpLoadedWorldIssue37State { - source_kind: probe.source_kind.clone(), - semantic_family: probe.semantic_family.clone(), - issue_value: probe.issue_value_lane.raw_u32, - issue_value_hex: probe.issue_value_lane.raw_u32_hex.clone(), - issue_38_value: u32::from(probe.issue_38_raw_u8), - issue_38_value_hex: probe.issue_38_raw_hex.clone(), - issue_39_value: u32::from(probe.issue_39_raw_u8), - issue_39_value_hex: probe.issue_39_raw_hex.clone(), - issue_3a_value: u32::from(probe.issue_3a_raw_u8), - issue_3a_value_hex: probe.issue_3a_raw_hex.clone(), - multiplier_raw_u32: probe.multiplier_lane.raw_u32, - multiplier_raw_hex: probe.multiplier_lane.raw_u32_hex.clone(), - multiplier_value_f32_text: format!("{:.6}", probe.multiplier_lane.value_f32), - issue_opinion_base_terms_raw_i32: probe.issue_opinion_base_terms_raw_i32.clone(), - } -} - -fn derive_loaded_world_economic_tuning_state_from_probe( - probe: &SmpSaveWorldEconomicTuningProbe, -) -> SmpLoadedWorldEconomicTuningState { - SmpLoadedWorldEconomicTuningState { - source_kind: probe.source_kind.clone(), - semantic_family: probe.semantic_family.clone(), - mirror_raw_u32: probe.mirror_lane.raw_u32, - mirror_raw_hex: probe.mirror_lane.raw_u32_hex.clone(), - mirror_value_f32_text: format!("{:.6}", probe.mirror_lane.value_f32), - lane_raw_u32: probe.tuning_lanes.iter().map(|lane| lane.raw_u32).collect(), - lane_raw_hex: probe - .tuning_lanes - .iter() - .map(|lane| lane.raw_u32_hex.clone()) - .collect(), - lane_value_f32_text: probe - .tuning_lanes - .iter() - .map(|lane| format!("{:.6}", lane.value_f32)) - .collect(), - } -} - -fn derive_loaded_world_finance_neighborhood_state_from_probe( - probe: &SmpSaveWorldFinanceNeighborhoodProbe, -) -> SmpLoadedWorldFinanceNeighborhoodState { - SmpLoadedWorldFinanceNeighborhoodState { - source_kind: probe.source_kind.clone(), - semantic_family: probe.semantic_family.clone(), - packed_year_word_raw_u16: probe.packed_year_word_raw_u16, - packed_year_word_raw_hex: probe.packed_year_word_raw_hex.clone(), - partial_year_progress_raw_u8: probe.partial_year_progress_raw_u8, - partial_year_progress_raw_hex: probe.partial_year_progress_raw_hex.clone(), - current_calendar_tuple_word_raw_u32: probe.current_calendar_tuple_word_lane.raw_u32, - current_calendar_tuple_word_raw_hex: probe - .current_calendar_tuple_word_lane - .raw_u32_hex - .clone(), - current_calendar_tuple_word_2_raw_u32: probe.current_calendar_tuple_word_2_lane.raw_u32, - current_calendar_tuple_word_2_raw_hex: probe - .current_calendar_tuple_word_2_lane - .raw_u32_hex - .clone(), - absolute_counter_raw_u32: probe.absolute_counter_lane.raw_u32, - absolute_counter_raw_hex: probe.absolute_counter_lane.raw_u32_hex.clone(), - absolute_counter_mirror_raw_u32: probe.absolute_counter_mirror_lane.raw_u32, - absolute_counter_mirror_raw_hex: probe.absolute_counter_mirror_lane.raw_u32_hex.clone(), - stock_policy_raw_u8: probe.stock_policy_raw_u8, - stock_policy_raw_hex: probe.stock_policy_raw_hex.clone(), - bond_policy_raw_u8: probe.bond_policy_raw_u8, - bond_policy_raw_hex: probe.bond_policy_raw_hex.clone(), - bankruptcy_policy_raw_u8: probe.bankruptcy_policy_raw_u8, - bankruptcy_policy_raw_hex: probe.bankruptcy_policy_raw_hex.clone(), - dividend_policy_raw_u8: probe.dividend_policy_raw_u8, - dividend_policy_raw_hex: probe.dividend_policy_raw_hex.clone(), - building_density_growth_setting_raw_u32: probe.building_density_growth_setting_lane.raw_u32, - building_density_growth_setting_raw_hex: probe - .building_density_growth_setting_lane - .raw_u32_hex - .clone(), - labels: probe - .dword_candidates - .iter() - .map(|candidate| candidate.label.clone()) - .collect(), - relative_offsets: probe - .dword_candidates - .iter() - .map(|candidate| candidate.relative_offset) - .collect(), - relative_offset_hex: probe - .dword_candidates - .iter() - .map(|candidate| candidate.relative_offset_hex.clone()) - .collect(), - raw_u32: probe - .dword_candidates - .iter() - .map(|candidate| candidate.raw_u32) - .collect(), - raw_hex: probe - .dword_candidates - .iter() - .map(|candidate| candidate.raw_u32_hex.clone()) - .collect(), - value_i32: probe - .dword_candidates - .iter() - .map(|candidate| candidate.value_i32) - .collect(), - value_f32_text: probe - .dword_candidates - .iter() - .map(|candidate| format!("{:.6}", candidate.value_f32)) - .collect(), - } -} - -fn derive_loaded_world_locomotive_policy_state_from_probes( - post_text_probe: Option<&SmpPostTextFieldNeighborhoodProbe>, - locomotive_policy_probe: Option<&SmpLocomotivePolicyNeighborhoodProbe>, -) -> Option { - let field_by_name = |name: &str| { - locomotive_policy_probe? - .grounded_field_observations - .iter() - .find(|field| field.field_name == name) - }; - let post_text_field_by_name = |name: &str| { - post_text_probe? - .grounded_field_observations - .iter() - .find(|field| field.field_name == name) - }; - let selected_year_gap_scalar = field_by_name("selected-year bucket companion scalar"); - let linked_site_gate = field_by_name("linked-site removal follow-on gate"); - let auto_show_grade = post_text_field_by_name("Auto-Show Grade During Track Lay"); - let starting_building_density = post_text_field_by_name("Starting Building Density Level"); - let building_density_growth = post_text_field_by_name("Building Density Growth"); - let leftover_simulation_time = post_text_field_by_name("leftover simulation time accumulator"); - let selected_year_snapshot = post_text_field_by_name("selected-year lane snapshot"); - let all_steam = field_by_name("All Steam Locos Avail."); - let all_diesel = field_by_name("All Diesel Locos Avail."); - let all_electric = field_by_name("All Electric Locos Avail."); - let cached_available_rating = field_by_name("cached available-locomotive rating"); - Some(SmpLoadedWorldLocomotivePolicyState { - source_kind: locomotive_policy_probe - .map(|probe| probe.source_kind.clone()) - .or_else(|| post_text_probe.map(|probe| probe.source_kind.clone()))?, - semantic_family: "world-locomotive-policy".to_string(), - selected_year_gap_scalar_raw_u32: selected_year_gap_scalar - .and_then(|field| field.value_u32), - selected_year_gap_scalar_raw_hex: selected_year_gap_scalar - .and_then(|field| field.value_u32_hex.clone()), - selected_year_gap_scalar_value_f32_text: selected_year_gap_scalar - .and_then(|field| field.probable_f32_le.clone()), - linked_site_removal_follow_on_gate_raw_u8: linked_site_gate - .and_then(|field| field.value_u8), - linked_site_removal_follow_on_gate_raw_hex: linked_site_gate - .and_then(|field| field.value_u8_hex.clone()), - auto_show_grade_during_track_lay_raw_u8: auto_show_grade.and_then(|field| field.value_u8), - auto_show_grade_during_track_lay_raw_hex: auto_show_grade - .and_then(|field| field.value_u8_hex.clone()), - starting_building_density_level_raw_u8: starting_building_density - .and_then(|field| field.value_u8), - starting_building_density_level_raw_hex: starting_building_density - .and_then(|field| field.value_u8_hex.clone()), - building_density_growth_raw_u8: building_density_growth.and_then(|field| field.value_u8), - building_density_growth_raw_hex: building_density_growth - .and_then(|field| field.value_u8_hex.clone()), - leftover_simulation_time_accumulator_raw_u32: leftover_simulation_time - .and_then(|field| field.value_u32), - leftover_simulation_time_accumulator_raw_hex: leftover_simulation_time - .and_then(|field| field.value_u32_hex.clone()), - leftover_simulation_time_accumulator_value_f32_text: leftover_simulation_time - .and_then(|field| field.probable_f32_le.clone()), - selected_year_lane_snapshot_raw_u8: selected_year_snapshot.and_then(|field| field.value_u8), - selected_year_lane_snapshot_raw_hex: selected_year_snapshot - .and_then(|field| field.value_u8_hex.clone()), - all_steam_locomotives_available_raw_u8: all_steam.and_then(|field| field.value_u8), - all_steam_locomotives_available_raw_hex: all_steam - .and_then(|field| field.value_u8_hex.clone()), - all_diesel_locomotives_available_raw_u8: all_diesel.and_then(|field| field.value_u8), - all_diesel_locomotives_available_raw_hex: all_diesel - .and_then(|field| field.value_u8_hex.clone()), - all_electric_locomotives_available_raw_u8: all_electric.and_then(|field| field.value_u8), - all_electric_locomotives_available_raw_hex: all_electric - .and_then(|field| field.value_u8_hex.clone()), - cached_available_locomotive_rating_raw_u32: cached_available_rating - .and_then(|field| field.value_u32), - cached_available_locomotive_rating_raw_hex: cached_available_rating - .and_then(|field| field.value_u32_hex.clone()), - cached_available_locomotive_rating_value_f32_text: cached_available_rating - .and_then(|field| field.probable_f32_le.clone()), - }) -} - -fn derive_selection_only_company_roster_from_save_world_probe( - probe: &SmpSaveWorldSelectionContextProbe, - header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { - Some(SmpLoadedCompanyRoster { - source_kind: format!("{}-company-selection-only", probe.source_kind), - semantic_family: "scenario-selected-company-context".to_string(), - observed_entry_count: header_probe - .map(|probe| probe.live_record_count as usize) - .unwrap_or(0), - selected_company_id: Some(probe.selected_company_id), - entries: Vec::new(), - }) -} - -fn derive_selection_only_chairman_profile_table_from_save_world_probe( - probe: &SmpSaveWorldSelectionContextProbe, - header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { - Some(SmpLoadedChairmanProfileTable { - source_kind: format!("{}-chairman-selection-only", probe.source_kind), - semantic_family: "scenario-selected-chairman-context".to_string(), - observed_entry_count: header_probe - .map(|probe| probe.live_record_count as usize) - .unwrap_or(0), - selected_chairman_profile_id: Some(probe.selected_chairman_profile_id), - entries: Vec::new(), - }) -} - -const SAVE_COMPANY_RECORD_NAME_OFFSET: usize = 0x04; -const SAVE_COMPANY_RECORD_NAME_MAX_LEN: usize = 0x24; -const SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET: usize = 0x3b; -const SAVE_COMPANY_RECORD_ACTIVE_OFFSET: usize = 0x3f; -const SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET: usize = 0x47; -const SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET: usize = 0x4f; -const SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET: usize = 0x57; -const SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET: usize = 0x5b; -const SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET: usize = 0x5f; -const SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE: usize = 12; -const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET: usize = 0x14f; -const SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET: usize = 0x34f; -const SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET: usize = 0x353; -const SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET: usize = 0x15f; -const SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET: usize = 0x157; -const SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163; -const SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET: usize = 0x16b; -const SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2: usize = 0x16f; -const SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET: usize = 0x173; -const SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2: usize = 0x177; -const SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET: usize = 0x289; -const SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET: usize = 0x0d2d; -const SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET: usize = 0x0d17; -const SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18; -const SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET: usize = 0x0d35; -const SAVE_COMPANY_RECORD_LINKED_TRANSIT_AUTOROUTE_SITE_SCORE_CACHE_REFRESH_ABSOLUTE_COUNTER_OFFSET: usize = - 0x0d3a; -const SAVE_COMPANY_RECORD_LINKED_TRANSIT_SITE_PEER_CACHE_REFRESH_ABSOLUTE_COUNTER_OFFSET: usize = - 0x0d3e; -const SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET: usize = 0x0d07; -const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET: usize = 0x0d59; -const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56; -const SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET: usize = 0x0d19; -const SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET: usize = 0x0d7b; -const SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET: usize = 0x7680; -const SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_FALLBACK_COUNT_OFFSETS: [usize; 3] = - [0x7664, 0x7668, 0x766c]; -const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET: usize = 0x0cfb; -const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET: usize = 0x0d7f; -const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET: usize = 0x1c47; -const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS: usize = 32; -const SAVE_COMPANY_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x2ab; -const SAVE_COMPANY_RECORD_ISSUE_OPINION_TERM_COUNT: usize = - RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT; -const SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT: usize = - crate::runtime::RUNTIME_COMPANY_STAT_SLOT_COUNT as usize - * crate::runtime::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN as usize; -const SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT: usize = - crate::runtime::RUNTIME_COMPANY_STAT_SLOT_COUNT as usize; -const SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_FLOAT_FIELDS: [usize; 10] = [ - 0x4b, 0x53, 0x323, 0x327, 0x32b, 0x32f, 0x333, 0x337, 0x33b, 0x33f, -]; -const SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_INT_FIELDS: [usize; 5] = - [0x14f, 0x34b, 0x0d0b, 0x0d0f, 0x0d13]; -const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 9] = [ - ("mutable_support_scalar", 0x4f), - ("young_company_support_scalar", 0x57), - ( - "support_progress_word", - SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET, - ), - ( - "recent_per_share_subscore", - SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET, - ), - ("cached_share_price", 0x0d7b), - ( - "current_issue_calendar_word", - SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET, - ), - ( - "current_issue_calendar_word_2", - SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2, - ), - ( - "prior_issue_calendar_word", - SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET, - ), - ( - "prior_issue_calendar_word_2", - SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2, - ), -]; -const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [(&str, usize); 6] = [ - ("post_capacity_word_1", 0x7684), - ("post_capacity_word_2", 0x7688), - ("post_capacity_word_3", 0x768c), - ("post_capacity_word_4", 0x7690), - ("post_capacity_word_5", 0x7694), - ("post_capacity_word_6", 0x7698), -]; -const SAVE_COMPANY_RECORD_START_SCAN_LIMIT: usize = 0x120; - -const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08; -const SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN: usize = 0x1f; -const SAVE_CHAIRMAN_RECORD_CASH_OFFSET: usize = 0x154; -const SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET: usize = 0x15d; -const SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET: usize = 0x1dd; -const SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET: usize = 0x1e9; -const SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET: usize = 0x1f1; -const SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET: usize = 0x291; -const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x35b; -const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT: usize = - RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT; -const SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS: [usize; 7] = - [0x1e9, 0x1f1, 0x1f9, 0x201, 0x209, 0x211, 0x219]; -const SAVE_CHAIRMAN_RECORD_START_SCAN_LIMIT: usize = 0x80; - -fn parse_save_company_roster_probe( - bytes: &[u8], - header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, - selection_probe: Option<&SmpSaveWorldSelectionContextProbe>, -) -> Option { - let header_probe = header_probe?; - let observed_entry_count = header_probe.live_record_count as usize; - if observed_entry_count == 0 { - return Some(SmpLoadedCompanyRoster { - source_kind: "save-company-direct-records".to_string(), - semantic_family: "scenario-save-company-direct-records".to_string(), - observed_entry_count, - selected_company_id: selection_probe.map(|probe| probe.selected_company_id), - entries: Vec::new(), - }); - } - - let record_start_offset = detect_save_company_record_start_offset(bytes, header_probe)?; - let record_stride = header_probe.direct_record_stride as usize; - let base_offset = header_probe - .metadata_tag_offset - .checked_add(4)? - .checked_add(record_start_offset)?; - - let mut entries = Vec::with_capacity(observed_entry_count); - for index in 0..observed_entry_count { - let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?; - let company_id = read_u32_at(bytes, record_offset)?; - let active = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET)? != 0; - let linked_chairman_profile_id = parse_nonzero_u32( - bytes, - record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET, - )?; - let outstanding_shares = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET, - )?; - let debt = parse_save_company_total_debt(bytes, record_offset)?; - let bond_count = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)?; - let live_bond_slots = parse_save_company_live_bond_slots(bytes, record_offset)?; - let largest_live_bond_principal = - parse_save_company_largest_live_bond_principal(bytes, record_offset)?; - let highest_coupon_live_bond_principal = - parse_save_company_highest_coupon_live_bond_principal(bytes, record_offset)?; - let available_track_laying_capacity = - parse_save_company_available_track_laying_capacity(bytes, record_offset)?; - let mutable_support_scalar_raw_u32 = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET, - )?; - let young_company_support_scalar_raw_u32 = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET, - )?; - let support_progress_word = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET, - )?; - let recent_per_share_cache_absolute_counter = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET, - )?; - let recent_per_share_cached_value_bits = read_u64_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 4, - )?; - let recent_per_share_subscore_raw_u32 = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET, - )?; - let cached_share_price_raw_u32 = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET, - )?; - let chairman_salary_baseline = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET, - )?; - let chairman_salary_current = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET, - )?; - let chairman_bonus_year = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET, - )?; - let chairman_bonus_amount = read_i32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET, - )?; - let founding_year = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET, - )?; - let last_bankruptcy_year = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET, - )?; - let last_dividend_year = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET, - )?; - let current_issue_calendar_word = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET, - )?; - let current_issue_calendar_word_2 = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2, - )?; - let prior_issue_calendar_word = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET, - )?; - let prior_issue_calendar_word_2 = read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2, - )?; - let preferred_locomotive_engine_type_raw_u8 = read_u8_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET, - )?; - let city_connection_latch = read_u8_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET, - )? != 0; - let linked_transit_latch = read_u8_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET, - )? != 0; - let linked_transit_route_anchor_entry_id = parse_nonzero_u32( - bytes, - record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET, - )?; - let linked_transit_route_anchor_fallback_counts = - SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_FALLBACK_COUNT_OFFSETS - .iter() - .map(|relative_offset| read_u32_at(bytes, record_offset + *relative_offset)) - .collect::>>()?; - let merger_cooldown_year = parse_nonzero_u32( - bytes, - record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET, - )?; - let takeover_cooldown_year = parse_nonzero_u32( - bytes, - record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET, - )?; - let stat_band_root_0cfb_candidates = build_save_company_stat_band_candidates( - bytes, - record_offset, - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET, - "stat_band_0cfb", - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, - )?; - let stat_band_root_0d7f_candidates = build_save_company_stat_band_candidates( - bytes, - record_offset, - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET, - "stat_band_0d7f", - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, - )?; - let stat_band_root_1c47_candidates = build_save_company_stat_band_candidates( - bytes, - record_offset, - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET, - "stat_band_1c47", - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS, - )?; - let year_stat_family_qword_bits = build_save_company_stat_qword_bits( - bytes, - record_offset, - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET, - SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT, - )?; - let special_stat_family_232a_qword_bits = build_save_company_stat_qword_bits( - bytes, - record_offset, - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET, - SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT, - )?; - let direct_control_transfer_float_fields_raw_u32 = build_save_u32_field_map( - bytes, - record_offset, - &SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_FLOAT_FIELDS, - )?; - let direct_control_transfer_int_fields_raw_u32 = build_save_u32_field_map( - bytes, - record_offset, - &SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_INT_FIELDS, - )?; - let issue_opinion_terms_raw_i32 = build_save_i32_term_strip( - bytes, - record_offset, - SAVE_COMPANY_RECORD_ISSUE_OPINION_TERMS_OFFSET, - SAVE_COMPANY_RECORD_ISSUE_OPINION_TERM_COUNT, - )?; - let current_cash = decode_save_company_current_year_stat_slot( - &year_stat_family_qword_bits, - crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - ) - .and_then(round_f64_to_i64) - .unwrap_or(0); - entries.push(SmpLoadedCompanyRosterEntry { - company_id, - active, - controller_kind: RuntimeCompanyControllerKind::Unknown, - current_cash, - debt, - credit_rating_score: None, - prime_rate: None, - available_track_laying_capacity, - track_piece_counts: RuntimeTrackPieceCounts::default(), - linked_chairman_profile_id, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year, - merger_cooldown_year, - preferred_locomotive_engine_type_raw_u8: Some(preferred_locomotive_engine_type_raw_u8), - market_state: Some(RuntimeCompanyMarketState { - outstanding_shares, - bond_count, - live_bond_slots, - largest_live_bond_principal, - highest_coupon_live_bond_principal, - mutable_support_scalar_raw_u32, - young_company_support_scalar_raw_u32, - support_progress_word, - recent_per_share_cache_absolute_counter, - recent_per_share_cached_value_bits, - recent_per_share_subscore_raw_u32, - cached_share_price_raw_u32, - chairman_salary_baseline, - chairman_salary_current, - chairman_bonus_year, - chairman_bonus_amount, - founding_year, - last_bankruptcy_year, - last_dividend_year, - current_issue_calendar_word, - current_issue_calendar_word_2, - prior_issue_calendar_word, - prior_issue_calendar_word_2, - city_connection_latch, - linked_transit_latch, - linked_transit_route_anchor_entry_id, - linked_transit_route_anchor_fallback_counts, - stat_band_root_0cfb_candidates: stat_band_root_0cfb_candidates - .iter() - .map(runtime_company_stat_band_candidate_from_save) - .collect(), - stat_band_root_0d7f_candidates: stat_band_root_0d7f_candidates - .iter() - .map(runtime_company_stat_band_candidate_from_save) - .collect(), - stat_band_root_1c47_candidates: stat_band_root_1c47_candidates - .iter() - .map(runtime_company_stat_band_candidate_from_save) - .collect(), - year_stat_family_qword_bits, - special_stat_family_232a_qword_bits, - issue_opinion_terms_raw_i32, - direct_control_transfer_float_fields_raw_u32, - direct_control_transfer_int_fields_raw_u32, - }), - }); - } - - Some(SmpLoadedCompanyRoster { - source_kind: "save-company-direct-records".to_string(), - semantic_family: "scenario-save-company-direct-records".to_string(), - observed_entry_count, - selected_company_id: selection_probe.map(|probe| probe.selected_company_id), - entries, - }) -} - -fn runtime_company_stat_band_candidate_from_save( - candidate: &SmpSaveDwordCandidate, -) -> crate::RuntimeCompanyStatBandCandidate { - crate::RuntimeCompanyStatBandCandidate { - label: candidate.label.clone(), - relative_offset: candidate.relative_offset, - relative_offset_hex: candidate.relative_offset_hex.clone(), - raw_u32: candidate.raw_u32, - raw_u32_hex: candidate.raw_u32_hex.clone(), - value_i32: candidate.value_i32, - value_f32_text: format!("{:.6}", candidate.value_f32), - } -} - -fn build_save_company_stat_band_candidates( - bytes: &[u8], - record_offset: usize, - root_offset: usize, - label_prefix: &str, - word_count: usize, -) -> Option> { - (0..word_count) - .map(|index| { - let relative_offset = root_offset.checked_add(index.checked_mul(4)?)?; - let label = format!("{label_prefix}_word_{}", index + 1); - build_save_dword_candidate(bytes, record_offset, &label, relative_offset) - }) - .collect::>>() -} - -fn build_save_company_stat_qword_bits( - bytes: &[u8], - record_offset: usize, - root_offset: usize, - qword_count: usize, -) -> Option> { - (0..qword_count) - .map(|index| { - let relative_offset = root_offset.checked_add(index.checked_mul(8)?)?; - read_u64_at(bytes, record_offset + relative_offset) - }) - .collect::>>() -} - -fn build_save_i32_term_strip( - bytes: &[u8], - record_offset: usize, - root_offset: usize, - value_count: usize, -) -> Option> { - (0..value_count) - .map(|index| { - let relative_offset = root_offset.checked_add(index.checked_mul(4)?)?; - read_i32_at(bytes, record_offset + relative_offset) - }) - .collect::>>() -} - -fn build_save_u32_field_map( - bytes: &[u8], - record_offset: usize, - offsets: &[usize], -) -> Option> { - let mut fields = BTreeMap::new(); - for relative_offset in offsets { - fields.insert( - u32::try_from(*relative_offset).ok()?, - read_u32_at(bytes, record_offset + *relative_offset)?, - ); - } - Some(fields) -} - -fn decode_save_company_current_year_stat_slot( - year_stat_family_qword_bits: &[u64], - slot_id: u32, -) -> Option { - let index = slot_id.checked_mul(crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? as usize; - let value = f64::from_bits(*year_stat_family_qword_bits.get(index)?); - value.is_finite().then_some(value) -} - -fn detect_save_company_record_start_offset( - bytes: &[u8], - header_probe: &SmpSaveTaggedCollectionHeaderProbe, -) -> Option { - let observed_entry_count = header_probe.live_record_count as usize; - let record_stride = header_probe.direct_record_stride as usize; - let scan_limit = SAVE_COMPANY_RECORD_START_SCAN_LIMIT.min(record_stride); - let base_offset = header_probe.metadata_tag_offset.checked_add(4)?; - let mut best_start = None; - let mut best_score = 0usize; - - for start in 0..scan_limit { - let mut score = 0usize; - let mut seen_ids = std::collections::BTreeSet::new(); - let mut valid = true; - for index in 0..observed_entry_count { - let record_offset = match base_offset - .checked_add(start) - .and_then(|offset| offset.checked_add(index.checked_mul(record_stride)?)) - { - Some(offset) => offset, - None => { - valid = false; - break; - } - }; - let company_id = match read_u32_at(bytes, record_offset) { - Some(value) - if value >= 1 - && value <= header_probe.live_id_bound - && seen_ids.insert(value) => - { - value - } - _ => { - valid = false; - break; - } - }; - let active = match read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET) - { - Some(0 | 1) => { - score += 1; - true - } - _ => { - valid = false; - break; - } - }; - let linked = match read_u32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET, - ) { - Some(value) if value <= 0x100 => value, - _ => { - valid = false; - break; - } - }; - let name = match read_ascii_c_string_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_NAME_OFFSET, - SAVE_COMPANY_RECORD_NAME_MAX_LEN, - ) { - Some(name) if !name.is_empty() && name.chars().all(is_save_name_char) => name, - _ => { - valid = false; - break; - } - }; - score += name.len(); - if active { - score += 8; - } - if linked != 0 { - score += 2; - } - if company_id == (index + 1) as u32 { - score += 4; - } - } - if valid && score > best_score { - best_score = score; - best_start = Some(start); - } - } - - best_start -} - -fn parse_save_company_total_debt(bytes: &[u8], record_offset: usize) -> Option { - let bond_count = - read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize; - let mut total = 0u64; - for slot_index in 0..bond_count { - let slot_offset = record_offset - .checked_add(SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET)? - .checked_add(slot_index.checked_mul(SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE)?)?; - let principal = read_i32_at(bytes, slot_offset)?; - if principal > 0 { - total = total.checked_add(principal as u64)?; - } - } - Some(total) -} - -fn parse_save_company_live_bond_slots( - bytes: &[u8], - record_offset: usize, -) -> Option> { - let bond_count = - read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize; - let mut slots = Vec::new(); - for slot_index in 0..bond_count { - let slot_offset = record_offset - .checked_add(SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET)? - .checked_add(slot_index.checked_mul(SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE)?)?; - let principal = read_i32_at(bytes, slot_offset)?; - if principal <= 0 { - continue; - } - let maturity_year = read_u32_at(bytes, slot_offset + 4)?; - let coupon_rate_raw_u32 = read_u32_at(bytes, slot_offset + 8)?; - let coupon_rate = f32::from_bits(coupon_rate_raw_u32); - if !coupon_rate.is_finite() { - continue; - } - slots.push(crate::RuntimeCompanyBondSlot { - slot_index: slot_index as u32, - principal: principal as u32, - maturity_year, - coupon_rate_raw_u32, - }); - } - Some(slots) -} - -fn parse_save_company_largest_live_bond_principal( - bytes: &[u8], - record_offset: usize, -) -> Option> { - let mut largest_live_principal: Option = None; - for slot in parse_save_company_live_bond_slots(bytes, record_offset)? { - largest_live_principal = Some(match largest_live_principal { - Some(current) => current.max(slot.principal), - None => slot.principal, - }); - } - Some(largest_live_principal) -} - -fn parse_save_company_highest_coupon_live_bond_principal( - bytes: &[u8], - record_offset: usize, -) -> Option> { - let mut highest_coupon_principal = None; - let mut highest_coupon_rate = None; - for slot in parse_save_company_live_bond_slots(bytes, record_offset)? { - let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32); - match highest_coupon_rate { - Some(current_rate) if coupon_rate < current_rate => {} - Some(current_rate) if coupon_rate == current_rate => { - if let Some(current_principal) = highest_coupon_principal { - if slot.principal > current_principal { - highest_coupon_principal = Some(slot.principal); - } - } - } - _ => { - highest_coupon_rate = Some(coupon_rate); - highest_coupon_principal = Some(slot.principal); - } - } - } - Some(highest_coupon_principal) -} - -fn parse_save_company_available_track_laying_capacity( - bytes: &[u8], - record_offset: usize, -) -> Option> { - let raw = read_i32_at( - bytes, - record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET, - )?; - if raw < 0 { - Some(None) - } else { - Some(Some(raw as u32)) - } -} - -fn build_save_dword_candidate( - bytes: &[u8], - record_offset: usize, - label: &str, - relative_offset: usize, -) -> Option { - let raw_u32 = read_u32_at(bytes, record_offset + relative_offset)?; - Some(SmpSaveDwordCandidate { - label: label.to_string(), - relative_offset, - relative_offset_hex: format!("0x{relative_offset:x}"), - raw_u32, - raw_u32_hex: format!("0x{raw_u32:08x}"), - value_i32: raw_u32 as i32, - value_f32: f32::from_bits(raw_u32), - }) -} - -fn build_save_qword_candidate( - bytes: &[u8], - record_offset: usize, - relative_offset: usize, -) -> Option { - let raw_u64 = read_u64_at(bytes, record_offset + relative_offset)?; - Some(SmpSaveScalarCandidate { - relative_offset, - relative_offset_hex: format!("0x{relative_offset:x}"), - raw_u64, - raw_u64_hex: format!("0x{raw_u64:016x}"), - value_i64: raw_u64 as i64, - value_f64: f64::from_bits(raw_u64), - }) -} - -fn derive_chairman_holdings_share_price_total( - holdings_by_company: &BTreeMap, - company_share_prices: &BTreeMap, -) -> Option { - let mut total = 0i64; - for (company_id, units) in holdings_by_company { - let share_price = *company_share_prices.get(company_id)?; - total = total.checked_add((*units as i64).checked_mul(share_price)?)?; - } - Some(total) -} - -fn derive_chairman_cached_purchasing_power_total( - current_cash: i64, - cached_scalar_candidates: &[SmpSaveScalarCandidate], -) -> Option { - let strongest_cached_total = cached_scalar_candidates - .iter() - .filter_map(|candidate| round_f64_to_i64(candidate.value_f64)) - .filter(|value| *value >= 0) - .max()?; - current_cash.checked_add(strongest_cached_total) -} - -fn build_save_world_selection_role_analysis( - probe: &SmpSaveWorldSelectionContextProbe, -) -> SmpSaveWorldSelectionRoleAnalysis { - let chairman_slots = probe - .chairman_slot_selectors - .iter() - .copied() - .zip(probe.chairman_role_gate_bytes.iter().copied()) - .enumerate() - .map(|(slot_index, (selector_byte, role_gate_byte))| { - SmpSaveWorldSelectionRoleAnalysisEntry { - slot_index, - selector_byte, - selector_byte_hex: format!("0x{selector_byte:02x}"), - role_gate_byte, - role_gate_byte_hex: format!("0x{role_gate_byte:02x}"), - } - }) - .collect(); - - SmpSaveWorldSelectionRoleAnalysis { - selected_company_id: probe.selected_company_id, - selected_chairman_profile_id: probe.selected_chairman_profile_id, - campaign_override_flag: probe.campaign_override_flag, - campaign_override_flag_hex: probe.campaign_override_flag_hex.clone(), - chairman_slots, - } -} - -fn parse_save_chairman_profile_table_probe( - bytes: &[u8], - header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, - selection_probe: Option<&SmpSaveWorldSelectionContextProbe>, - company_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { - let header_probe = header_probe?; - let observed_entry_count = header_probe.live_record_count as usize; - if observed_entry_count == 0 { - return Some(SmpLoadedChairmanProfileTable { - source_kind: "save-chairman-profile-direct-records".to_string(), - semantic_family: "scenario-save-chairman-profile-direct-records".to_string(), - observed_entry_count, - selected_chairman_profile_id: selection_probe - .map(|probe| probe.selected_chairman_profile_id), - entries: Vec::new(), - }); - } - - let record_start_offset = - detect_save_chairman_profile_record_start_offset(bytes, header_probe)?; - let record_stride = header_probe.direct_record_stride as usize; - let base_offset = header_probe - .metadata_tag_offset - .checked_add(4)? - .checked_add(record_start_offset)?; - let company_id_bound = company_header_probe - .map(|probe| probe.live_id_bound) - .unwrap_or(0); - - let mut entries = Vec::with_capacity(observed_entry_count); - for index in 0..observed_entry_count { - let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?; - let profile_id = read_u32_at(bytes, record_offset)?; - let active = read_u32_at(bytes, record_offset + 4)? != 0; - let name = read_ascii_c_string_at( - bytes, - record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET, - SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN, - )?; - let current_cash = round_f64_to_i64(read_f64_at( - bytes, - record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET, - )?)?; - let linked_company_id = parse_nonzero_u32( - bytes, - record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, - )?; - let personality_byte_0x291 = read_u8_at( - bytes, - record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET, - )?; - let cache_0 = round_f64_to_i64(read_f64_at( - bytes, - record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET, - )?)?; - let cache_1 = round_f64_to_i64(read_f64_at( - bytes, - record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET, - )?)?; - let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS - .iter() - .map(|relative_offset| { - build_save_qword_candidate(bytes, record_offset, *relative_offset) - }) - .collect::>>()?; - let issue_opinion_terms_raw_i32 = build_save_i32_term_strip( - bytes, - record_offset, - SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET, - SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT, - )?; - let holdings_value_total = cache_0.max(cache_1).max(0); - let net_worth_total = current_cash.saturating_add(holdings_value_total); - let purchasing_power_total = - derive_chairman_cached_purchasing_power_total(current_cash, &cached_scalar_candidates) - .unwrap_or(net_worth_total); - let mut company_holdings = BTreeMap::new(); - for company_id in 1..=company_id_bound { - let slot_offset = record_offset - .checked_add(SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET)? - .checked_add((company_id as usize).checked_mul(4)?)?; - let units = read_u32_at(bytes, slot_offset)?; - if units != 0 { - company_holdings.insert(company_id, units); - } - } - entries.push(SmpLoadedChairmanProfileEntry { - profile_id, - name, - active, - current_cash, - linked_company_id, - company_holdings, - holdings_value_total, - net_worth_total, - purchasing_power_total, - personality_byte_0x291: Some(personality_byte_0x291), - issue_opinion_terms_raw_i32, - }); - } - - Some(SmpLoadedChairmanProfileTable { - source_kind: "save-chairman-profile-direct-records".to_string(), - semantic_family: "scenario-save-chairman-profile-direct-records".to_string(), - observed_entry_count, - selected_chairman_profile_id: selection_probe - .map(|probe| probe.selected_chairman_profile_id), - entries, - }) -} - -fn detect_save_chairman_profile_record_start_offset( - bytes: &[u8], - header_probe: &SmpSaveTaggedCollectionHeaderProbe, -) -> Option { - let observed_entry_count = header_probe.live_record_count as usize; - let record_stride = header_probe.direct_record_stride as usize; - let scan_limit = SAVE_CHAIRMAN_RECORD_START_SCAN_LIMIT.min(record_stride); - let base_offset = header_probe.metadata_tag_offset.checked_add(4)?; - let mut best_start = None; - let mut best_score = 0usize; - - for start in 0..scan_limit { - let mut score = 0usize; - let mut seen_ids = std::collections::BTreeSet::new(); - let mut valid = true; - for index in 0..observed_entry_count { - let record_offset = match base_offset - .checked_add(start) - .and_then(|offset| offset.checked_add(index.checked_mul(record_stride)?)) - { - Some(offset) => offset, - None => { - valid = false; - break; - } - }; - let profile_id = match read_u32_at(bytes, record_offset) { - Some(value) - if value >= 1 - && value <= header_probe.live_id_bound - && seen_ids.insert(value) => - { - value - } - _ => { - valid = false; - break; - } - }; - match read_u32_at(bytes, record_offset + 4) { - Some(0 | 1) => score += 1, - _ => { - valid = false; - break; - } - } - let name = match read_ascii_c_string_at( - bytes, - record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET, - SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN, - ) { - Some(name) if !name.is_empty() && name.chars().all(is_save_name_char) => name, - _ => { - valid = false; - break; - } - }; - match read_u32_at( - bytes, - record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, - ) { - Some(value) if value <= 0x100 => score += (value != 0) as usize, - _ => { - valid = false; - break; - } - } - match read_f64_at(bytes, record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET) { - Some(value) if value.is_finite() && value.abs() < 1.0e12 => { - score += name.len() + 4; - } - _ => { - valid = false; - break; - } - } - if profile_id == (index + 1) as u32 { - score += 4; - } - } - if valid && score > best_score { - best_score = score; - best_start = Some(start); - } - } - - best_start -} - -fn is_save_name_char(ch: char) -> bool { - ch.is_ascii_alphanumeric() - || matches!( - ch, - ' ' | '&' | '\'' | ',' | '.' | '-' | '/' | '(' | ')' | ':' - ) -} - -fn known_cargo_slot_definition(slot_id: u32) -> Option { - KNOWN_CARGO_SLOT_DEFINITIONS - .iter() - .copied() - .find(|definition| definition.slot_id == slot_id) -} - -fn known_cargo_slot_definition_for_descriptor_id( - descriptor_id: u32, -) -> Option { - KNOWN_CARGO_SLOT_DEFINITIONS - .iter() - .copied() - .find(|definition| definition.descriptor_id == descriptor_id) -} - -fn runtime_cargo_class_name(cargo_class: RuntimeCargoClass) -> &'static str { - match cargo_class { - RuntimeCargoClass::Factory => "factory", - RuntimeCargoClass::FarmMine => "farm_mine", - RuntimeCargoClass::Other => "other", - } -} - -fn parse_event_runtime_collection_summary( - bytes: &[u8], - container_profile: Option<&SmpContainerProfile>, - save_load_summary: Option<&SmpSaveLoadSummary>, -) -> Option { - parse_event_runtime_collection_summary_with_tag_width( - bytes, - container_profile, - save_load_summary, - 2, - ) - .or_else(|| { - parse_event_runtime_collection_summary_with_tag_width( - bytes, - container_profile, - save_load_summary, - 4, - ) - }) -} - -fn parse_event_runtime_collection_summary_with_tag_width( - bytes: &[u8], - container_profile: Option<&SmpContainerProfile>, - save_load_summary: Option<&SmpSaveLoadSummary>, - tag_width: usize, -) -> Option { - let (metadata_offsets, record_offsets, close_offsets) = match tag_width { - 2 => ( - find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG), - find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG), - find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG), - ), - 4 => ( - find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32), - find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32), - find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32), - ), - _ => return None, - }; - - for metadata_tag_offset in metadata_offsets { - let packed_state_version = read_u32_at(bytes, metadata_tag_offset + tag_width)?; - if packed_state_version != EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION { - continue; - } - - let records_tag_offset = record_offsets - .iter() - .copied() - .find(|offset| *offset > metadata_tag_offset + tag_width + 4)?; - let close_tag_offset = close_offsets - .iter() - .copied() - .find(|offset| *offset > records_tag_offset)?; - let metadata_payload = - bytes.get(metadata_tag_offset + tag_width + 4..records_tag_offset)?; - if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { - continue; - } - - let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT) - .map(|index| read_u32_at(metadata_payload, index * 4)) - .collect::>>()?; - let direct_collection_flag = header_words[0]; - let direct_record_stride = usize::try_from(header_words[1]).ok()?; - let live_id_bound = header_words[4]; - let live_record_count = usize::try_from(header_words[5]).ok()?; - - let records_payload = bytes.get(records_tag_offset + tag_width..close_tag_offset)?; - let (source_kind, live_entry_ids) = if direct_collection_flag == 0 && tag_width == 4 { - ( - "packed-event-runtime-collection-nondirect".to_string(), - (1..=u32::try_from(live_record_count).ok()?).collect::>(), - ) - } else { - if direct_collection_flag == 0 || direct_record_stride == 0 { - continue; - } - - let bitset_len = ((usize::try_from(live_id_bound).ok()?).saturating_add(15)) / 8; - let payload_bytes = direct_record_stride.checked_mul(live_record_count)?; - if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + bitset_len { - continue; - } - if metadata_payload.len() < bitset_len + payload_bytes { - continue; - } - - let bitset_offset = metadata_payload.len() - bitset_len - payload_bytes; - if bitset_offset < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { - continue; - } - let bitset = metadata_payload.get(bitset_offset..bitset_offset + bitset_len)?; - let live_entry_ids = - decode_live_entry_ids_from_tombstone_bitset(bitset, live_id_bound)?; - if live_entry_ids.len() != live_record_count { - continue; - } - ( - "packed-event-runtime-collection".to_string(), - live_entry_ids, - ) - }; - let records = if source_kind == "packed-event-runtime-collection-nondirect" { - try_parse_nondirect_event_runtime_record_summaries( - records_payload, - records_tag_offset + tag_width, - &live_entry_ids, - ) - .unwrap_or_else(|| { - parse_event_runtime_record_summaries( - records_payload, - records_tag_offset + tag_width, - &live_entry_ids, - ) - }) - } else { - parse_event_runtime_record_summaries( - records_payload, - records_tag_offset + tag_width, - &live_entry_ids, - ) - }; - let decoded_record_count = records - .iter() - .filter(|record| record.decode_status != "unsupported_framing") - .count(); - let imported_runtime_record_count = records - .iter() - .filter(|record| record.executable_import_ready) - .count(); - let records_with_trigger_kind = records - .iter() - .filter(|record| record.trigger_kind.is_some()) - .count(); - let records_missing_trigger_kind = records.len().saturating_sub(records_with_trigger_kind); - let nondirect_compact_record_count = records - .iter() - .filter(|record| record.payload_family == "real_packed_nondirect_compact_v1") - .count(); - let nondirect_compact_records_missing_trigger_kind = records - .iter() - .filter(|record| { - record.payload_family == "real_packed_nondirect_compact_v1" - && record.trigger_kind.is_none() - }) - .count(); - let mut trigger_kinds_present = records - .iter() - .filter_map(|record| record.trigger_kind) - .collect::>(); - trigger_kinds_present.sort_unstable(); - trigger_kinds_present.dedup(); - let mut mutation_candidate_record_indexes = records - .iter() - .filter(|record| { - record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - }) - }) - .map(|record| record.record_index) - .collect::>(); - mutation_candidate_record_indexes.sort_unstable(); - mutation_candidate_record_indexes.dedup(); - let mut mutation_candidate_opcodes = records - .iter() - .flat_map(|record| record.grouped_effect_rows.iter().map(|row| row.opcode)) - .filter(|opcode| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(*opcode) - }) - .collect::>(); - mutation_candidate_opcodes.sort_unstable(); - mutation_candidate_opcodes.dedup(); - let mut opcode_0x08_record_indexes = records - .iter() - .filter(|record| { - record - .grouped_effect_rows - .iter() - .any(|row| row.opcode == 0x08) - }) - .map(|record| record.record_index) - .collect::>(); - opcode_0x08_record_indexes.sort_unstable(); - opcode_0x08_record_indexes.dedup(); - let mut add_building_dispatch_strip_record_indexes = records - .iter() - .filter(|record| { - record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) - }) - }) - .map(|record| record.record_index) - .collect::>(); - add_building_dispatch_strip_record_indexes.sort_unstable(); - add_building_dispatch_strip_record_indexes.dedup(); - let add_building_dispatch_strip_records_with_trigger_kind = records - .iter() - .filter(|record| { - record.trigger_kind.is_some() - && record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) - }) - }) - .count(); - let add_building_dispatch_strip_records_missing_trigger_kind = records - .iter() - .filter(|record| { - record.trigger_kind.is_none() - && record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) - }) - }) - .count(); - let mut mutation_candidate_descriptor_labels = records - .iter() - .flat_map(|record| record.grouped_effect_rows.iter()) - .filter(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - }) - .filter_map(|row| row.descriptor_label.clone()) - .collect::>(); - mutation_candidate_descriptor_labels.sort_unstable(); - mutation_candidate_descriptor_labels.dedup(); - let mut add_building_dispatch_strip_descriptor_labels = records - .iter() - .flat_map(|record| record.grouped_effect_rows.iter()) - .filter(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) - }) - .filter_map(|row| row.descriptor_label.clone()) - .collect::>(); - add_building_dispatch_strip_descriptor_labels.sort_unstable(); - add_building_dispatch_strip_descriptor_labels.dedup(); - let mut add_building_dispatch_strip_row_shape_families = records - .iter() - .filter(|record| { - record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) - }) - }) - .map(|record| { - compact_event_dispatch_row_shape_family_from_summary_rows( - &record.grouped_effect_rows, - ) - }) - .collect::>(); - add_building_dispatch_strip_row_shape_families.sort_unstable(); - add_building_dispatch_strip_row_shape_families.dedup(); - let mut add_building_dispatch_strip_signature_families = records - .iter() - .filter(|record| { - record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) - }) - }) - .map(|record| { - compact_event_signature_family_from_notes(&record.notes) - .unwrap_or_else(|| "unknown-signature-family".to_string()) - }) - .collect::>(); - add_building_dispatch_strip_signature_families.sort_unstable(); - add_building_dispatch_strip_signature_families.dedup(); - let mut add_building_dispatch_strip_condition_tuple_families = records - .iter() - .filter(|record| { - record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) - }) - }) - .map(|record| { - compact_event_dispatch_condition_tuple_family_from_summary_rows( - &record.standalone_condition_rows, - ) - }) - .collect::>(); - add_building_dispatch_strip_condition_tuple_families.sort_unstable(); - add_building_dispatch_strip_condition_tuple_families.dedup(); - let mut add_building_dispatch_strip_signature_condition_clusters = records - .iter() - .filter(|record| { - record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && compact_event_dispatch_add_building_descriptor_id(row.descriptor_id) - }) - }) - .map(|record| { - compact_event_dispatch_signature_condition_cluster_from_summary_rows( - compact_event_signature_family_from_notes(&record.notes).as_deref(), - &record.standalone_condition_rows, - ) - }) - .collect::>(); - add_building_dispatch_strip_signature_condition_clusters.sort_unstable(); - add_building_dispatch_strip_signature_condition_clusters.dedup(); - let mut mutation_candidate_unknown_descriptor_ids = records - .iter() - .flat_map(|record| record.grouped_effect_rows.iter()) - .filter(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - && row.descriptor_label.is_none() - }) - .map(|row| row.descriptor_id) - .collect::>(); - mutation_candidate_unknown_descriptor_ids.sort_unstable(); - mutation_candidate_unknown_descriptor_ids.dedup(); - let mut mutation_candidate_special_condition_label_matches = - mutation_candidate_unknown_descriptor_ids - .iter() - .filter_map(|descriptor_id| { - known_special_condition_label_for_compact_descriptor_id(*descriptor_id) - .map(|label| format!("{descriptor_id} -> {label}")) - }) - .collect::>(); - mutation_candidate_special_condition_label_matches.sort(); - mutation_candidate_special_condition_label_matches.dedup(); - let mut dispatch_strip_unknown_condition_ids = records - .iter() - .filter(|record| { - record.grouped_effect_rows.iter().any(|row| { - opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(row.opcode) - }) - }) - .flat_map(|record| record.standalone_condition_rows.iter()) - .filter(|row| row.raw_condition_id >= 0 && row.metric.is_none()) - .map(|row| row.raw_condition_id) - .collect::>(); - dispatch_strip_unknown_condition_ids.sort_unstable(); - dispatch_strip_unknown_condition_ids.dedup(); - let mut control_lane_notes = Vec::new(); - if nondirect_compact_record_count != 0 - && nondirect_compact_record_count == nondirect_compact_records_missing_trigger_kind - { - control_lane_notes.push( - "all compact non-direct rows currently decode row bodies only and still lack a decoded trigger/control lane".to_string(), - ); - control_lane_notes.push( - "direct disassembly now grounds that as a real loader boundary: 0x0042db20 allocates linked 0x1e/0x28 row nodes from the 0x4e9a slice and leaves [event+0x7ee..0x80f] to the separate 0x0042e050 full-event clone path, so missing trigger-kind bytes on this family are not just a parser gap".to_string(), - ); - control_lane_notes.push( - "the checked 0x0042e050 caller census is narrow too: current direct caller 0x004dba23 sits under the event-editor duplication path rather than 0x00433130 load, so ordinary 0x4e9a restore is not currently grounded to inherit trigger/control bytes through that deep-copy seam".to_string(), - ); - control_lane_notes.push( - "the first non-editor positive control-lane writer is bounded away from ordinary restore too: 0x00430b50 allocates a fresh live runtime-effect row through 0x00432ea0 -> 0x0042d5a0, seeds [event+0x7ef] to 2 or 3 plus adjacent control bytes, and is reached from 0x004323a0 service at 0x0043232e rather than 0x00433130 load".to_string(), - ); - control_lane_notes.push( - "the remaining non-editor [event+0x7ef] mutators are bounded away from restore too: the 0x00443200..0x004436e3 sweep searches existing live runtime-event names through 0x005a57cf (including strings like 'New Beginnings', 'Chicago to New York', 'The American', and 'Labor') and retags already-live records, so it reads as scenario-specific live maintenance rather than the missing 0x4e9a restore owner".to_string(), - ); - control_lane_notes.push( - "direct disassembly of 0x004323a0 now makes the live gate explicit too: the per-record service returns before dispatch unless one-shot latch [event+0x81f] is clear, mode byte [event+0x7ef] matches the selected trigger kind from 0x00432f40, and compact chain root [event+0x00] is nonzero; its kind-8 side path at 0x00432ca1..0x00432cb0 only calls 0x00438710 on already-live records with [event+0x7ef] == 8".to_string(), - ); - control_lane_notes.push( - "the post-load name-driven retagger is narrower than a bulk trigger-kind materializer too: direct disassembly of 0x00442c30 (called from 0x00443a50 at 0x00444b50) shows a hardcoded scenario-name patch table over already-live records in 0x0062be18/0x0062bae0, and the checked cases mostly tweak modifier bytes [event+0x7f9/+0x7fa] or nested payload scalars on records that already carry concrete kinds such as 7 ('Open Aus', 'The American'), 6 ('Test connections'), 5 ('Win - Gold'), and 1 ('Win - Silver' / 'Win - Bronze')".to_string(), - ); - control_lane_notes.push( - "direct disassembly now boxes in the explicit trigger-kind materializations inside that same retagger too: the 'SP - GOLD' branch at 0x00443526 rewrites [event+0x7ef] from 1 to 5 on live runtime-event id 1 when the scenario flag [world+0x66de] is set and payload-root kind 7 carries subtype byte 5, while the 'Labor' branch at 0x00443601 rewrites [event+0x7ef] from 0 to 2 on live runtime-event id 0x0d when the same scenario flag is set and the checked 0x3c -> 0x3d child payload pair carries the matching negative scalar sentinel".to_string(), - ); - } - if records_with_trigger_kind != 0 { - control_lane_notes.push(format!( - "decoded trigger kinds present in this collection = {:?}", - trigger_kinds_present - )); - } - if !mutation_candidate_record_indexes.is_empty() { - control_lane_notes.push(format!( - "records with grouped opcodes already in the 0x00431b20 dispatch strip = {:?}", - mutation_candidate_record_indexes - )); - if records_with_trigger_kind == 0 { - control_lane_notes.push( - "decoded grouped rows already reach the 0x00431b20 dispatch strip in this collection even though the current inspection surface recovered no trigger/control kind bytes for those records" - .to_string(), - ); - if nondirect_compact_record_count == records.len() { - control_lane_notes.push( - "every currently decoded dispatch-strip row in this collection still sits in the nondirect compact 0x4e99/0x4e9a/0x4e9b family with null [event+0x7ef], so the direct full-record 0x4e21/0x4e22 framing path is not currently bridging trigger-kind control bytes for these mutation-capable rows".to_string(), - ); - } - } - control_lane_notes.push(format!( - "0x00431b20 dispatch-strip opcodes present in decoded grouped rows = {:?}", - mutation_candidate_opcodes - )); - if !mutation_candidate_descriptor_labels.is_empty() { - control_lane_notes.push(format!( - "decoded grouped descriptor labels present in the 0x00431b20 dispatch strip = {:?}", - mutation_candidate_descriptor_labels - )); - } - if !mutation_candidate_unknown_descriptor_ids.is_empty() { - control_lane_notes.push(format!( - "grouped descriptor ids still missing checked-in labels in the 0x00431b20 dispatch strip = {:?}", - mutation_candidate_unknown_descriptor_ids - )); - } - if !mutation_candidate_special_condition_label_matches.is_empty() { - control_lane_notes.push(format!( - "unlabeled 0x00431b20 dispatch-strip descriptor ids matching known special-condition label_id-2000 values = {:?}", - mutation_candidate_special_condition_label_matches - )); - } - if !dispatch_strip_unknown_condition_ids.is_empty() { - control_lane_notes.push(format!( - "standalone condition ids still missing checked-in labels in the 0x00431b20 dispatch strip = {:?}", - dispatch_strip_unknown_condition_ids - )); - } - if !opcode_0x08_record_indexes.is_empty() { - control_lane_notes.push(format!( - "records with opcode 0x08 in the 0x00431b20 dispatch strip = {:?}", - opcode_0x08_record_indexes - )); - control_lane_notes.push( - "checked-in function-map evidence currently grounds opcode 0x08 on the 0x00426d60 company_deactivate_and_clear_chairman_share_links branch".to_string(), - ); - } - if !add_building_dispatch_strip_record_indexes.is_empty() { - control_lane_notes.push(format!( - "records with Add Building descriptors in the 0x00431b20 dispatch strip = {:?}", - add_building_dispatch_strip_record_indexes - )); - control_lane_notes.push(format!( - "decoded Add Building descriptor labels present in the 0x00431b20 dispatch strip = {:?}", - add_building_dispatch_strip_descriptor_labels - )); - control_lane_notes.push(format!( - "Add Building row-shape families present in the 0x00431b20 dispatch strip = {:?}", - add_building_dispatch_strip_row_shape_families - )); - control_lane_notes.push(format!( - "Add Building signature families present in the 0x00431b20 dispatch strip = {:?}", - add_building_dispatch_strip_signature_families - )); - control_lane_notes.push(format!( - "Add Building condition-tuple families present in the 0x00431b20 dispatch strip = {:?}", - add_building_dispatch_strip_condition_tuple_families - )); - control_lane_notes.push(format!( - "Add Building signature/condition clusters present in the 0x00431b20 dispatch strip = {:?}", - add_building_dispatch_strip_signature_condition_clusters - )); - if add_building_dispatch_strip_records_with_trigger_kind == 0 { - control_lane_notes.push( - "every currently decoded Add Building dispatch-strip row still has null trigger kind, so the missing control-lane mapping remains the blocker above the already-grounded add-building descriptor bridge".to_string(), - ); - } - } - } - - return Some(SmpLoadedEventRuntimeCollectionSummary { - source_kind, - mechanism_family: save_load_summary - .map(|summary| summary.mechanism_family.clone()) - .unwrap_or_else(|| "unknown".to_string()), - mechanism_confidence: save_load_summary - .map(|summary| summary.mechanism_confidence.clone()) - .unwrap_or_else(|| "inferred".to_string()), - container_profile_family: container_profile - .map(|profile| profile.profile_family.clone()), - metadata_tag_offset, - records_tag_offset, - close_tag_offset, - packed_state_version, - packed_state_version_hex: format!("0x{packed_state_version:08x}"), - live_id_bound, - live_record_count, - live_entry_ids, - decoded_record_count, - imported_runtime_record_count, - records_with_trigger_kind, - records_missing_trigger_kind, - nondirect_compact_record_count, - nondirect_compact_records_missing_trigger_kind, - trigger_kinds_present, - add_building_dispatch_strip_record_indexes, - add_building_dispatch_strip_descriptor_labels, - add_building_dispatch_strip_records_with_trigger_kind, - add_building_dispatch_strip_records_missing_trigger_kind, - add_building_dispatch_strip_row_shape_families, - add_building_dispatch_strip_signature_families, - add_building_dispatch_strip_condition_tuple_families, - add_building_dispatch_strip_signature_condition_clusters, - control_lane_notes, - records, - }); - } - - None -} - -fn opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(opcode: u8) -> bool { - matches!(opcode, 0x04..=0x08 | 0x0d | 0x10..=0x13 | 0x16) -} - -fn compact_event_dispatch_add_building_descriptor_id(descriptor_id: u32) -> bool { - (503..=613).contains(&descriptor_id) -} - -fn compact_event_signature_family_from_notes(notes: &[String]) -> Option { - notes.iter().find_map(|note| { - note.strip_prefix("compact signature family = ") - .map(ToString::to_string) - }) -} - -fn compact_event_dispatch_condition_tuple_family_from_summary_rows( - rows: &[SmpLoadedPackedEventConditionRowSummary], -) -> String { - if rows.is_empty() { - return "[]".to_string(); - } - let parts = rows - .iter() - .map(|row| match &row.metric { - Some(metric) => format!("{}:{}:{}", row.raw_condition_id, row.subtype, metric), - None => format!("{}:{}", row.raw_condition_id, row.subtype), - }) - .collect::>(); - format!("[{}]", parts.join(",")) -} - -fn compact_event_dispatch_row_shape_family_from_summary_rows( - rows: &[SmpLoadedPackedEventGroupedEffectRowSummary], -) -> String { - if rows.is_empty() { - return "[]".to_string(); - } - let parts = rows - .iter() - .map(|row| { - format!( - "{}:{}:{}", - row.group_index, row.opcode, row.raw_scalar_value - ) - }) - .collect::>(); - format!("[{}]", parts.join(",")) -} - -fn compact_event_dispatch_signature_condition_cluster_from_summary_rows( - signature_family: Option<&str>, - rows: &[SmpLoadedPackedEventConditionRowSummary], -) -> String { - format!( - "{} :: {}", - signature_family.unwrap_or("unknown-signature-family"), - compact_event_dispatch_condition_tuple_family_from_summary_rows(rows) - ) -} - -fn known_special_condition_label_for_compact_descriptor_id( - descriptor_id: u32, -) -> Option<&'static str> { - let label_id = descriptor_id.checked_add(2000)?; - KNOWN_SPECIAL_CONDITION_DEFINITIONS - .iter() - .find(|definition| definition.label_id == label_id) - .map(|definition| definition.label) -} - -fn try_parse_nondirect_event_runtime_record_summaries( - records_payload: &[u8], - records_payload_offset: usize, - live_entry_ids: &[u32], -) -> Option> { - let marker_offsets = - find_u32_le_offsets(records_payload, PACKED_EVENT_REAL_CONDITION_MARKER as u32); - if marker_offsets.len() != live_entry_ids.len() || marker_offsets.first().copied() != Some(0) { - return None; - } - - let mut record_offsets = marker_offsets; - record_offsets.push(records_payload.len()); - let mut records = Vec::with_capacity(live_entry_ids.len()); - - for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() { - let start = *record_offsets.get(record_index)?; - let end = *record_offsets.get(record_index + 1)?; - let record_body = records_payload.get(start..end)?; - let record = parse_nondirect_event_runtime_record_summary( - record_body, - records_payload_offset + start, - record_index, - live_entry_id, - ) - .or_else(|| { - build_nondirect_event_runtime_record_summary_from_signatures( - record_body, - records_payload_offset + start, - record_index, - live_entry_id, - ) - })?; - records.push(record); - } - - Some(records) -} - -fn parse_nondirect_event_runtime_record_summary( - record_body: &[u8], - payload_offset: usize, - record_index: usize, - live_entry_id: u32, -) -> Option { - let mut cursor = 0usize; - if read_u32_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER as u32 { - return None; - } - cursor += 4; - - let standalone_condition_row_count = usize::try_from(read_u32_at(record_body, cursor)?).ok()?; - cursor += 4; - let mut standalone_condition_rows = Vec::with_capacity(standalone_condition_row_count); - for row_index in 0..standalone_condition_row_count { - let remaining_minimum = standalone_condition_row_count - .checked_sub(row_index + 1)? - .checked_mul(PACKED_EVENT_NONDIRECT_CONDITION_ROW_SERIALIZED_LEN)? - .checked_add(4)? - .checked_add(PACKED_EVENT_REAL_GROUP_COUNT.checked_mul(4)?)? - .checked_add(4)?; - let (row, consumed_len) = parse_nondirect_condition_row_summary( - record_body.get(cursor..)?, - row_index, - remaining_minimum, - )?; - standalone_condition_rows.push(row); - cursor += consumed_len; - } - - let grouped_marker_relative_offset = cursor; - if read_u32_at(record_body, cursor)? != PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER as u32 { - return None; - } - cursor += 4; - - let mut grouped_effect_row_counts = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); - let mut grouped_effect_rows = Vec::new(); - for group_index in 0..PACKED_EVENT_REAL_GROUP_COUNT { - let group_row_count = usize::try_from(read_u32_at(record_body, cursor)?).ok()?; - cursor += 4; - grouped_effect_row_counts.push(group_row_count); - for row_index in 0..group_row_count { - let remaining_groups_minimum = (group_row_count - row_index - 1) - .checked_mul(PACKED_EVENT_NONDIRECT_GROUPED_EFFECT_ROW_SERIALIZED_LEN)? - .checked_add((PACKED_EVENT_REAL_GROUP_COUNT - group_index - 1).checked_mul(4)?)? - .checked_add(4)?; - let (row, consumed_len) = parse_nondirect_grouped_effect_row_summary( - record_body.get(cursor..)?, - group_index, - row_index, - remaining_groups_minimum, - )?; - grouped_effect_rows.push(row); - cursor += consumed_len; - } - } - - let end_marker_relative_offset = cursor; - if read_u32_at(record_body, cursor)? != PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER as u32 { - return None; - } - cursor += 4; - if cursor != record_body.len() { - return None; - } - - let head_signature_words = read_u16_window(record_body, 0, 18); - let post_group_signature_words = - read_u16_window(record_body, grouped_marker_relative_offset + 4, 12); - let ascii_preview_before_grouped_marker = record_body - .get(..grouped_marker_relative_offset) - .map(ascii_preview); - - let mut notes = vec![ - "decoded from compact non-direct 0x4e99/0x4e9a/0x4e9b map-bundle row framing recovered from the paired 0x433060/0x430d70 writer strip and 0x433130/0x42db20 loader strip".to_string(), - format!( - "compact signature family = {}", - compact_nondirect_signature_family( - Some(grouped_marker_relative_offset), - &head_signature_words, - &post_group_signature_words, - ) - ), - format!( - "head signature u16 words = {}", - format_u16_word_signature(&head_signature_words) - ), - format!( - "grouped-effect marker 0x4eb8 at relative offset +0x{grouped_marker_relative_offset:x}" - ), - format!( - "row terminator marker 0x4eb9 at relative offset +0x{end_marker_relative_offset:x}" - ), - ]; - if !post_group_signature_words.is_empty() { - notes.push(format!( - "post-group signature u16 words = {}", - format_u16_word_signature(&post_group_signature_words) - )); - } - if let Some(preview) = ascii_preview_before_grouped_marker { - notes.push(format!("ascii preview before grouped marker = {preview}")); - } - notes.push(format!( - "compact non-direct grouped row counts by group = {:?}", - grouped_effect_row_counts - )); - notes.push( - "the compact non-direct row body reconstructs standalone/grouped rows only; the separate 0x42e050 full-event clone helper is the nearby owner that copies text bands plus control lane [event+0x7ee..0x80f] between live runtime-event rows".to_string(), - ); - notes.push( - "direct disassembly of 0x0042db20 now grounds that absence too: this loader allocates 0x1e-byte standalone-condition nodes and 0x28-byte grouped-row nodes from the 0x4e9a slice, but does not materialize the compact control lane [event+0x7ee..0x80f] or a trigger-kind byte for this non-direct row family".to_string(), - ); - notes.push( - "the adjacent deep-copy seam is bounded too: current direct caller census shows 0x0042e050 reached from editor duplication at 0x004dba23, not from the ordinary 0x00433130 load path, so this non-direct row family is not currently grounded to inherit trigger/control bytes during restore through that helper".to_string(), - ); - notes.push( - "the first non-editor positive control-lane writer is bounded away from ordinary restore too: 0x00430b50 allocates a fresh live runtime-effect row through 0x00432ea0 -> 0x0042d5a0, seeds [event+0x7ef] to 2 or 3 plus adjacent control bytes, and is only reached from the 0x004323a0 follow-on service strip rather than the 0x00433130 nondirect load path".to_string(), - ); - - let decoded_conditions = decode_real_condition_rows(&standalone_condition_rows, None); - - Some(SmpLoadedPackedEventRecordSummary { - record_index, - live_entry_id, - payload_offset: Some(payload_offset), - payload_len: Some(cursor), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_nondirect_compact_v1".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count, - standalone_condition_rows, - negative_sentinel_scope: None, - grouped_effect_row_counts, - grouped_effect_rows, - decoded_conditions, - decoded_actions: Vec::new(), - executable_import_ready: false, - notes, - }) -} - -fn build_nondirect_event_runtime_record_summary_from_signatures( - record_body: &[u8], - payload_offset: usize, - record_index: usize, - live_entry_id: u32, -) -> Option { - let grouped_marker_relative_offset = - find_u32_le_offsets(record_body, PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER as u32) - .into_iter() - .next(); - let end_marker_relative_offset = find_u32_le_offsets( - record_body, - PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER as u32, - ) - .into_iter() - .next(); - let head_signature_words = read_u16_window(record_body, 0, 18); - let post_group_signature_words = grouped_marker_relative_offset - .map(|offset| offset + 4) - .map(|offset| read_u16_window(record_body, offset, 12)) - .unwrap_or_default(); - let ascii_preview_before_grouped_marker = grouped_marker_relative_offset - .and_then(|offset| record_body.get(..offset).map(ascii_preview)); - - let mut notes = vec![ - "decoded from non-direct 0x4e99/0x4e9a/0x4e9b map-bundle row segmentation using 0x526f-delimited slices".to_string(), - format!( - "compact signature family = {}", - compact_nondirect_signature_family( - grouped_marker_relative_offset, - &head_signature_words, - &post_group_signature_words, - ) - ), - format!( - "head signature u16 words = {}", - format_u16_word_signature(&head_signature_words) - ), - ]; - if let Some(offset) = grouped_marker_relative_offset { - notes.push(format!( - "grouped-effect marker 0x4eb8 at relative offset +0x{offset:x}" - )); - if !post_group_signature_words.is_empty() { - notes.push(format!( - "post-group signature u16 words = {}", - format_u16_word_signature(&post_group_signature_words) - )); - } - } - if let Some(offset) = end_marker_relative_offset { - notes.push(format!( - "row terminator marker 0x4eb9 at relative offset +0x{offset:x}" - )); - } - if let Some(preview) = ascii_preview_before_grouped_marker { - notes.push(format!("ascii preview before grouped marker = {preview}")); - } - - Some(SmpLoadedPackedEventRecordSummary { - record_index, - live_entry_id, - payload_offset: Some(payload_offset), - payload_len: Some(record_body.len()), - decode_status: "compact_nondirect_parity_only".to_string(), - payload_family: "real_packed_nondirect_compact_v1".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - notes, - }) -} - -fn parse_nondirect_condition_row_summary( - record_body: &[u8], - row_index: usize, - remaining_minimum: usize, -) -> Option<(SmpLoadedPackedEventConditionRowSummary, usize)> { - let mut cursor = 0usize; - let mut row_bytes = vec![0u8; PACKED_EVENT_REAL_CONDITION_ROW_LEN]; - row_bytes - .get_mut(0..4)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes[4] = read_u8_at(record_body, cursor)?; - cursor += 1; - row_bytes - .get_mut(5..9)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(9..13)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes[0x0d] = read_u8_at(record_body, cursor)?; - cursor += 1; - row_bytes - .get_mut(0x0e..0x12)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(0x12..0x16)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - let candidate_name = - maybe_parse_nondirect_optional_name_block(record_body, &mut cursor, remaining_minimum)?; - let mut row = parse_real_condition_row_summary(&row_bytes, row_index, candidate_name)?; - row.notes.push( - "condition row reconstructed from the compact non-direct serializer fields under 0x430e80" - .to_string(), - ); - Some((row, cursor)) -} - -fn parse_nondirect_grouped_effect_row_summary( - record_body: &[u8], - group_index: usize, - row_index: usize, - remaining_minimum: usize, -) -> Option<(SmpLoadedPackedEventGroupedEffectRowSummary, usize)> { - let mut cursor = 0usize; - let mut row_bytes = vec![0u8; PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN]; - row_bytes - .get_mut(0..4)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(4..8)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes[8] = read_u8_at(record_body, cursor)?; - cursor += 1; - row_bytes - .get_mut(9..13)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(0x0d..0x11)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(0x11..0x15)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(0x12..0x16)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(0x14..0x18)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(0x16..0x1a)? - .copy_from_slice(record_body.get(cursor..cursor + 4)?); - cursor += 4; - row_bytes - .get_mut(0x18..0x24)? - .copy_from_slice(record_body.get(cursor..cursor + 12)?); - cursor += 12; - let locomotive_name = - maybe_parse_nondirect_optional_name_block(record_body, &mut cursor, remaining_minimum)?; - let mut row = - parse_real_grouped_effect_row_summary(&row_bytes, group_index, row_index, locomotive_name)?; - row.notes.push( - "grouped effect row reconstructed from the compact non-direct serializer fields under 0x430f68" - .to_string(), - ); - Some((row, cursor)) -} - -fn maybe_parse_nondirect_optional_name_block( - record_body: &[u8], - cursor: &mut usize, - remaining_minimum: usize, -) -> Option> { - if record_body.len() < *cursor + PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN { - return Some(None); - } - if record_body.len() - < *cursor + PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN + remaining_minimum - { - return Some(None); - } - let block = - record_body.get(*cursor..*cursor + PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN)?; - let name = read_ascii_c_string_at(block, 0, PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN); - let Some(name) = name.filter(|name| { - !name.is_empty() - && block - .iter() - .copied() - .all(|byte| byte == 0 || is_ascii_preview_byte(byte)) - }) else { - return Some(None); - }; - *cursor += PACKED_EVENT_NONDIRECT_OPTIONAL_NAME_BLOCK_LEN; - Some(Some(name)) -} - -fn decode_live_entry_ids_from_tombstone_bitset( - bitset: &[u8], - live_id_bound: u32, -) -> Option> { - let ids = decode_live_entry_ids_with_mapping(bitset, live_id_bound, false); - if ids.is_some() { - return ids; - } - decode_live_entry_ids_with_mapping(bitset, live_id_bound, true) -} - -fn decode_live_entry_ids_with_mapping( - bitset: &[u8], - live_id_bound: u32, - subtract_one: bool, -) -> Option> { - let mut live_entry_ids = Vec::new(); - - for entry_id in 1..=live_id_bound { - let bit_index = if subtract_one { - entry_id.checked_sub(1)? - } else { - entry_id - }; - let byte_index = usize::try_from(bit_index / 8).ok()?; - let bit_mask = 1u8.checked_shl(bit_index % 8).unwrap_or(0); - let tombstone_byte = *bitset.get(byte_index)?; - if tombstone_byte & bit_mask == 0 { - live_entry_ids.push(entry_id); - } - } - - Some(live_entry_ids) -} - -fn parse_event_runtime_record_summaries( - records_payload: &[u8], - records_payload_offset: usize, - live_entry_ids: &[u32], -) -> Vec { - try_parse_synthetic_event_runtime_record_summaries( - records_payload, - records_payload_offset, - live_entry_ids, - ) - .or_else(|| { - try_parse_real_event_runtime_record_summaries( - records_payload, - records_payload_offset, - live_entry_ids, - ) - }) - .unwrap_or_else(|| { - build_unsupported_event_runtime_record_summaries( - live_entry_ids, - "0x4e9a payload did not match the current packed-event record decode harness", - ) - }) -} - -fn try_parse_synthetic_event_runtime_record_summaries( - records_payload: &[u8], - records_payload_offset: usize, - live_entry_ids: &[u32], -) -> Option> { - if !records_payload.starts_with(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC) { - return None; - } - - let mut cursor = PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC.len(); - let mut records = Vec::with_capacity(live_entry_ids.len()); - for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() { - let record_len = usize::try_from(read_u32_at(records_payload, cursor)?).ok()?; - cursor += 4; - let record_body = records_payload.get(cursor..cursor + record_len)?; - records.push(parse_synthetic_event_runtime_record_summary( - record_body, - records_payload_offset + cursor, - record_index, - live_entry_id, - )?); - cursor += record_len; - } - - if cursor != records_payload.len() { - return None; - } - - Some(records) -} - -fn parse_synthetic_event_runtime_record_summary( - record_body: &[u8], - payload_offset: usize, - record_index: usize, - live_entry_id: u32, -) -> Option { - if !record_body.starts_with(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC) { - return None; - } - - let mut cursor = PACKED_EVENT_RECORD_SYNTHETIC_MAGIC.len(); - let trigger_kind = read_u8_at(record_body, cursor)?; - cursor += 1; - let flags = read_u8_at(record_body, cursor)?; - cursor += 1; - let standalone_condition_row_count = usize::from(read_u8_at(record_body, cursor)?); - cursor += 1; - let action_count = usize::from(read_u8_at(record_body, cursor)?); - cursor += 1; - - let mut grouped_effect_row_counts = Vec::with_capacity(4); - for _ in 0..4 { - grouped_effect_row_counts.push(usize::from(read_u8_at(record_body, cursor)?)); - cursor += 1; - } - - let mut text_bands = Vec::with_capacity(PACKED_EVENT_TEXT_BAND_LABELS.len()); - for label in PACKED_EVENT_TEXT_BAND_LABELS { - let packed_len = usize::from(read_u16_at(record_body, cursor)?); - cursor += 2; - let band_bytes = record_body.get(cursor..cursor + packed_len)?; - cursor += packed_len; - text_bands.push(SmpLoadedPackedEventTextBandSummary { - label: label.to_string(), - packed_len, - present: packed_len != 0, - preview: ascii_preview(band_bytes), - }); - } - - let mut decoded_actions = Vec::with_capacity(action_count); - for _ in 0..action_count { - decoded_actions.push(parse_synthetic_packed_event_action( - record_body, - &mut cursor, - )?); - } - - if cursor != record_body.len() { - return None; - } - - let executable_import_ready = decoded_actions - .iter() - .all(runtime_effect_supported_for_save_import); - - Some(SmpLoadedPackedEventRecordSummary { - record_index, - live_entry_id, - payload_offset: Some(payload_offset), - payload_len: Some(record_body.len()), - decode_status: if executable_import_ready { - "executable".to_string() - } else { - "parity_only".to_string() - }, - payload_family: "synthetic_harness".to_string(), - trigger_kind: Some(trigger_kind), - active: Some(flags & 0x01 != 0), - marks_collection_dirty: Some(flags & 0x02 != 0), - one_shot: Some(flags & 0x04 != 0), - compact_control: None, - text_bands, - standalone_condition_row_count, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts, - grouped_effect_rows: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions, - executable_import_ready, - notes: vec!["decoded from the current synthetic packed-event record harness".to_string()], - }) -} - -fn try_parse_real_event_runtime_record_summaries( - records_payload: &[u8], - records_payload_offset: usize, - live_entry_ids: &[u32], -) -> Option> { - let mut cursor = 0usize; - let mut records = Vec::with_capacity(live_entry_ids.len()); - - for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() { - let (record, consumed_len) = parse_real_event_runtime_record_summary( - records_payload.get(cursor..)?, - records_payload_offset + cursor, - record_index, - live_entry_id, - )?; - records.push(record); - cursor += consumed_len; - } - - if cursor != records_payload.len() { - return None; - } - - Some(records) -} - -fn parse_real_event_runtime_record_summary( - record_body: &[u8], - payload_offset: usize, - record_index: usize, - live_entry_id: u32, -) -> Option<(SmpLoadedPackedEventRecordSummary, usize)> { - let mut cursor = 0usize; - let mut text_bands = Vec::with_capacity(PACKED_EVENT_TEXT_BAND_LABELS.len()); - for label in PACKED_EVENT_TEXT_BAND_LABELS { - let packed_len = usize::from(read_u16_at(record_body, cursor)?); - cursor += 2; - let band_bytes = record_body.get(cursor..cursor + packed_len)?; - cursor += packed_len; - text_bands.push(SmpLoadedPackedEventTextBandSummary { - label: label.to_string(), - packed_len, - present: packed_len != 0, - preview: ascii_preview(band_bytes), - }); - } - - let compact_control = parse_optional_real_compact_control_summary(record_body, &mut cursor)?; - - if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER { - return None; - } - cursor += 2; - let standalone_condition_row_count = usize::from(read_u16_at(record_body, cursor)?); - cursor += 2; - - let mut standalone_condition_rows = Vec::with_capacity(standalone_condition_row_count); - for row_index in 0..standalone_condition_row_count { - let row_bytes = record_body.get(cursor..cursor + PACKED_EVENT_REAL_CONDITION_ROW_LEN)?; - cursor += PACKED_EVENT_REAL_CONDITION_ROW_LEN; - let candidate_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?; - standalone_condition_rows.push(parse_real_condition_row_summary( - row_bytes, - row_index, - candidate_name, - )?); - } - - if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER { - return None; - } - cursor += 2; - - let mut grouped_effect_row_counts = Vec::with_capacity(4); - for _ in 0..4 { - grouped_effect_row_counts.push(usize::from(read_u16_at(record_body, cursor)?)); - cursor += 2; - } - - let mut grouped_effect_rows = - Vec::with_capacity(grouped_effect_row_counts.iter().sum::()); - for (group_index, row_count) in grouped_effect_row_counts.iter().copied().enumerate() { - for row_index in 0..row_count { - let row_bytes = - record_body.get(cursor..cursor + PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN)?; - cursor += PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN; - let locomotive_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?; - grouped_effect_rows.push(parse_real_grouped_effect_row_summary( - row_bytes, - group_index, - row_index, - locomotive_name, - )?); - } - } - if let Some(control) = compact_control.as_ref() { - for row in &mut grouped_effect_rows { - let target_subject = derive_real_grouped_target_subject(row, control); - let target_scope_ordinal = control - .grouped_target_scope_ordinals_0x7fb - .get(row.group_index) - .copied(); - row.grouped_target_subject = target_subject - .map(real_grouped_target_subject_name) - .map(str::to_string); - row.grouped_target_scope = derive_real_grouped_target_scope_name( - row, - control, - target_subject, - target_scope_ordinal, - ); - let company_target_present = control - .grouped_target_scope_ordinals_0x7fb - .get(row.group_index) - .copied() - .and_then(real_grouped_company_target) - .is_some(); - let chairman_target_present = control - .grouped_target_scope_ordinals_0x7fb - .get(row.group_index) - .copied() - .is_some_and(real_grouped_chairman_target_supported_in_runtime); - let territory_target_present = control - .grouped_territory_selectors_0x80f - .get(row.group_index) - .is_some_and(|selector| *selector >= 0); - if row.descriptor_id == 15 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - && !company_target_present - && !territory_target_present - { - row.notes - .push("retire train row is missing company and territory scope".to_string()); - } - if row.descriptor_id == 3 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - && (!company_target_present || !territory_target_present) - { - row.notes - .push("territory access row is missing company or territory scope".to_string()); - } - if matches!(target_subject, Some(RealGroupedTargetSubject::Chairman)) - && !chairman_target_present - { - let ordinal = target_scope_ordinal.unwrap_or(u8::MAX); - row.notes.push(format!( - "chairman row uses unsupported grouped target scope ordinal {ordinal}" - )); - } - } - } - - let negative_sentinel_scope = compact_control.as_ref().and_then(|control| { - derive_negative_sentinel_scope_summary(&standalone_condition_rows, control) - }); - let decoded_conditions = - decode_real_condition_rows(&standalone_condition_rows, negative_sentinel_scope.as_ref()); - let decoded_actions = compact_control - .as_ref() - .map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control)) - .unwrap_or_default(); - let ordinary_condition_row_count = standalone_condition_rows - .iter() - .filter(|row| row.raw_condition_id >= 0) - .count(); - let executable_import_ready = !grouped_effect_rows.is_empty() - && decoded_actions.len() == grouped_effect_rows.len() - && decoded_conditions.len() == ordinary_condition_row_count - && decoded_actions - .iter() - .all(runtime_effect_supported_for_save_import) - && decoded_conditions - .iter() - .all(runtime_condition_supported_for_save_import); - let consumed_len = cursor; - Some(( - SmpLoadedPackedEventRecordSummary { - record_index, - live_entry_id, - payload_offset: Some(payload_offset), - payload_len: Some(consumed_len), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: compact_control.as_ref().map(|control| control.mode_byte_0x7ef), - active: None, - marks_collection_dirty: None, - one_shot: compact_control - .as_ref() - .map(|control| control.one_shot_header_0x7f5 != 0), - compact_control, - text_bands, - standalone_condition_row_count, - standalone_condition_rows, - negative_sentinel_scope, - grouped_effect_row_counts, - grouped_effect_rows, - decoded_conditions, - decoded_actions, - executable_import_ready, - notes: vec![ - "decoded from grounded real 0x4e9a row framing".to_string(), - "grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(), - ], - }, - consumed_len, - )) -} - -fn parse_optional_real_compact_control_summary( - record_body: &[u8], - cursor: &mut usize, -) -> Option> { - if read_u16_at(record_body, *cursor)? == PACKED_EVENT_REAL_CONDITION_MARKER { - return Some(None); - } - - let end = cursor.checked_add(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN)?; - let bytes = record_body.get(*cursor..end)?; - let mut local = 0usize; - let mode_byte_0x7ef = read_u8_at(bytes, local)?; - local += 1; - let primary_selector_0x7f0 = read_u32_at(bytes, local)?; - local += 4; - let grouped_mode_0x7f4 = read_u8_at(bytes, local)?; - local += 1; - let one_shot_header_0x7f5 = read_u32_at(bytes, local)?; - local += 4; - let modifier_flag_0x7f9 = read_u8_at(bytes, local)?; - local += 1; - let modifier_flag_0x7fa = read_u8_at(bytes, local)?; - local += 1; - - let mut grouped_target_scope_ordinals_0x7fb = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); - for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { - grouped_target_scope_ordinals_0x7fb.push(read_u8_at(bytes, local)?); - local += 1; - } - - let mut grouped_scope_checkboxes_0x7ff = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); - for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { - grouped_scope_checkboxes_0x7ff.push(read_u8_at(bytes, local)?); - local += 1; - } - - let summary_toggle_0x800 = read_u8_at(bytes, local)?; - local += 1; - - let mut grouped_territory_selectors_0x80f = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); - for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { - grouped_territory_selectors_0x80f.push(read_i32_at(bytes, local)?); - local += 4; - } - - if local != bytes.len() { - return None; - } - if read_u16_at(record_body, end)? != PACKED_EVENT_REAL_CONDITION_MARKER { - return None; - } - - *cursor = end; - Some(Some(SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef, - primary_selector_0x7f0, - grouped_mode_0x7f4, - one_shot_header_0x7f5, - modifier_flag_0x7f9, - modifier_flag_0x7fa, - grouped_target_scope_ordinals_0x7fb, - grouped_scope_checkboxes_0x7ff, - summary_toggle_0x800, - grouped_territory_selectors_0x80f, - })) -} - -fn parse_real_condition_row_summary( - row_bytes: &[u8], - row_index: usize, - candidate_name: Option, -) -> Option { - let raw_condition_id = read_u32_at(row_bytes, 0)? as i32; - let subtype = read_u8_at(row_bytes, 4)?; - let flag_bytes = row_bytes - .get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)? - .to_vec(); - let candidate_name_display = candidate_name.clone(); - let candidate_name_ref = candidate_name_display.as_deref(); - let ordinary_metadata = real_ordinary_condition_metadata(raw_condition_id); - let comparator = ordinary_metadata - .and_then(|_| decode_real_condition_comparator(subtype)) - .map(condition_comparator_label); - let metric = ordinary_metadata - .map(|metadata| real_ordinary_condition_metric_label(metadata, candidate_name_ref)); - let threshold = ordinary_metadata.and_then(|_| decode_real_condition_threshold(&flag_bytes)); - let requires_candidate_name_binding = ordinary_metadata.is_some_and(|metadata| { - matches!( - metadata.kind, - RealOrdinaryConditionKind::Numeric( - RealOrdinaryConditionMetric::Territory(_) - | RealOrdinaryConditionMetric::CompanyTerritory(_) - ) - ) && candidate_name.is_some() - }); - let mut notes = Vec::new(); - if raw_condition_id < 0 { - notes.push("negative sentinel-style condition row id".to_string()); - } - if candidate_name.is_some() { - notes.push("condition row carries candidate-name side string".to_string()); - } - if ordinary_metadata.is_none() && raw_condition_id >= 0 { - notes.push( - "ordinary condition id is not yet recovered in the checked-in condition table" - .to_string(), - ); - } - if ordinary_metadata.is_some_and(|metadata| { - matches!( - metadata.kind, - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) - ) && candidate_name.is_none() - }) { - notes.push( - "candidate-availability condition row is missing its candidate-name side string" - .to_string(), - ); - } - if ordinary_metadata.is_some_and(|metadata| { - matches!( - metadata.kind, - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::NamedLocomotiveAvailability - | RealWorldConditionKind::NamedLocomotiveCost - ) - ) && candidate_name.is_none() - }) { - notes.push("named locomotive condition row is missing its side-string binding".to_string()); - } - if ordinary_metadata.is_some_and(|metadata| { - matches!( - metadata.kind, - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) - ) && candidate_name.is_none() - }) { - notes.push( - "named cargo-production condition row is missing its side-string binding".to_string(), - ); - } - if ordinary_metadata.is_some_and(|metadata| { - matches!( - metadata.kind, - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::FactoryProductionTotal - | RealWorldConditionKind::FarmMineProductionTotal - | RealWorldConditionKind::OtherCargoProductionTotal - ) - ) - }) { - notes.push( - "checked-in RT3.lng label is known, but this cargo aggregate condition family is not yet lowered" - .to_string(), - ); - } - if ordinary_metadata.is_some_and(|metadata| { - matches!( - metadata.kind, - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) - ) && candidate_name_ref - .and_then(recovered_cargo_production_slot_from_condition_name) - .is_none() - }) { - notes.push( - "named cargo-production condition side string does not yet map to a checked-in cargo slot" - .to_string(), - ); - } - Some(SmpLoadedPackedEventConditionRowSummary { - row_index, - raw_condition_id, - subtype, - flag_bytes, - candidate_name, - comparator, - metric, - semantic_family: ordinary_metadata - .map(|metadata| real_ordinary_condition_semantic_family(metadata).to_string()), - semantic_preview: ordinary_metadata.and_then(|metadata| { - threshold.map(|value| { - let comparator_text = decode_real_condition_comparator(subtype) - .map(condition_comparator_symbol) - .unwrap_or("?"); - let metric_label = - real_ordinary_condition_metric_label(metadata, candidate_name_ref); - format!("Test {} {} {}", metric_label, comparator_text, value) - }) - }), - recovered_cargo_slot: ordinary_metadata.and_then(|metadata| match metadata.kind { - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { - candidate_name_ref.and_then(recovered_cargo_production_slot_from_condition_name) - } - _ => None, - }), - recovered_cargo_class: ordinary_metadata.and_then(|metadata| match metadata.kind { - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { - candidate_name_ref - .and_then(recovered_cargo_production_slot_from_condition_name) - .and_then(known_cargo_slot_definition) - .map(|definition| runtime_cargo_class_name(definition.cargo_class).to_string()) - } - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::FactoryProductionTotal, - ) => Some("factory".to_string()), - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::FarmMineProductionTotal, - ) => Some("farm_mine".to_string()), - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::OtherCargoProductionTotal, - ) => Some("other".to_string()), - _ => None, - }), - requires_candidate_name_binding, - notes, - }) -} - -fn derive_negative_sentinel_scope_summary( - rows: &[SmpLoadedPackedEventConditionRowSummary], - control: &SmpLoadedPackedEventCompactControlSummary, -) -> Option { - let source_row_indexes = rows - .iter() - .filter(|row| row.raw_condition_id == -1) - .map(|row| row.row_index) - .collect::>(); - if source_row_indexes.is_empty() { - return None; - } - - Some(SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: decode_company_condition_test_scope(control.modifier_flag_0x7f9)?, - player_test_scope: decode_player_condition_test_scope(control.modifier_flag_0x7fa)?, - territory_scope_selector_is_0x63: control.primary_selector_0x7f0 == 0x63, - source_row_indexes, - }) -} - -fn decode_company_condition_test_scope(value: u8) -> Option { - match value { - 0 => Some(RuntimeCompanyConditionTestScope::Disabled), - 1 => Some(RuntimeCompanyConditionTestScope::AllCompanies), - 2 => Some(RuntimeCompanyConditionTestScope::SelectedCompanyOnly), - 3 => Some(RuntimeCompanyConditionTestScope::AiCompaniesOnly), - 4 => Some(RuntimeCompanyConditionTestScope::HumanCompaniesOnly), - _ => None, - } -} - -fn decode_player_condition_test_scope(value: u8) -> Option { - match value { - 0 => Some(RuntimePlayerConditionTestScope::Disabled), - 1 => Some(RuntimePlayerConditionTestScope::AllPlayers), - 2 => Some(RuntimePlayerConditionTestScope::SelectedPlayerOnly), - 3 => Some(RuntimePlayerConditionTestScope::AiPlayersOnly), - 4 => Some(RuntimePlayerConditionTestScope::HumanPlayersOnly), - _ => None, - } -} - -fn real_ordinary_condition_metadata( - raw_condition_id: i32, -) -> Option { - REAL_ORDINARY_CONDITION_METADATA - .iter() - .copied() - .find(|metadata| metadata.raw_condition_id == raw_condition_id) - .or_else(|| { - known_special_condition_definition_for_label_id(raw_condition_id as u32).map( - |definition| { - let kind = if let Some(world_toggle) = - real_grouped_effect_descriptor_metadata(110 + definition.slot_index as u32) - .filter(|metadata| metadata.parameter_family == "world_flag_toggle") - { - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { - key: world_toggle.runtime_key.unwrap_or(world_toggle.label), - }) - } else { - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::SpecialCondition { - label: definition.label, - }, - ) - }; - RealOrdinaryConditionMetadata { - raw_condition_id, - label: definition.label, - kind, - } - }, - ) - }) -} - -fn real_ordinary_condition_metric_label( - metadata: RealOrdinaryConditionMetadata, - candidate_name: Option<&str>, -) -> String { - match metadata.kind { - RealOrdinaryConditionKind::Numeric(_) => metadata.label.to_string(), - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition { - label, - }) => { - format!("Special Condition: {label}") - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => { - match candidate_name { - Some(name) => format!("Candidate Availability: {name}"), - None => "Candidate Availability".to_string(), - } - } - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::NamedLocomotiveAvailability, - ) => match candidate_name { - Some(name) => format!("Named Locomotive Availability: {name}"), - None => "Named Locomotive Availability".to_string(), - }, - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => { - match candidate_name { - Some(name) => format!("Named Locomotive Cost: {name}"), - None => "Named Locomotive Cost".to_string(), - } - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { - match candidate_name { - Some(name) => format!("Cargo Production: {name}"), - None => "Cargo Production".to_string(), - } - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => { - "Cargo Production Total".to_string() - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal) => { - "Factory Production Total".to_string() - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FarmMineProductionTotal) => { - "Farm/Mine Production Total".to_string() - } - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::OtherCargoProductionTotal, - ) => "Other Cargo Production Total".to_string(), - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::LimitedTrackBuildingAmount, - ) => "Limited Track Building Amount".to_string(), - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => { - "Territory Access Cost".to_string() - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => { - "Economic Status".to_string() - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { .. }) => { - format!("World Flag: {}", metadata.label) - } - } -} - -fn real_ordinary_condition_semantic_family( - metadata: RealOrdinaryConditionMetadata, -) -> &'static str { - match metadata.kind { - RealOrdinaryConditionKind::Numeric(_) => "numeric_threshold", - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { .. }) => { - "world_flag_equals" - } - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::NamedLocomotiveAvailability - | RealWorldConditionKind::NamedLocomotiveCost - | RealWorldConditionKind::CargoProductionSlot - | RealWorldConditionKind::CargoProductionTotal - | RealWorldConditionKind::FactoryProductionTotal - | RealWorldConditionKind::FarmMineProductionTotal - | RealWorldConditionKind::OtherCargoProductionTotal - | RealWorldConditionKind::LimitedTrackBuildingAmount - | RealWorldConditionKind::TerritoryAccessCost, - ) => "world_scalar_threshold", - RealOrdinaryConditionKind::WorldState(_) => "world_state_threshold", - } -} - -fn decode_real_condition_comparator(subtype: u8) -> Option { - match subtype { - 0 => Some(RuntimeConditionComparator::Ge), - 1 => Some(RuntimeConditionComparator::Le), - 2 => Some(RuntimeConditionComparator::Gt), - 3 => Some(RuntimeConditionComparator::Lt), - 4 => Some(RuntimeConditionComparator::Eq), - 5 => Some(RuntimeConditionComparator::Ne), - _ => None, - } -} - -fn decode_real_condition_threshold(flag_bytes: &[u8]) -> Option { - let raw = flag_bytes.get(0..4)?; - let mut bytes = [0u8; 4]; - bytes.copy_from_slice(raw); - Some(i32::from_le_bytes(bytes).into()) -} - -fn condition_comparator_label(comparator: RuntimeConditionComparator) -> String { - match comparator { - RuntimeConditionComparator::Ge => "ge".to_string(), - RuntimeConditionComparator::Le => "le".to_string(), - RuntimeConditionComparator::Gt => "gt".to_string(), - RuntimeConditionComparator::Lt => "lt".to_string(), - RuntimeConditionComparator::Eq => "eq".to_string(), - RuntimeConditionComparator::Ne => "ne".to_string(), - } -} - -fn condition_comparator_symbol(comparator: RuntimeConditionComparator) -> &'static str { - match comparator { - RuntimeConditionComparator::Ge => ">=", - RuntimeConditionComparator::Le => "<=", - RuntimeConditionComparator::Gt => ">", - RuntimeConditionComparator::Lt => "<", - RuntimeConditionComparator::Eq => "==", - RuntimeConditionComparator::Ne => "!=", - } -} - -fn parse_real_grouped_effect_row_summary( - row_bytes: &[u8], - group_index: usize, - row_index: usize, - locomotive_name: Option, -) -> Option { - let descriptor_id = read_u32_at(row_bytes, 0)?; - let raw_scalar_value = read_u32_at(row_bytes, 4)? as i32; - let opcode = read_u8_at(row_bytes, 8)?; - let value_byte_0x09 = read_u8_at(row_bytes, 9)?; - let value_dword_0x0d = read_u32_at(row_bytes, 0x0d)?; - let value_byte_0x11 = read_u8_at(row_bytes, 0x11)?; - let value_byte_0x12 = read_u8_at(row_bytes, 0x12)?; - let value_word_0x14 = read_u16_at(row_bytes, 0x14)?; - let value_word_0x16 = read_u16_at(row_bytes, 0x16)?; - let descriptor_metadata = real_grouped_effect_descriptor_metadata(descriptor_id); - let mut row_shape = classify_real_grouped_effect_row_shape( - opcode, - raw_scalar_value, - value_byte_0x11, - value_byte_0x12, - value_word_0x14, - value_word_0x16, - ) - .to_string(); - let mut semantic_family = classify_real_grouped_effect_semantic_family( - opcode, - raw_scalar_value, - value_byte_0x11, - value_byte_0x12, - value_word_0x14, - value_word_0x16, - ) - .to_string(); - if descriptor_metadata.is_some_and(|metadata| { - matches!( - metadata.parameter_family, - "special_condition_scalar" | "candidate_availability_scalar" - ) && opcode == 3 - && value_byte_0x11 == 0 - && value_byte_0x12 == 0 - && value_word_0x14 == 0 - && value_word_0x16 == 0 - }) { - row_shape = "scalar_assignment".to_string(); - semantic_family = "scalar_assignment".to_string(); - } - if descriptor_metadata - .is_some_and(|metadata| metadata.parameter_family == "world_building_spawn") - { - row_shape = "building_spawn_batch".to_string(); - semantic_family = "building_spawn_batch".to_string(); - } - - let mut notes = Vec::new(); - if locomotive_name.is_some() { - notes.push("grouped effect row carries locomotive-name side string".to_string()); - } - if let Some(metadata) = descriptor_metadata { - if metadata.runtime_status != RealGroupedEffectRuntimeStatus::Executable { - notes.push(format!( - "descriptor is recovered in the checked-in effect table as {} parity", - real_grouped_effect_runtime_status_name(metadata.runtime_status) - )); - } - } else { - notes.push("descriptor id not yet recovered in the checked-in effect table".to_string()); - } - if let Some(loco_id) = recovered_locomotive_availability_loco_id(descriptor_id) { - notes.push(format!( - "locomotive availability descriptor maps to live locomotive id {loco_id}" - )); - } - if let Some(loco_id) = recovered_locomotive_cost_loco_id(descriptor_id) { - notes.push(format!( - "locomotive cost descriptor maps to live locomotive id {loco_id}" - )); - } - if let Some(cargo_slot) = recovered_cargo_production_slot(descriptor_id) { - notes.push(format!( - "cargo-production descriptor maps to world production slot {cargo_slot}" - )); - } - if let Some(cargo_label) = grounded_named_cargo_production_label(descriptor_id) { - notes.push(format!( - "named cargo production descriptor maps to cargo {cargo_label}" - )); - } - if descriptor_metadata.is_some_and(|metadata| metadata.parameter_family == "cargo_price_scalar") - { - if let Some(cargo_label) = grounded_named_cargo_price_label(descriptor_id) { - notes.push(format!( - "named cargo price descriptor maps to cargo {cargo_label}" - )); - } - } - if descriptor_metadata - .is_some_and(|metadata| metadata.parameter_family == "world_building_spawn") - { - let candidate_id = descriptor_id.saturating_sub(503); - notes.push(format!( - "add-building descriptor maps to live candidate id {candidate_id}" - )); - notes.push( - "0x430270 add-building consumer uses placement count byte 0x11, center words 0x12/0x14, and radius word 0x16 after the descriptor_id - 503 candidate bridge; it clamps radius to at least 1 and retries up to 200 randomized placements without branching directly on grouped opcode".to_string(), - ); - if candidate_id > 66 { - notes.push( - "current non-hook candidate-name catalogs only ground concrete add-building names through candidate id 66, so this descriptor remains on the checked-in candidate-slot boundary beyond the live RT3 1.05 table".to_string(), - ); - } - } - - Some(SmpLoadedPackedEventGroupedEffectRowSummary { - group_index, - row_index, - descriptor_id, - descriptor_label: descriptor_metadata.map(|metadata| metadata.label.to_string()), - target_mask_bits: descriptor_metadata.map(|metadata| metadata.target_mask_bits), - parameter_family: descriptor_metadata.map(|metadata| metadata.parameter_family.to_string()), - grouped_target_subject: None, - grouped_target_scope: None, - opcode, - raw_scalar_value, - value_byte_0x09, - value_dword_0x0d, - value_byte_0x11, - value_byte_0x12, - value_word_0x14, - value_word_0x16, - row_shape, - semantic_family: Some(semantic_family.clone()), - semantic_preview: Some(build_real_grouped_effect_semantic_preview( - descriptor_metadata.map(|metadata| metadata.label), - &semantic_family, - raw_scalar_value, - value_byte_0x11, - value_byte_0x12, - value_word_0x14, - value_word_0x16, - )), - recovered_cargo_slot: recovered_cargo_production_slot(descriptor_id), - recovered_cargo_class: recovered_cargo_production_slot(descriptor_id) - .and_then(known_cargo_slot_definition) - .map(|definition| runtime_cargo_class_name(definition.cargo_class).to_string()), - recovered_cargo_label: grounded_named_cargo_production_label(descriptor_id) - .or_else(|| { - descriptor_metadata - .filter(|metadata| metadata.parameter_family == "cargo_price_scalar") - .and_then(|_| grounded_named_cargo_price_label(descriptor_id)) - }) - .map(ToString::to_string), - recovered_locomotive_id: recovered_locomotive_availability_loco_id(descriptor_id) - .or_else(|| recovered_locomotive_cost_loco_id(descriptor_id)), - locomotive_name, - notes, - }) -} - -fn decode_real_condition_rows( - rows: &[SmpLoadedPackedEventConditionRowSummary], - negative_sentinel_scope: Option<&SmpLoadedPackedEventNegativeSentinelScopeSummary>, -) -> Vec { - rows.iter() - .filter(|row| row.raw_condition_id >= 0) - .filter_map(|row| decode_real_condition_row(row, negative_sentinel_scope)) - .collect() -} - -fn decode_real_condition_row( - row: &SmpLoadedPackedEventConditionRowSummary, - negative_sentinel_scope: Option<&SmpLoadedPackedEventNegativeSentinelScopeSummary>, -) -> Option { - let metadata = real_ordinary_condition_metadata(row.raw_condition_id)?; - let comparator = decode_real_condition_comparator(row.subtype)?; - let value = decode_real_condition_threshold(&row.flag_bytes)?; - match metadata.kind { - RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(index)) => { - Some(RuntimeCondition::WorldVariableThreshold { - index, - comparator, - value, - }) - } - RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(metric)) => { - Some(RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - metric, - comparator, - value, - }) - } - RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(index)) => { - Some(RuntimeCondition::CompanyVariableThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - index, - comparator, - value, - }) - } - RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(index)) => { - negative_sentinel_scope.and_then(|scope| { - real_condition_player_target(scope).map(|target| { - RuntimeCondition::PlayerVariableThreshold { - target, - index, - comparator, - value, - } - }) - }) - } - RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(metric)) => { - negative_sentinel_scope.and_then(|scope| { - real_condition_chairman_target(scope).map(|target| { - RuntimeCondition::ChairmanNumericThreshold { - target, - metric, - comparator, - value, - } - }) - }) - } - RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable( - index, - )) => negative_sentinel_scope - .filter(|scope| scope.territory_scope_selector_is_0x63) - .map(|_| RuntimeCondition::TerritoryVariableThreshold { - target: RuntimeTerritoryTarget::AllTerritories, - index, - comparator, - value, - }), - RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => { - negative_sentinel_scope - .filter(|scope| scope.territory_scope_selector_is_0x63) - .map(|_| RuntimeCondition::TerritoryNumericThreshold { - target: RuntimeTerritoryTarget::AllTerritories, - metric, - comparator, - value, - }) - } - RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( - metric, - )) => negative_sentinel_scope - .filter(|scope| scope.territory_scope_selector_is_0x63) - .map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - territory: RuntimeTerritoryTarget::AllTerritories, - metric, - comparator, - value, - }), - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition { - label, - }) => Some(RuntimeCondition::SpecialConditionThreshold { - label: label.to_string(), - comparator, - value, - }), - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => row - .candidate_name - .as_ref() - .map(|name| RuntimeCondition::CandidateAvailabilityThreshold { - name: name.clone(), - comparator, - value, - }), - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { - row.candidate_name.as_ref().and_then(|name| { - recovered_cargo_production_slot_from_condition_name(name).map(|slot| { - let label = known_cargo_slot_definition(slot) - .map(|definition| definition.label.to_string()) - .unwrap_or_else(|| name.clone()); - RuntimeCondition::CargoProductionSlotThreshold { - slot, - label, - comparator, - value, - } - }) - }) - } - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::NamedLocomotiveAvailability, - ) => row.candidate_name.as_ref().map(|name| { - RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name: name.clone(), - comparator, - value, - } - }), - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => row - .candidate_name - .as_ref() - .map(|name| RuntimeCondition::NamedLocomotiveCostThreshold { - name: name.clone(), - comparator, - value, - }), - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => { - Some(RuntimeCondition::CargoProductionTotalThreshold { comparator, value }) - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal) => { - Some(RuntimeCondition::FactoryProductionTotalThreshold { comparator, value }) - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FarmMineProductionTotal) => { - Some(RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value }) - } - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::OtherCargoProductionTotal, - ) => Some(RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value }), - RealOrdinaryConditionKind::WorldState( - RealWorldConditionKind::LimitedTrackBuildingAmount, - ) => Some(RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value }), - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => { - Some(RuntimeCondition::TerritoryAccessCostThreshold { comparator, value }) - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => { - Some(RuntimeCondition::EconomicStatusCodeThreshold { comparator, value }) - } - RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { key }) => { - decode_world_flag_condition(comparator, value, key) - } - } -} - -fn decode_world_flag_condition( - comparator: RuntimeConditionComparator, - value: i64, - key: &'static str, -) -> Option { - let bool_value = match (comparator, value) { - (RuntimeConditionComparator::Eq, 0) | (RuntimeConditionComparator::Ne, 1) => false, - (RuntimeConditionComparator::Eq, 1) | (RuntimeConditionComparator::Ne, 0) => true, - _ => return None, - }; - Some(RuntimeCondition::WorldFlagEquals { - key: key.to_string(), - value: bool_value, - }) -} - -fn real_condition_chairman_target( - scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, -) -> Option { - match scope.player_test_scope { - RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimeChairmanTarget::AllActive), - RuntimePlayerConditionTestScope::SelectedPlayerOnly => { - Some(RuntimeChairmanTarget::SelectedChairman) - } - RuntimePlayerConditionTestScope::AiPlayersOnly => Some(RuntimeChairmanTarget::AiChairmen), - RuntimePlayerConditionTestScope::HumanPlayersOnly => { - Some(RuntimeChairmanTarget::HumanChairmen) - } - RuntimePlayerConditionTestScope::Disabled => None, - } -} - -fn real_condition_player_target( - scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, -) -> Option { - match scope.player_test_scope { - RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimePlayerTarget::AllActive), - RuntimePlayerConditionTestScope::SelectedPlayerOnly => { - Some(RuntimePlayerTarget::SelectedPlayer) - } - RuntimePlayerConditionTestScope::AiPlayersOnly => Some(RuntimePlayerTarget::AiPlayers), - RuntimePlayerConditionTestScope::HumanPlayersOnly => { - Some(RuntimePlayerTarget::HumanPlayers) - } - RuntimePlayerConditionTestScope::Disabled => None, - } -} - -fn real_grouped_effect_descriptor_metadata( - descriptor_id: u32, -) -> Option { - recovered_cargo_price_descriptor_metadata(descriptor_id) - .or_else(|| recovered_cargo_economics_descriptor_metadata(descriptor_id)) - .or_else(|| recovered_cargo_production_descriptor_metadata(descriptor_id)) - .or_else(|| recovered_locomotive_availability_descriptor_metadata(descriptor_id)) - .or_else(|| recovered_locomotive_cost_descriptor_metadata(descriptor_id)) - .or_else(|| recovered_territory_access_cost_descriptor_metadata(descriptor_id)) - .or_else(|| recovered_locomotive_policy_descriptor_metadata(descriptor_id)) - .or_else(|| special_condition_world_scalar_descriptor_metadata(descriptor_id)) - .or_else(|| special_condition_world_toggle_descriptor_metadata(descriptor_id)) - .or_else(|| { - REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA - .iter() - .copied() - .find(|metadata| metadata.descriptor_id == descriptor_id) - }) - .or_else(|| { - checked_in_event_effect_descriptor_rows() - .get(&descriptor_id) - .copied() - }) -} - -fn recovered_cargo_price_descriptor_metadata( - descriptor_id: u32, -) -> Option { - (descriptor_id == 105).then_some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: "All Cargo Prices", - target_mask_bits: 0x08, - parameter_family: "cargo_price_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }) -} - -fn recovered_cargo_economics_descriptor_metadata( - descriptor_id: u32, -) -> Option { - match descriptor_id { - 177 => Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: "All Cargo Production", - target_mask_bits: 0x08, - parameter_family: "cargo_production_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }), - 178 => Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: "All Factory Production", - target_mask_bits: 0x08, - parameter_family: "cargo_production_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }), - 179 => Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: "All Farm/Mine Production", - target_mask_bits: 0x08, - parameter_family: "cargo_production_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }), - _ => None, - } -} - -const GROUNDED_NAMED_CARGO_PRODUCTION_LABELS: [(&str, &str); 50] = [ - ("Alcohol", "Alcohol Production"), - ("Aluminum", "Aluminum Production"), - ("Ammunition", "Ammunition Production"), - ("Automobiles", "Automobiles Production"), - ("Bauxite", "Bauxite Production"), - ("Ceramics", "Ceramics Production"), - ("Cheese", "Cheese Production"), - ("Chemicals", "Chemicals Production"), - ("Clothing", "Clothing Production"), - ("Coal", "Coal Production"), - ("Coffee", "Coffee Production"), - ("Concrete", "Concrete Production"), - ("Corn", "Corn Production"), - ("Cotton", "Cotton Production"), - ("Crystals", "Crystals Production"), - ("Diesel", "Diesel Production"), - ("Dye", "Dye Production"), - ("Electronics", "Electronics Production"), - ("Fertilizer", "Fertilizer Production"), - ("Furniture", "Furniture Production"), - ("Goods", "Goods Production"), - ("Grain", "Grain Production"), - ("Ingots", "Ingots Production"), - ("Iron", "Iron Production"), - ("Livestock", "Livestock Production"), - ("Logs", "Logs Production"), - ("Lumber", "Lumber Production"), - ("Machinery", "Machinery Production"), - ("Mail", "Mail Production"), - ("Meat", "Meat Production"), - ("Medicine", "Medicine Production"), - ("Milk", "Milk Production"), - ("Oil", "Oil Production"), - ("Ore", "Ore Production"), - ("Paper", "Paper Production"), - ("Passengers", "Passengers Production"), - ("Plastic", "Plastic Production"), - ("Produce", "Produce Production"), - ("Pulpwood", "Pulpwood Production"), - ("Rice", "Rice Production"), - ("Rubber", "Rubber Production"), - ("Steel", "Steel Production"), - ("Sugar", "Sugar Production"), - ("Tires", "Tires Production"), - ("Toys", "Toys Production"), - ("Troops", "Troops Production"), - ("Uranium", "Uranium Production"), - ("Waste", "Waste Production"), - ("Weapons", "Weapons Production"), - ("Wool", "Wool Production"), -]; - -#[derive(Debug, Deserialize)] -struct CheckedInCargoBindingsArtifact { - bindings: Vec, -} - -#[derive(Debug, Deserialize)] -struct CheckedInCargoBindingRow { - descriptor_id: u32, - band: String, - cargo_name: String, -} - -fn grounded_named_cargo_price_bindings() -> &'static BTreeMap { - static BINDINGS: OnceLock> = OnceLock::new(); - BINDINGS.get_or_init(|| { - let artifact: CheckedInCargoBindingsArtifact = serde_json::from_str(include_str!( - "../../../artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json" - )) - .expect("checked-in cargo bindings artifact should parse"); - artifact - .bindings - .into_iter() - .filter(|binding| binding.band == "cargo_price_named") - .map(|binding| { - let cargo_name = Box::leak(binding.cargo_name.into_boxed_str()) as &'static str; - let descriptor_label = - Box::leak(format!("{cargo_name} Price").into_boxed_str()) as &'static str; - (binding.descriptor_id, (cargo_name, descriptor_label)) - }) - .collect() - }) -} - -pub(crate) fn grounded_named_cargo_price_label(descriptor_id: u32) -> Option<&'static str> { - grounded_named_cargo_price_bindings() - .get(&descriptor_id) - .map(|(cargo_label, _)| *cargo_label) -} - -fn grounded_named_cargo_production_label(descriptor_id: u32) -> Option<&'static str> { - let index = descriptor_id.checked_sub(180)? as usize; - GROUNDED_NAMED_CARGO_PRODUCTION_LABELS - .get(index) - .map(|(cargo_label, _)| *cargo_label) -} - -fn grounded_named_cargo_production_descriptor_label(descriptor_id: u32) -> Option<&'static str> { - let index = descriptor_id.checked_sub(180)? as usize; - GROUNDED_NAMED_CARGO_PRODUCTION_LABELS - .get(index) - .map(|(_, descriptor_label)| *descriptor_label) -} - -fn recovered_cargo_production_descriptor_metadata( - descriptor_id: u32, -) -> Option { - if let Some(label) = grounded_named_cargo_production_descriptor_label(descriptor_id) { - return Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label, - target_mask_bits: 0x08, - parameter_family: "cargo_production_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }); - } - recovered_cargo_production_label(descriptor_id).map(|label| { - RealGroupedEffectDescriptorMetadata { - descriptor_id, - label, - target_mask_bits: 0x08, - parameter_family: "cargo_production_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - } - }) -} - -fn recovered_cargo_production_slot(descriptor_id: u32) -> Option { - let slot = descriptor_id.checked_sub(229)?; - (1..=11).contains(&slot).then_some(slot) -} - -fn recovered_locomotive_availability_descriptor_metadata( - descriptor_id: u32, -) -> Option { - if let Some(loco_id) = recovered_locomotive_availability_loco_id(descriptor_id) { - let label = recovered_locomotive_availability_label(loco_id); - let executable_in_runtime = (loco_id as usize) <= GROUNDED_LOCOMOTIVE_PREFIX.len(); - return Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label, - target_mask_bits: 0x08, - parameter_family: "locomotive_availability_scalar", - runtime_key: None, - runtime_status: if executable_in_runtime { - RealGroupedEffectRuntimeStatus::Executable - } else { - RealGroupedEffectRuntimeStatus::EvidenceBlocked - }, - executable_in_runtime, - }); - } - (457..=474) - .contains(&descriptor_id) - .then(|| RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: upper_band_locomotive_availability_label(descriptor_id), - target_mask_bits: 0x08, - parameter_family: "locomotive_availability_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::EvidenceBlocked, - executable_in_runtime: false, - }) -} - -fn recovered_locomotive_availability_loco_id(descriptor_id: u32) -> Option { - if (241..=351).contains(&descriptor_id) { - return Some(descriptor_id - 240); - } - None -} - -fn grounded_locomotive_name(loco_id: u32) -> Option<&'static str> { - let index = loco_id.checked_sub(1)? as usize; - GROUNDED_LOCOMOTIVE_PREFIX.get(index).copied() -} - -fn recovered_locomotive_availability_label(loco_id: u32) -> &'static str { - static LABELS: OnceLock> = OnceLock::new(); - LABELS - .get_or_init(|| { - (1..=111) - .map(|loco_id| { - let label = grounded_locomotive_name(loco_id) - .map(|name| format!("{name} Availability")) - .unwrap_or_else(|| { - format!("Lower-Band Locomotive Availability Slot {loco_id}") - }); - (loco_id, Box::leak(label.into_boxed_str()) as &'static str) - }) - .collect() - }) - .get(&loco_id) - .copied() - .expect("lower-band locomotive availability label should exist") -} - -fn upper_band_locomotive_availability_label(descriptor_id: u32) -> &'static str { - static LABELS: OnceLock> = OnceLock::new(); - LABELS - .get_or_init(|| { - (457..=474) - .map(|descriptor_id| { - let label = format!( - "Upper-Band Locomotive Availability Slot {}", - descriptor_id - 456 - ); - ( - descriptor_id, - Box::leak(label.into_boxed_str()) as &'static str, - ) - }) - .collect() - }) - .get(&descriptor_id) - .copied() - .expect("upper-band locomotive availability label should exist") -} - -fn recovered_cargo_production_label(descriptor_id: u32) -> Option<&'static str> { - known_cargo_slot_definition_for_descriptor_id(descriptor_id).map(|definition| definition.label) -} - -fn recovered_cargo_production_slot_from_condition_name(name: &str) -> Option { - KNOWN_CARGO_SLOT_DEFINITIONS - .iter() - .find(|definition| definition.label == name) - .map(|definition| definition.slot_id) -} - -fn recovered_locomotive_cost_loco_id(descriptor_id: u32) -> Option { - if (352..=451).contains(&descriptor_id) { - return Some(descriptor_id - 351); - } - None -} - -fn recovered_locomotive_cost_label(descriptor_id: u32) -> Option<&'static str> { - static LABELS: OnceLock> = OnceLock::new(); - LABELS - .get_or_init(|| { - (352..=451) - .filter_map(|descriptor_id| { - recovered_locomotive_cost_loco_id(descriptor_id).map(|loco_id| { - let label = grounded_locomotive_name(loco_id) - .map(|name| format!("{name} Cost")) - .unwrap_or_else(|| { - format!("Lower-Band Locomotive Cost Slot {loco_id}") - }); - let label = Box::leak(label.into_boxed_str()) as &'static str; - (descriptor_id, label) - }) - }) - .collect() - }) - .get(&descriptor_id) - .copied() - .or_else(|| upper_band_locomotive_cost_label(descriptor_id)) -} - -fn upper_band_locomotive_cost_label(descriptor_id: u32) -> Option<&'static str> { - static LABELS: OnceLock> = OnceLock::new(); - LABELS - .get_or_init(|| { - (475..=502) - .map(|descriptor_id| { - let label = format!("Upper-Band Locomotive Cost Slot {}", descriptor_id - 474); - ( - descriptor_id, - Box::leak(label.into_boxed_str()) as &'static str, - ) - }) - .collect() - }) - .get(&descriptor_id) - .copied() -} - -fn recovered_locomotive_cost_descriptor_metadata( - descriptor_id: u32, -) -> Option { - recovered_locomotive_cost_label(descriptor_id).map(|label| { - let executable_in_runtime = recovered_locomotive_cost_loco_id(descriptor_id) - .is_some_and(|loco_id| (loco_id as usize) <= GROUNDED_LOCOMOTIVE_PREFIX.len()); - RealGroupedEffectDescriptorMetadata { - descriptor_id, - label, - target_mask_bits: 0x08, - parameter_family: "locomotive_cost_scalar", - runtime_key: None, - runtime_status: if executable_in_runtime { - RealGroupedEffectRuntimeStatus::Executable - } else { - RealGroupedEffectRuntimeStatus::EvidenceBlocked - }, - executable_in_runtime, - } - }) -} - -fn recovered_territory_access_cost_descriptor_metadata( - descriptor_id: u32, -) -> Option { - (descriptor_id == 453).then_some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: "Territory Access Cost", - target_mask_bits: 0x08, - parameter_family: "territory_access_cost_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }) -} - -fn recovered_locomotive_policy_descriptor_metadata( - descriptor_id: u32, -) -> Option { - match descriptor_id { - 454 => Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: "All Steam Locos Avail.", - target_mask_bits: 0x08, - parameter_family: "world_flag_toggle", - runtime_key: Some("world.all_steam_locos_available"), - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }), - 455 => Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: "All Diesel Locos Avail.", - target_mask_bits: 0x08, - parameter_family: "world_flag_toggle", - runtime_key: Some("world.all_diesel_locos_available"), - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }), - 456 => Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: "All Electric Locos Avail.", - target_mask_bits: 0x08, - parameter_family: "world_flag_toggle", - runtime_key: Some("world.all_electric_locos_available"), - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }), - _ => None, - } -} - -fn special_condition_world_scalar_descriptor_metadata( - descriptor_id: u32, -) -> Option { - let slot_index = descriptor_id.checked_sub(110)? as usize; - if slot_index != 12 { - return None; - } - let definition = KNOWN_SPECIAL_CONDITION_DEFINITIONS.get(slot_index)?; - if definition.hidden { - return None; - } - Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: definition.label, - target_mask_bits: 0x08, - parameter_family: "world_track_build_limit_scalar", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }) -} - -fn special_condition_world_toggle_descriptor_metadata( - descriptor_id: u32, -) -> Option { - let slot_index = descriptor_id.checked_sub(110)? as usize; - if !(1..=34).contains(&slot_index) || matches!(slot_index, 12 | 31) { - return None; - } - let definition = KNOWN_SPECIAL_CONDITION_DEFINITIONS.get(slot_index)?; - if definition.hidden { - return None; - } - Some(RealGroupedEffectDescriptorMetadata { - descriptor_id, - label: definition.label, - target_mask_bits: 0x08, - parameter_family: "world_flag_toggle", - runtime_key: None, - runtime_status: RealGroupedEffectRuntimeStatus::Executable, - executable_in_runtime: true, - }) -} - -fn classify_real_grouped_effect_semantic_family( - opcode: u8, - raw_scalar_value: i32, - value_byte_0x11: u8, - value_byte_0x12: u8, - value_word_0x14: u16, - value_word_0x16: u16, -) -> &'static str { - if opcode == 8 { - return "multivalue_scalar"; - } - if value_byte_0x11 != 0 || value_byte_0x12 != 0 || value_word_0x14 != 0 || value_word_0x16 != 0 - { - return "timed_duration"; - } - if raw_scalar_value == 0 || raw_scalar_value == 1 { - return "bool_toggle"; - } - "scalar_assignment" -} - -fn classify_real_grouped_effect_row_shape( - opcode: u8, - raw_scalar_value: i32, - value_byte_0x11: u8, - value_byte_0x12: u8, - value_word_0x14: u16, - value_word_0x16: u16, -) -> &'static str { - if opcode == 8 { - return "multivalue_scalar"; - } - if value_byte_0x11 != 0 || value_byte_0x12 != 0 || value_word_0x14 != 0 || value_word_0x16 != 0 - { - return "timed_duration"; - } - if raw_scalar_value == 0 || raw_scalar_value == 1 { - return "bool_toggle"; - } - "scalar_assignment" -} - -fn build_real_grouped_effect_semantic_preview( - descriptor_label: Option<&str>, - semantic_family: &str, - raw_scalar_value: i32, - value_byte_0x11: u8, - value_byte_0x12: u8, - value_word_0x14: u16, - value_word_0x16: u16, -) -> String { - let label = descriptor_label.unwrap_or("descriptor"); - match semantic_family { - "bool_toggle" => { - let state = if raw_scalar_value == 0 { - "FALSE" - } else { - "TRUE" - }; - format!("Set {label} to {state}") - } - "building_spawn_batch" => format!( - "Batch place {label} with scalar {raw_scalar_value}, count {value_byte_0x11}, and span words [{value_word_0x14}, {value_word_0x16}]" - ), - "timed_duration" => format!( - "Set {label} to {raw_scalar_value} for {value_word_0x14} years {value_word_0x16} months" - ), - "multivalue_scalar" => format!( - "Set {label} to {raw_scalar_value} with aux [{value_byte_0x11}, {value_byte_0x12}, {value_word_0x14}, {value_word_0x16}]" - ), - _ => format!("Set {label} to {raw_scalar_value}"), - } -} - -fn runtime_candidate_availability_name(label: &str) -> String { - label - .strip_suffix(" Availability") - .unwrap_or(label) - .to_string() -} - -fn runtime_world_flag_key( - descriptor_metadata: RealGroupedEffectDescriptorMetadata, -) -> Option { - descriptor_metadata - .runtime_key - .map(str::to_string) - .or_else(|| { - (descriptor_metadata.parameter_family == "world_flag_toggle") - .then(|| runtime_world_flag_key_from_label(descriptor_metadata.label)) - }) -} - -pub(crate) fn runtime_world_flag_key_from_label(label: &str) -> String { - normalize_runtime_world_key(label) -} - -fn runtime_world_scalar_key( - descriptor_metadata: RealGroupedEffectDescriptorMetadata, -) -> Option { - descriptor_metadata - .runtime_key - .map(str::to_string) - .or_else(|| { - (descriptor_metadata.parameter_family == "world_scalar_override") - .then(|| normalize_runtime_world_key(descriptor_metadata.label)) - }) -} - -pub(crate) fn runtime_world_scalar_key_from_label(label: &str) -> String { - normalize_runtime_world_key(label) -} - -fn normalize_runtime_world_key(label: &str) -> String { - let mut key = String::with_capacity(label.len() + 6); - key.push_str("world."); - let mut last_was_underscore = false; - for ch in label.chars() { - if ch.is_ascii_alphanumeric() { - key.push(ch.to_ascii_lowercase()); - last_was_underscore = false; - } else if !last_was_underscore { - key.push('_'); - last_was_underscore = true; - } - } - while key.ends_with('_') { - key.pop(); - } - key -} - -fn real_grouped_company_governance_metric( - descriptor_metadata: RealGroupedEffectDescriptorMetadata, -) -> Option { - match descriptor_metadata.label { - "Credit Rating" => Some(RuntimeCompanyMetric::CreditRating), - "Prime Rate" => Some(RuntimeCompanyMetric::PrimeRate), - "Book Value Per Share" => Some(RuntimeCompanyMetric::BookValuePerShare), - "Investor Confidence" => Some(RuntimeCompanyMetric::InvestorConfidence), - "Management Attitude" => Some(RuntimeCompanyMetric::ManagementAttitude), - _ => None, - } -} - -fn derive_real_grouped_target_subject( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, - compact_control: &SmpLoadedPackedEventCompactControlSummary, -) -> Option { - if row.parameter_family.as_deref() == Some("company_governance_scalar") { - return Some(RealGroupedTargetSubject::Company); - } - if row.parameter_family.as_deref() == Some("world_scalar_override") { - return Some(RealGroupedTargetSubject::WholeGame); - } - match row.target_mask_bits { - Some(0x08) => Some(RealGroupedTargetSubject::WholeGame), - Some(0x01) => Some(RealGroupedTargetSubject::Company), - Some(0x04) => Some(RealGroupedTargetSubject::Territory), - Some(0x02) => match compact_control - .grouped_scope_checkboxes_0x7ff - .get(row.group_index) - .copied() - { - Some(2) => Some(RealGroupedTargetSubject::Chairman), - _ => Some(RealGroupedTargetSubject::Player), - }, - _ if row.descriptor_id == 3 => Some(RealGroupedTargetSubject::Territory), - _ if row.descriptor_id == 15 - && compact_control - .grouped_territory_selectors_0x80f - .get(row.group_index) - .is_some_and(|selector| *selector >= 0) => - { - Some(RealGroupedTargetSubject::Territory) - } - _ => None, - } -} - -fn real_grouped_target_subject_name(subject: RealGroupedTargetSubject) -> &'static str { - match subject { - RealGroupedTargetSubject::Company => "company", - RealGroupedTargetSubject::Player => "player", - RealGroupedTargetSubject::Chairman => "chairman", - RealGroupedTargetSubject::Territory => "territory", - RealGroupedTargetSubject::WholeGame => "whole_game", - } -} - -fn derive_real_grouped_target_scope_name( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, - compact_control: &SmpLoadedPackedEventCompactControlSummary, - target_subject: Option, - target_scope_ordinal: Option, -) -> Option { - match target_subject { - Some(RealGroupedTargetSubject::Company) => target_scope_ordinal - .map(real_grouped_company_scope_name) - .map(str::to_string), - Some(RealGroupedTargetSubject::Player) => target_scope_ordinal - .map(real_grouped_player_scope_name) - .map(str::to_string), - Some(RealGroupedTargetSubject::Chairman) => target_scope_ordinal - .map(real_grouped_chairman_scope_name) - .map(str::to_string), - Some(RealGroupedTargetSubject::Territory) => compact_control - .grouped_territory_selectors_0x80f - .get(row.group_index) - .copied() - .filter(|selector| *selector >= 0) - .map(|_| "specified_territories".to_string()), - Some(RealGroupedTargetSubject::WholeGame) => Some("whole_game".to_string()), - None => None, - } -} - -fn real_grouped_company_scope_name(ordinal: u8) -> &'static str { - match ordinal { - 0 => "condition_true_company", - 1 => "selected_company", - 2 => "human_companies", - 3 => "ai_companies", - _ => "unsupported_company_scope", - } -} - -fn real_grouped_player_scope_name(ordinal: u8) -> &'static str { - match ordinal { - 0 => "condition_true_player", - 1 => "selected_player", - 2 => "human_players", - 3 => "ai_players", - _ => "unsupported_player_scope", - } -} - -fn real_grouped_chairman_scope_name(ordinal: u8) -> &'static str { - match ordinal { - 0 => "condition_true_chairman", - 1 => "selected_chairman", - 2 => "human_chairmen", - 3 => "ai_chairmen", - _ => "unsupported_chairman_scope", - } -} - -fn runtime_variable_index(descriptor_id: u32) -> Option { - match descriptor_id { - 39..=42 => Some(descriptor_id - 38), - 43..=46 => Some(descriptor_id - 42), - 47..=50 => Some(descriptor_id - 46), - 51..=54 => Some(descriptor_id - 50), - _ => None, - } -} - -fn decode_real_grouped_effect_actions( - grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary], - compact_control: &SmpLoadedPackedEventCompactControlSummary, -) -> Vec { - grouped_effect_rows - .iter() - .filter_map(|row| decode_real_grouped_effect_action(row, compact_control)) - .collect() -} - -fn decode_real_grouped_effect_action( - row: &SmpLoadedPackedEventGroupedEffectRowSummary, - compact_control: &SmpLoadedPackedEventCompactControlSummary, -) -> Option { - let descriptor_metadata = real_grouped_effect_descriptor_metadata(row.descriptor_id)?; - let target_scope_ordinal = compact_control - .grouped_target_scope_ordinals_0x7fb - .get(row.group_index) - .copied()?; - let target_subject = derive_real_grouped_target_subject(row, compact_control); - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.parameter_family == "runtime_variable_scalar" - && row.row_shape == "scalar_assignment" - { - let index = runtime_variable_index(descriptor_metadata.descriptor_id)?; - return match target_subject { - Some(RealGroupedTargetSubject::WholeGame) => Some(RuntimeEffect::SetWorldVariable { - index, - value: i64::from(row.raw_scalar_value), - }), - Some(RealGroupedTargetSubject::Company) => Some(RuntimeEffect::SetCompanyVariable { - target: real_grouped_company_target(target_scope_ordinal)?, - index, - value: i64::from(row.raw_scalar_value), - }), - Some(RealGroupedTargetSubject::Player) => Some(RuntimeEffect::SetPlayerVariable { - target: real_grouped_player_target(target_scope_ordinal)?, - index, - value: i64::from(row.raw_scalar_value), - }), - Some(RealGroupedTargetSubject::Territory) => compact_control - .grouped_territory_selectors_0x80f - .get(row.group_index) - .copied() - .filter(|selector| *selector >= 0) - .map(|selector| RuntimeEffect::SetTerritoryVariable { - target: RuntimeTerritoryTarget::Ids { - ids: vec![selector as u32], - }, - index, - value: i64::from(row.raw_scalar_value), - }), - _ => None, - }; - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.parameter_family == "company_governance_scalar" - && row.row_shape == "scalar_assignment" - { - let target = real_grouped_company_target(target_scope_ordinal)?; - let metric = real_grouped_company_governance_metric(descriptor_metadata)?; - return Some(RuntimeEffect::SetCompanyGovernanceScalar { - target, - metric, - value: i64::from(row.raw_scalar_value), - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 1 - && row.opcode == 8 - && row.row_shape == "multivalue_scalar" - { - return match target_subject { - Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::SetChairmanCash { - target: real_grouped_chairman_target(target_scope_ordinal)?, - value: i64::from(row.raw_scalar_value), - }), - _ => Some(RuntimeEffect::SetPlayerCash { - target: real_grouped_player_target(target_scope_ordinal)?, - value: i64::from(row.raw_scalar_value), - }), - }; - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 2 - && row.opcode == 8 - && row.row_shape == "multivalue_scalar" - { - let target = real_grouped_company_target(target_scope_ordinal)?; - return Some(RuntimeEffect::SetCompanyCash { - target, - value: i64::from(row.raw_scalar_value), - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 3 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - { - let target = real_grouped_company_target(target_scope_ordinal)?; - let territory = compact_control - .grouped_territory_selectors_0x80f - .get(row.group_index) - .copied() - .filter(|selector| *selector >= 0) - .map(|selector| RuntimeTerritoryTarget::Ids { - ids: vec![selector as u32], - })?; - return Some(RuntimeEffect::SetCompanyTerritoryAccess { - target, - territory, - value: true, - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 8 - && row.row_shape == "scalar_assignment" - { - return Some(RuntimeEffect::SetEconomicStatusCode { - value: row.raw_scalar_value, - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 108 - && row.row_shape == "scalar_assignment" - { - return Some(RuntimeEffect::SetSpecialCondition { - label: descriptor_metadata.label.to_string(), - value: row.raw_scalar_value as u32, - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 109 - && row.row_shape == "scalar_assignment" - { - return Some(RuntimeEffect::SetCandidateAvailability { - name: runtime_candidate_availability_name(descriptor_metadata.label), - value: row.raw_scalar_value as u32, - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 122 - && row.row_shape == "scalar_assignment" - { - return Some(RuntimeEffect::SetLimitedTrackBuildingAmount { - value: row.raw_scalar_value, - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.parameter_family == "world_scalar_override" - && row.row_shape == "scalar_assignment" - { - return Some(RuntimeEffect::SetWorldScalarOverride { - key: runtime_world_scalar_key(descriptor_metadata)?, - value: i64::from(row.raw_scalar_value), - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.parameter_family == "cargo_price_scalar" - && row.row_shape == "scalar_assignment" - && row.raw_scalar_value >= 0 - { - return match row.descriptor_id { - 105 => Some(RuntimeEffect::SetCargoPriceOverride { - target: RuntimeCargoPriceTarget::All, - value: row.raw_scalar_value as u32, - }), - descriptor_id => grounded_named_cargo_price_label(descriptor_id).map(|name| { - RuntimeEffect::SetCargoPriceOverride { - target: RuntimeCargoPriceTarget::Named { - name: name.to_string(), - }, - value: row.raw_scalar_value as u32, - } - }), - }; - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.parameter_family == "cargo_production_scalar" - && row.row_shape == "scalar_assignment" - && row.raw_scalar_value >= 0 - { - return match descriptor_metadata.descriptor_id { - 177 => Some(RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::All, - value: row.raw_scalar_value as u32, - }), - 178 => Some(RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::Factory, - value: row.raw_scalar_value as u32, - }), - 179 => Some(RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::FarmMine, - value: row.raw_scalar_value as u32, - }), - 230..=240 => { - let slot = descriptor_metadata.descriptor_id.checked_sub(229)?; - Some(RuntimeEffect::SetCargoProductionSlot { - slot, - value: row.raw_scalar_value as u32, - }) - } - _ => None, - }; - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.parameter_family == "territory_access_cost_scalar" - && row.row_shape == "scalar_assignment" - && row.raw_scalar_value >= 0 - { - return Some(RuntimeEffect::SetTerritoryAccessCost { - value: row.raw_scalar_value as u32, - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.parameter_family == "world_flag_toggle" - && row.row_shape == "bool_toggle" - { - return Some(RuntimeEffect::SetWorldFlag { - key: runtime_world_flag_key(descriptor_metadata)?, - value: row.raw_scalar_value != 0, - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 9 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - { - let target = real_grouped_company_target(target_scope_ordinal)?; - return Some(RuntimeEffect::ConfiscateCompanyAssets { target }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 13 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - { - let target = real_grouped_company_target(target_scope_ordinal)?; - return Some(RuntimeEffect::DeactivateCompany { target }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 14 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - { - return match target_subject { - Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::DeactivateChairman { - target: real_grouped_chairman_target(target_scope_ordinal)?, - }), - _ => Some(RuntimeEffect::DeactivatePlayer { - target: real_grouped_player_target(target_scope_ordinal)?, - }), - }; - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 16 - && row.row_shape == "scalar_assignment" - && row.raw_scalar_value >= 0 - { - let target = real_grouped_company_target(target_scope_ordinal)?; - return Some(RuntimeEffect::SetCompanyTrackLayingCapacity { - target, - value: Some(row.raw_scalar_value as u32), - }); - } - - if descriptor_metadata.executable_in_runtime - && descriptor_metadata.descriptor_id == 15 - && row.row_shape == "bool_toggle" - && row.raw_scalar_value != 0 - { - let company_target = real_grouped_company_target(target_scope_ordinal); - let territory_target = compact_control - .grouped_territory_selectors_0x80f - .get(row.group_index) - .copied() - .filter(|selector| *selector >= 0) - .map(|selector| RuntimeTerritoryTarget::Ids { - ids: vec![selector as u32], - }); - if company_target.is_none() && territory_target.is_none() { - return None; - } - return Some(RuntimeEffect::RetireTrains { - company_target, - territory_target, - locomotive_name: row.locomotive_name.clone(), - }); - } - - None -} - -fn real_grouped_company_target(ordinal: u8) -> Option { - match ordinal { - 0 => Some(RuntimeCompanyTarget::ConditionTrueCompany), - 1 => Some(RuntimeCompanyTarget::SelectedCompany), - 2 => Some(RuntimeCompanyTarget::HumanCompanies), - 3 => Some(RuntimeCompanyTarget::AiCompanies), - _ => None, - } -} - -fn real_grouped_player_target(ordinal: u8) -> Option { - match ordinal { - 0 => Some(RuntimePlayerTarget::ConditionTruePlayer), - 1 => Some(RuntimePlayerTarget::SelectedPlayer), - 2 => Some(RuntimePlayerTarget::HumanPlayers), - 3 => Some(RuntimePlayerTarget::AiPlayers), - _ => None, - } -} - -fn real_grouped_chairman_target(ordinal: u8) -> Option { - match ordinal { - 0 => Some(RuntimeChairmanTarget::ConditionTrueChairman), - 1 => Some(RuntimeChairmanTarget::SelectedChairman), - 2 => Some(RuntimeChairmanTarget::HumanChairmen), - 3 => Some(RuntimeChairmanTarget::AiChairmen), - _ => None, - } -} - -fn real_grouped_chairman_target_supported_in_runtime(ordinal: u8) -> bool { - real_grouped_chairman_target(ordinal).is_some() -} - -fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option { - let opcode = read_u8_at(bytes, *cursor)?; - *cursor += 1; - match opcode { - 0x01 => { - let key = parse_len_prefixed_string(bytes, cursor)?; - let value = read_u8_at(bytes, *cursor)? != 0; - *cursor += 1; - Some(RuntimeEffect::SetWorldFlag { key, value }) - } - 0x02 => { - let target = parse_synthetic_company_target(bytes, cursor)?; - let delta = read_i64_at(bytes, *cursor)?; - *cursor += 8; - Some(RuntimeEffect::AdjustCompanyCash { target, delta }) - } - 0x03 => { - let target = parse_synthetic_company_target(bytes, cursor)?; - let delta = read_i64_at(bytes, *cursor)?; - *cursor += 8; - Some(RuntimeEffect::AdjustCompanyDebt { target, delta }) - } - 0x04 => { - let name = parse_len_prefixed_string(bytes, cursor)?; - let value = read_u32_at(bytes, *cursor)?; - *cursor += 4; - Some(RuntimeEffect::SetCandidateAvailability { name, value }) - } - 0x05 => { - let label = parse_len_prefixed_string(bytes, cursor)?; - let value = read_u32_at(bytes, *cursor)?; - *cursor += 4; - Some(RuntimeEffect::SetSpecialCondition { label, value }) - } - 0x06 => { - let template_len = usize::try_from(read_u32_at(bytes, *cursor)?).ok()?; - *cursor += 4; - let template_bytes = bytes.get(*cursor..*cursor + template_len)?; - let record = parse_synthetic_event_runtime_record_template(template_bytes)?; - *cursor += template_len; - Some(RuntimeEffect::AppendEventRecord { - record: Box::new(record), - }) - } - 0x07 => { - let record_id = read_u32_at(bytes, *cursor)?; - *cursor += 4; - Some(RuntimeEffect::ActivateEventRecord { record_id }) - } - 0x08 => { - let record_id = read_u32_at(bytes, *cursor)?; - *cursor += 4; - Some(RuntimeEffect::DeactivateEventRecord { record_id }) - } - 0x09 => { - let record_id = read_u32_at(bytes, *cursor)?; - *cursor += 4; - Some(RuntimeEffect::RemoveEventRecord { record_id }) - } - _ => None, - } -} - -fn parse_synthetic_event_runtime_record_template( - bytes: &[u8], -) -> Option { - if !bytes.starts_with(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC) { - return None; - } - - let mut cursor = PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC.len(); - let record_id = read_u32_at(bytes, cursor)?; - cursor += 4; - let trigger_kind = read_u8_at(bytes, cursor)?; - cursor += 1; - let flags = read_u8_at(bytes, cursor)?; - cursor += 1; - let action_count = usize::from(read_u8_at(bytes, cursor)?); - cursor += 1; - cursor += 1; - - let mut effects = Vec::with_capacity(action_count); - for _ in 0..action_count { - effects.push(parse_synthetic_packed_event_action(bytes, &mut cursor)?); - } - - if cursor != bytes.len() { - return None; - } - - Some(RuntimeEventRecordTemplate { - record_id, - trigger_kind, - active: flags & 0x01 != 0, - marks_collection_dirty: flags & 0x02 != 0, - one_shot: flags & 0x04 != 0, - conditions: Vec::new(), - effects, - }) -} - -fn parse_synthetic_company_target( - bytes: &[u8], - cursor: &mut usize, -) -> Option { - let target_kind = read_u8_at(bytes, *cursor)?; - *cursor += 1; - match target_kind { - 0x00 => Some(RuntimeCompanyTarget::AllActive), - 0x01 => { - let count = usize::from(read_u8_at(bytes, *cursor)?); - *cursor += 1; - let mut ids = Vec::with_capacity(count); - for _ in 0..count { - ids.push(read_u32_at(bytes, *cursor)?); - *cursor += 4; - } - Some(RuntimeCompanyTarget::Ids { ids }) - } - _ => None, - } -} - -fn parse_len_prefixed_string(bytes: &[u8], cursor: &mut usize) -> Option { - let len = usize::from(read_u8_at(bytes, *cursor)?); - *cursor += 1; - let text_bytes = bytes.get(*cursor..*cursor + len)?; - *cursor += len; - Some(String::from_utf8_lossy(text_bytes).into_owned()) -} - -fn parse_optional_u16_len_prefixed_string( - bytes: &[u8], - cursor: &mut usize, -) -> Option> { - let len = usize::from(read_u16_at(bytes, *cursor)?); - *cursor += 2; - if len == 0 { - return Some(None); - } - let text_bytes = bytes.get(*cursor..*cursor + len)?; - *cursor += len; - Some(Some(String::from_utf8_lossy(text_bytes).into_owned())) -} - -fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { - match effect { - RuntimeEffect::SetChairmanCash { target, .. } - | RuntimeEffect::DeactivateChairman { target } => matches!( - target, - RuntimeChairmanTarget::AllActive - | RuntimeChairmanTarget::HumanChairmen - | RuntimeChairmanTarget::AiChairmen - | RuntimeChairmanTarget::SelectedChairman - | RuntimeChairmanTarget::ConditionTrueChairman - | RuntimeChairmanTarget::Ids { .. } - ), - RuntimeEffect::SetWorldFlag { .. } - | RuntimeEffect::SetWorldVariable { .. } - | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } - | RuntimeEffect::SetEconomicStatusCode { .. } - | RuntimeEffect::SetCompanyGovernanceScalar { .. } - | RuntimeEffect::SetCandidateAvailability { .. } - | RuntimeEffect::SetNamedLocomotiveAvailability { .. } - | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. } - | RuntimeEffect::SetNamedLocomotiveCost { .. } - | RuntimeEffect::SetCargoPriceOverride { .. } - | RuntimeEffect::SetCargoProductionOverride { .. } - | RuntimeEffect::SetCargoProductionSlot { .. } - | RuntimeEffect::SetWorldScalarOverride { .. } - | RuntimeEffect::SetTerritoryAccessCost { .. } - | RuntimeEffect::SetSpecialCondition { .. } - | RuntimeEffect::ConfiscateCompanyAssets { .. } - | RuntimeEffect::DeactivateCompany { .. } - | RuntimeEffect::DeactivatePlayer { .. } - | RuntimeEffect::SetCompanyTrackLayingCapacity { .. } - | RuntimeEffect::RetireTrains { .. } - | RuntimeEffect::ActivateEventRecord { .. } - | RuntimeEffect::DeactivateEventRecord { .. } - | RuntimeEffect::RemoveEventRecord { .. } => true, - RuntimeEffect::SetPlayerCash { target, .. } - | RuntimeEffect::SetPlayerVariable { target, .. } => matches!( - target, - RuntimePlayerTarget::AllActive - | RuntimePlayerTarget::Ids { .. } - | RuntimePlayerTarget::HumanPlayers - | RuntimePlayerTarget::AiPlayers - | RuntimePlayerTarget::SelectedPlayer - | RuntimePlayerTarget::ConditionTruePlayer - ), - RuntimeEffect::SetCompanyTerritoryAccess { - target, territory, .. - } => { - matches!( - target, - RuntimeCompanyTarget::AllActive - | RuntimeCompanyTarget::Ids { .. } - | RuntimeCompanyTarget::HumanCompanies - | RuntimeCompanyTarget::AiCompanies - | RuntimeCompanyTarget::SelectedCompany - | RuntimeCompanyTarget::ConditionTrueCompany - ) && matches!( - territory, - RuntimeTerritoryTarget::AllTerritories | RuntimeTerritoryTarget::Ids { .. } - ) - } - RuntimeEffect::SetCompanyCash { target, .. } - | RuntimeEffect::SetCompanyVariable { target, .. } - | RuntimeEffect::AdjustCompanyCash { target, .. } - | RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!( - target, - RuntimeCompanyTarget::AllActive - | RuntimeCompanyTarget::Ids { .. } - | RuntimeCompanyTarget::HumanCompanies - | RuntimeCompanyTarget::AiCompanies - | RuntimeCompanyTarget::SelectedCompany - | RuntimeCompanyTarget::ConditionTrueCompany - ), - RuntimeEffect::SetTerritoryVariable { target, .. } => matches!( - target, - RuntimeTerritoryTarget::AllTerritories | RuntimeTerritoryTarget::Ids { .. } - ), - RuntimeEffect::AppendEventRecord { record } => record - .effects - .iter() - .all(runtime_effect_supported_for_save_import), - } -} - -fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> bool { - match condition { - RuntimeCondition::WorldVariableThreshold { .. } - | RuntimeCondition::CompanyNumericThreshold { .. } - | RuntimeCondition::CompanyVariableThreshold { .. } - | RuntimeCondition::PlayerVariableThreshold { .. } - | RuntimeCondition::ChairmanNumericThreshold { .. } - | RuntimeCondition::TerritoryNumericThreshold { .. } - | RuntimeCondition::TerritoryVariableThreshold { .. } - | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } - | RuntimeCondition::SpecialConditionThreshold { .. } - | RuntimeCondition::CandidateAvailabilityThreshold { .. } - | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } - | RuntimeCondition::NamedLocomotiveCostThreshold { .. } - | RuntimeCondition::CargoProductionSlotThreshold { .. } - | RuntimeCondition::CargoProductionTotalThreshold { .. } - | RuntimeCondition::FactoryProductionTotalThreshold { .. } - | RuntimeCondition::FarmMineProductionTotalThreshold { .. } - | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } - | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } - | RuntimeCondition::TerritoryAccessCostThreshold { .. } - | RuntimeCondition::EconomicStatusCodeThreshold { .. } - | RuntimeCondition::WorldFlagEquals { .. } => true, - } -} - -fn build_unsupported_event_runtime_record_summaries( - live_entry_ids: &[u32], - note: &str, -) -> Vec { - live_entry_ids - .iter() - .copied() - .enumerate() - .map( - |(record_index, live_entry_id)| SmpLoadedPackedEventRecordSummary { - record_index, - live_entry_id, - payload_offset: None, - payload_len: None, - decode_status: "unsupported_framing".to_string(), - payload_family: "unsupported_framing".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - notes: vec![note.to_string()], - }, - ) - .collect() -} - -fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option) -> SmpInspectionReport { - let known_tag_hits = KNOWN_TAG_DEFINITIONS - .iter() - .filter_map(|definition| { - let offsets = find_u16_le_offsets(bytes, definition.tag_id); - if offsets.is_empty() { - return None; - } - - Some(SmpKnownTagHit { - tag_id: definition.tag_id, - tag_hex: format!("0x{:04x}", definition.tag_id), - label: definition.label.to_string(), - grounded_meaning: definition.grounded_meaning.to_string(), - hit_count: offsets.len(), - sample_offsets: offsets - .iter() - .copied() - .take(TAG_OFFSET_SAMPLE_LIMIT) - .collect(), - last_offset: offsets.last().copied(), - }) - }) - .collect::>(); - - let shared_header = parse_shared_header(bytes); - let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe); - let first_ascii_run = find_first_ascii_run(bytes); - let early_content_probe = first_ascii_run - .as_ref() - .and_then(|ascii_run| probe_early_content_layout(bytes, ascii_run)); - let secondary_variant_probe = early_content_probe - .as_ref() - .and_then(classify_secondary_variant_probe); - let container_profile = classify_container_profile( - file_extension_hint.as_deref(), - header_variant_probe.as_ref(), - secondary_variant_probe.as_ref(), - ); - let runtime_anchor_cycle_block = parse_runtime_anchor_cycle_block( - bytes, - container_profile.as_ref(), - secondary_variant_probe.as_ref(), - ); - let save_bootstrap_block = - parse_save_bootstrap_block(container_profile.as_ref(), secondary_variant_probe.as_ref()); - let save_anchor_run_block = parse_save_anchor_run_block( - bytes, - container_profile.as_ref(), - save_bootstrap_block.as_ref(), - ); - let runtime_trailer_block = parse_runtime_trailer_block( - container_profile.as_ref(), - runtime_anchor_cycle_block.as_ref(), - ); - let runtime_post_span_probe = - parse_runtime_post_span_probe(bytes, runtime_trailer_block.as_ref()); - let rt3_105_packed_profile_probe = parse_rt3_105_packed_profile_probe( - bytes, - file_extension_hint.as_deref(), - header_variant_probe.as_ref(), - container_profile.as_ref(), - ); - let rt3_105_post_span_bridge_probe = parse_rt3_105_post_span_bridge_probe( - runtime_trailer_block.as_ref(), - runtime_post_span_probe.as_ref(), - rt3_105_packed_profile_probe.as_ref(), - ); - let rt3_105_save_bridge_payload_probe = - parse_rt3_105_save_bridge_payload_probe(bytes, rt3_105_post_span_bridge_probe.as_ref()); - let save_world_selection_context_probe = parse_save_world_selection_context_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_world_issue_37_probe = parse_save_world_issue_37_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_world_economic_tuning_probe = parse_save_world_economic_tuning_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_world_finance_neighborhood_probe = parse_save_world_finance_neighborhood_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_company_collection_header_probe = parse_save_company_collection_header_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_chairman_profile_collection_header_probe = - parse_save_chairman_profile_collection_header_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_train_collection_header_probe = parse_save_train_collection_header_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_train_collection_directory_probe = parse_save_train_collection_directory_probe( - bytes, - save_train_collection_header_probe.as_ref(), - ); - let save_region_collection_header_probe = parse_save_region_collection_header_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_region_record_triplet_probe = - parse_save_region_record_triplet_probe(bytes, save_region_collection_header_probe.as_ref()); - let save_region_queued_notice_record_probe = parse_save_region_queued_notice_record_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - save_region_collection_header_probe.as_ref(), - ); - let save_region_fixed_row_run_candidate_probe = parse_save_region_fixed_row_run_candidate_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - save_region_collection_header_probe.as_ref(), - ); - let save_placed_structure_collection_header_probe = - parse_save_placed_structure_collection_header_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let save_placed_structure_record_triplet_probe = - parse_save_placed_structure_record_triplet_probe( - bytes, - save_placed_structure_collection_header_probe.as_ref(), - ); - let save_placed_structure_dynamic_side_buffer_probe = - parse_save_placed_structure_dynamic_side_buffer_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let known_header_probes = [ - save_company_collection_header_probe.as_ref(), - save_chairman_profile_collection_header_probe.as_ref(), - save_train_collection_header_probe.as_ref(), - save_region_collection_header_probe.as_ref(), - save_placed_structure_collection_header_probe.as_ref(), - ]; - let save_unclassified_tagged_collection_header_probes = - filter_unclassified_tagged_collection_header_probes_outside_known_spans( - scan_save_unclassified_tagged_collection_header_probes( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ), - &known_header_probes, - ); - let save_company_roster_probe = parse_save_company_roster_probe( - bytes, - save_company_collection_header_probe.as_ref(), - save_world_selection_context_probe.as_ref(), - ); - let save_chairman_profile_table_probe = parse_save_chairman_profile_table_probe( - bytes, - save_chairman_profile_collection_header_probe.as_ref(), - save_world_selection_context_probe.as_ref(), - save_company_collection_header_probe.as_ref(), - ); - let map_title_hint_probe = parse_map_title_hint_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let rt3_105_save_name_table_probe = parse_rt3_105_save_name_table_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - rt3_105_save_bridge_payload_probe.as_ref(), - ); - let rt3_105_save_named_locomotive_availability_probe = - parse_rt3_105_save_named_locomotive_availability_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - rt3_105_packed_profile_probe.as_ref(), - ); - let special_conditions_probe = parse_special_conditions_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - ); - let smp_aligned_runtime_rule_band_probe = parse_smp_aligned_runtime_rule_band_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - special_conditions_probe.as_ref(), - ); - let post_special_conditions_scalar_probe = parse_post_special_conditions_scalar_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - special_conditions_probe.as_ref(), - ); - let post_text_field_neighborhood_probe = parse_post_text_field_neighborhood_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - special_conditions_probe.as_ref(), - ); - let locomotive_policy_neighborhood_probe = parse_locomotive_policy_neighborhood_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - special_conditions_probe.as_ref(), - ); - let pre_recipe_scalar_plateau_probe = parse_pre_recipe_scalar_plateau_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - special_conditions_probe.as_ref(), - ); - let recipe_book_summary_probe = parse_recipe_book_summary_probe( - bytes, - file_extension_hint.as_deref(), - container_profile.as_ref(), - special_conditions_probe.as_ref(), - ); - let classic_rehydrate_profile_probe = - parse_classic_rehydrate_profile_probe(bytes, runtime_post_span_probe.as_ref()); - let save_load_summary = build_save_load_summary( - file_extension_hint.as_deref(), - container_profile.as_ref(), - runtime_trailer_block.as_ref(), - rt3_105_post_span_bridge_probe.as_ref(), - classic_rehydrate_profile_probe.as_ref(), - rt3_105_packed_profile_probe.as_ref(), - rt3_105_save_name_table_probe.as_ref(), - ); - let event_runtime_collection_summary = parse_event_runtime_collection_summary( - bytes, - container_profile.as_ref(), - save_load_summary.as_ref(), - ); - let mut warnings = Vec::new(); - if bytes.is_empty() { - warnings - .push("File is empty, so no `.smp` container structure could be observed.".to_string()); - } - - if known_tag_hits.is_empty() { - warnings.push( - "No grounded runtime bundle tags were found in little-endian form. This does not prove the file is invalid." - .to_string(), - ); - } - if shared_header.is_none() && !bytes.is_empty() { - warnings.push( - "File is shorter than the observed 64-byte common RT3 bundle preamble.".to_string(), - ); - } - if let Some(shared_header) = &shared_header { - let header_family_is_known = header_variant_probe - .as_ref() - .map(|probe| probe.is_known_family) - .unwrap_or(false); - if !shared_header.matches_grounded_common_signature && !header_family_is_known { - warnings.push( - "The first 64-byte preamble does not match the currently observed shared RT3 bundle signature." - .to_string(), - ); - } - } - if first_ascii_run.is_some() && early_content_probe.is_none() { - warnings.push( - "Found early text content but could not resolve the next stable nonzero region after its zero padding." - .to_string(), - ); - } - if container_profile - .as_ref() - .is_some_and(|profile| !profile.is_known_profile) - { - warnings.push( - "The current probes did not match any known composite container profile.".to_string(), - ); - } - if known_tag_hits - .iter() - .any(|hit| hit.hit_count > hit.sample_offsets.len()) - { - warnings.push( - "Known-tag offsets are sampled in this report. Large hit counts usually mean byte-pattern noise, not validated chunk boundaries." - .to_string(), - ); - } - warnings.push( - "Inspection scans raw bytes for a small grounded tag set only. It does not validate bundle layout or decode payloads." - .to_string(), - ); - - SmpInspectionReport { - inspection_mode: "grounded-tag-scan-plus-preamble".to_string(), - file_extension_hint, - file_size: bytes.len(), - sha256: sha256_hex(bytes), - preamble: parse_preamble(bytes), - shared_header, - header_variant_probe, - first_ascii_run, - early_content_probe, - secondary_variant_probe, - container_profile, - save_bootstrap_block, - save_anchor_run_block, - runtime_anchor_cycle_block, - runtime_trailer_block, - runtime_post_span_probe, - rt3_105_post_span_bridge_probe, - rt3_105_save_bridge_payload_probe, - save_world_selection_context_probe, - save_world_issue_37_probe, - save_world_economic_tuning_probe, - save_world_finance_neighborhood_probe, - save_company_collection_header_probe, - save_chairman_profile_collection_header_probe, - save_train_collection_header_probe, - save_train_collection_directory_probe, - save_region_collection_header_probe, - save_region_record_triplet_probe, - save_region_queued_notice_record_probe, - save_region_fixed_row_run_candidate_probe, - save_placed_structure_collection_header_probe, - save_placed_structure_record_triplet_probe, - save_placed_structure_dynamic_side_buffer_probe, - save_unclassified_tagged_collection_header_probes, - save_company_roster_probe, - save_chairman_profile_table_probe, - map_title_hint_probe, - rt3_105_save_name_table_probe, - rt3_105_save_named_locomotive_availability_probe, - special_conditions_probe, - smp_aligned_runtime_rule_band_probe, - post_special_conditions_scalar_probe, - post_text_field_neighborhood_probe, - locomotive_policy_neighborhood_probe, - pre_recipe_scalar_plateau_probe, - recipe_book_summary_probe, - classic_rehydrate_profile_probe, - rt3_105_packed_profile_probe, - save_load_summary, - event_runtime_collection_summary, - contains_grounded_runtime_tags: !known_tag_hits.is_empty(), - known_tag_hits, - notes: vec![ - "Grounded `.smp` runtime tags currently include mask-plane payload ids 0x2cee and 0x2d51.".to_string(), - "Grounded sidecar-byte-plane bundle family currently spans 0x9471..0x9472.".to_string(), - "The shared-header parse is intentionally conservative: it only names common preamble lanes and checks the observed RT3 bundle-family signature.".to_string(), - "The header-variant probe classifies the preamble into one of the currently observed install-era families when possible." - .to_string(), - "The early-content probe resolves the first stable nonzero block after the padded scenario text and then captures the next aligned word window." - .to_string(), - "The secondary-variant probe classifies that aligned word window into one of the currently observed file-family patterns." - .to_string(), - "The recipe-book summary probe reports per-book structural signatures at the grounded recipe-book root [world+0x0fe7] without attempting a full cargo-line decode." - .to_string(), - "Where a recipe cargo-token word looks like two printable letters in its high 16 bits, the probe exposes that as one probable ASCII stem while still treating the wider token semantics as inferred." - .to_string(), - "The container-profile layer combines extension hint, header family, and second-window family into one observed container classification." - .to_string(), - "The save-bootstrap reader currently parses one conservative 8-word descriptor only for known save-container profiles." - .to_string(), - "The save-anchor-run reader follows that descriptor tail into the observed repeated 9-word anchor cycle and captures the first trailer words after the cycle diverges." - .to_string(), - "The runtime-anchor-cycle reader applies the same cycle/trailer scan across the currently known save and sandbox runtime container profiles." - .to_string(), - "The runtime-trailer reader classifies the first 16 words after the cycle divergence into the currently observed runtime trailer families." - .to_string(), - "The runtime post-span probe follows the trailer's high-16 span lane into the later file region and records the next nonzero bytes, the first aligned high-16-dense candidate window, and any grounded progress-id hits found nearby." - .to_string(), - "The RT3 1.05 post-span bridge probe correlates the trailer selector/descriptor lanes with the next candidate region and the later packed-profile block for the currently observed 1.05 save families." - .to_string(), - "The RT3 1.05 common-save bridge payload probe captures the two stable bridge-stage blocks currently observed under the base 1.05 save branch." - .to_string(), - "The RT3 1.05 candidate-availability table probe decodes the fixed-width trailing name table from either the common-save bridge payload or the fixed 0x6a70..0x73c0 source range when that header validates." - .to_string(), - "The RT3 1.05 save-side named locomotive availability probe scans the post-profile save region for the grounded fixed-width locomotive-name-plus-dword row family when that run is present." - .to_string(), - "The post-special-conditions scalar probe captures the fixed 0x0df4..0x0f30 dword window immediately after the hidden sentinel slot, splits it into the aligned-band overlap prefix and the later tail, and records the live-object offset alignment of that tail without claiming a byte-for-byte mirror." - .to_string(), - "The classic rehydrate-profile probe recognizes the grounded 0x32dc -> 0x3714 -> 0x3715 progress-id sequence and captures the exact 0x108-byte block between the latter two ids when that pattern appears." - .to_string(), - "The classic packed-profile block reader exposes the stable map-path, display-name, atlas-tracked latch bytes, and the small set of nonzero word lanes observed inside that 0x108-byte block." - .to_string(), - "The RT3 1.05 packed-profile probe recognizes the later string-bearing save block rooted at the first post-header .gmp path and exposes the observed map-path, display-name, atlas-tracked byte lanes, and stable nonzero words." - .to_string(), - format!( - "Restore-side loading of the four sidecar byte planes is only grounded for bundle versions >= 0x{:04x}.", - SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION - ), - ], - warnings, - } -} - -fn build_save_load_summary( - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>, - rt3_105_post_span_bridge_probe: Option<&SmpRt3105PostSpanBridgeProbe>, - classic_rehydrate_profile_probe: Option<&SmpClassicRehydrateProfileProbe>, - rt3_105_packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>, - rt3_105_save_name_table_probe: Option<&SmpRt3105SaveNameTableProbe>, -) -> Option { - let file_extension_hint = file_extension_hint.map(str::to_string); - let container_profile_family = container_profile.map(|profile| profile.profile_family.clone()); - let trailer_family = runtime_trailer_block.map(|trailer| trailer.trailer_family.clone()); - let bridge_family = rt3_105_post_span_bridge_probe.map(|bridge| bridge.bridge_family.clone()); - let candidate_table = - rt3_105_save_name_table_probe.map(|probe| SmpSaveLoadCandidateTableSummary { - source_kind: probe.source_kind.clone(), - semantic_family: probe.semantic_family.clone(), - observed_entry_count: probe.observed_entry_count, - zero_availability_count: probe.zero_trailer_entry_count, - zero_availability_names: probe.zero_trailer_entry_names.clone(), - footer_progress_hex_words: vec![ - probe.footer_progress_word_0_hex.clone(), - probe.footer_progress_word_1_hex.clone(), - ], - }); - - if let Some(probe) = classic_rehydrate_profile_probe { - let block = &probe.packed_profile_block; - let mut notes = vec![ - "Classic save load reaches the grounded late rehydrate band 0x32dc -> 0x3714 -> 0x3715." - .to_string(), - "The file exposes one exact 0x108 packed-profile block between progress ids 0x3714 and 0x3715." - .to_string(), - ]; - if let Some(map_path) = &block.map_path { - notes.push(format!("Packed profile map path: {map_path}")); - } - if let Some(display_name) = &block.display_name { - notes.push(format!("Packed profile display name: {display_name}")); - } - - return Some(SmpSaveLoadSummary { - file_extension_hint, - container_profile_family, - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - packed_profile_kind: Some("classic-rehydrate-profile".to_string()), - packed_profile_family: Some(probe.profile_family.clone()), - packed_profile_offset: Some(probe.packed_profile_offset), - packed_profile_len: Some(probe.packed_profile_len), - map_path: block.map_path.clone(), - display_name: block.display_name.clone(), - profile_byte_0x77: Some(block.profile_byte_0x77), - profile_byte_0x77_hex: Some(block.profile_byte_0x77_hex.clone()), - profile_byte_0x82: Some(block.profile_byte_0x82), - profile_byte_0x82_hex: Some(block.profile_byte_0x82_hex.clone()), - profile_byte_0x97: Some(block.profile_byte_0x97), - profile_byte_0x97_hex: Some(block.profile_byte_0x97_hex.clone()), - profile_byte_0xc5: Some(block.profile_byte_0xc5), - profile_byte_0xc5_hex: Some(block.profile_byte_0xc5_hex.clone()), - trailer_family, - bridge_family: None, - candidate_table, - notes, - }); - } - - if let Some(probe) = rt3_105_packed_profile_probe { - let block = &probe.packed_profile_block; - let mechanism_family = rt3_105_post_span_bridge_probe - .map(|bridge| bridge.bridge_family.clone()) - .unwrap_or_else(|| match probe.profile_family.as_str() { - "rt3-105-scenario-save-container-v1" => { - "rt3-105-scenario-save-profile-analog-v1".to_string() - } - "rt3-105-alt-save-container-v1" => "rt3-105-alt-save-profile-analog-v1".to_string(), - _ => "rt3-105-save-profile-analog-v1".to_string(), - }); - let mechanism_confidence = if rt3_105_post_span_bridge_probe.is_some() { - "mixed" - } else { - "inferred" - } - .to_string(); - let mut notes = Vec::new(); - if let Some(bridge) = rt3_105_post_span_bridge_probe { - notes.push(format!( - "RT3 1.05 save branch uses {} with selector/descriptor {} -> {}.", - bridge.bridge_family, bridge.selector_high_hex, bridge.descriptor_high_hex - )); - } else { - notes.push( - "RT3 1.05 save exposes a packed-profile analogue, but the upstream load bridge is not resolved for this branch." - .to_string(), - ); - } - if let Some(map_path) = &block.map_path { - notes.push(format!("Packed profile map path: {map_path}")); - } - if let Some(display_name) = &block.display_name { - notes.push(format!("Packed profile display name: {display_name}")); - } - if let Some(table) = &candidate_table { - notes.push(format!( - "Candidate table source {} carries {} entries with {} zero-availability overrides.", - table.source_kind, table.observed_entry_count, table.zero_availability_count - )); - } - - return Some(SmpSaveLoadSummary { - file_extension_hint, - container_profile_family, - mechanism_family, - mechanism_confidence, - packed_profile_kind: Some("rt3-105-packed-profile".to_string()), - packed_profile_family: Some(probe.profile_family.clone()), - packed_profile_offset: Some(probe.packed_profile_offset), - packed_profile_len: Some(probe.packed_profile_len), - map_path: block.map_path.clone(), - display_name: block.display_name.clone(), - profile_byte_0x77: Some(block.profile_byte_0x77), - profile_byte_0x77_hex: Some(block.profile_byte_0x77_hex.clone()), - profile_byte_0x82: Some(block.profile_byte_0x82), - profile_byte_0x82_hex: Some(block.profile_byte_0x82_hex.clone()), - profile_byte_0x97: Some(block.profile_byte_0x97), - profile_byte_0x97_hex: Some(block.profile_byte_0x97_hex.clone()), - profile_byte_0xc5: Some(block.profile_byte_0xc5), - profile_byte_0xc5_hex: Some(block.profile_byte_0xc5_hex.clone()), - trailer_family, - bridge_family, - candidate_table, - notes, - }); - } - - if let Some(table) = candidate_table { - return Some(SmpSaveLoadSummary { - file_extension_hint, - container_profile_family, - mechanism_family: "rt3-105-candidate-catalog-source-v1".to_string(), - mechanism_confidence: "mixed".to_string(), - packed_profile_kind: None, - packed_profile_family: None, - packed_profile_offset: None, - packed_profile_len: None, - map_path: None, - display_name: None, - profile_byte_0x77: None, - profile_byte_0x77_hex: None, - profile_byte_0x82: None, - profile_byte_0x82_hex: None, - profile_byte_0x97: None, - profile_byte_0x97_hex: None, - profile_byte_0xc5: None, - profile_byte_0xc5_hex: None, - trailer_family, - bridge_family, - notes: vec![ - format!( - "The file carries the shared 1.05 candidate table source block through {}.", - table.source_kind - ), - format!( - "The table exposes {} named entries with {} zero-availability overrides.", - table.observed_entry_count, table.zero_availability_count - ), - ], - candidate_table: Some(table), - }); - } - - None -} - -fn parse_preamble(bytes: &[u8]) -> SmpPreamble { - let byte_len = bytes.len().min(PREAMBLE_U32_WORD_COUNT * 4); - let words = bytes[..byte_len] - .chunks_exact(4) - .enumerate() - .map(|(index, chunk)| { - let value_le = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - SmpPreambleWord { - index, - offset: index * 4, - value_le, - value_hex: format!("0x{value_le:08x}"), - } - }) - .collect::>(); - - SmpPreamble { - byte_len, - word_count: words.len(), - words, - } -} - -fn parse_shared_header(bytes: &[u8]) -> Option { - let words = read_preamble_words(bytes)?; - let shared_signature_words_1_to_7 = words[1..=7].to_vec(); - let payload_window_words_8_to_9 = words[8..=9].to_vec(); - let reserved_words_10_to_14 = words[10..=14].to_vec(); - let final_flag_word = words[15]; - - Some(SmpSharedHeader { - byte_len: PREAMBLE_U32_WORD_COUNT * 4, - root_kind_word: words[0], - root_kind_word_hex: format!("0x{:08x}", words[0]), - primary_family_tag: words[1], - primary_family_tag_hex: format!("0x{:08x}", words[1]), - shared_signature_hex_words_1_to_7: shared_signature_words_1_to_7 - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - matches_grounded_common_signature: shared_signature_words_1_to_7 - == SHARED_SIGNATURE_WORDS_1_TO_7, - shared_signature_words_1_to_7, - payload_window_hex_words_8_to_9: payload_window_words_8_to_9 - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - payload_window_words_8_to_9, - reserved_words_10_to_14_all_zero: reserved_words_10_to_14.iter().all(|word| *word == 0), - reserved_words_10_to_14, - final_flag_word, - final_flag_word_hex: format!("0x{final_flag_word:08x}"), - }) -} - -fn classify_header_variant_probe(shared_header: &SmpSharedHeader) -> SmpHeaderVariantProbe { - let words = &shared_header.shared_signature_words_1_to_7; - let root = shared_header.root_kind_word; - let final_flag = shared_header.final_flag_word; - - let (variant_family, evidence, is_known_family) = match (root, words.as_slice(), final_flag) { - ( - 0x00002649, - [ - 0x00002ee0, - 0x00040001, - 0x00028000, - 0x00010000, - 0x00000771, - 0x00000771, - 0x00000771, - ], - 0x00000001, - ) => ( - "rt3-105-gmx-header-v1".to_string(), - vec![ - "root kind word 0x00002649".to_string(), - "1.05 common signature words 1..7".to_string(), - "final flag 0x00000001".to_string(), - ], - true, - ), - ( - 0x000025e5, - [ - 0x00002ee0, - 0x00040001, - 0x00028000, - 0x00010000, - 0x00000771, - 0x00000771, - 0x00000771, - ], - 0x00000000, - ) => ( - "rt3-105-common-header-v1".to_string(), - vec![ - "root kind word 0x000025e5".to_string(), - "1.05 common signature words 1..7".to_string(), - "final flag 0x00000000".to_string(), - ], - true, - ), - ( - 0x000025e5, - [ - 0x00002ee0, - 0x00040001, - 0x00018000, - 0x00010000, - 0x00000746, - 0x00000746, - 0x00000746, - ], - 0x00000000, - ) => ( - "rt3-105-scenario-save-header-v1".to_string(), - vec![ - "root kind word 0x000025e5".to_string(), - "1.05 scenario-save signature words 1..7".to_string(), - "final flag 0x00000000".to_string(), - ], - true, - ), - ( - 0x000025e5, - [ - 0x00002ee0, - 0x0001c001, - 0x00018000, - 0x00010000, - 0x00000754, - 0x00000754, - 0x00000754, - ], - 0x00000000, - ) => ( - "rt3-105-alt-save-header-v1".to_string(), - vec![ - "root kind word 0x000025e5".to_string(), - "1.05 alternate-save signature words 1..7".to_string(), - "final flag 0x00000000".to_string(), - ], - true, - ), - ( - 0x000026ad, - [ - 0x00002ee0, - 0x00014001, - 0x00020000, - 0x00010000, - 0x00000725, - 0x00000725, - 0x00000725, - ], - 0x00000100, - ) => ( - "rt3-classic-gms-header-v1".to_string(), - vec![ - "root kind word 0x000026ad".to_string(), - "classic save signature words 1..7".to_string(), - "final flag 0x00000100".to_string(), - ], - true, - ), - ( - 0x000026ad, - [ - 0x00002ee0, - 0x0001c001, - 0x00018000, - 0x00010000, - 0x00000765, - 0x00000765, - 0x00000765, - ], - 0x00000001, - ) => ( - "rt3-classic-gmx-header-v1".to_string(), - vec![ - "root kind word 0x000026ad".to_string(), - "classic sandbox signature words 1..7".to_string(), - "final flag 0x00000001".to_string(), - ], - true, - ), - (0x000025e5, [0x00002ee0, _, _, 0x00010000, _, _, _], 0x00000000 | 0x00000100) => ( - "rt3-map-header-family".to_string(), - vec![ - "root kind word 0x000025e5".to_string(), - "map-family anchor 0x00002ee0".to_string(), - "word4 0x00010000".to_string(), - ], - true, - ), - _ => ( - "unknown".to_string(), - vec![format!( - "root=0x{root:08x}, words1..7={}, final=0x{final_flag:08x}", - words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect::>() - .join(", ") - )], - false, - ), - }; - - SmpHeaderVariantProbe { - variant_family, - variant_evidence: evidence, - is_known_family, - } -} - -fn read_preamble_words(bytes: &[u8]) -> Option<[u32; PREAMBLE_U32_WORD_COUNT]> { - if bytes.len() < PREAMBLE_U32_WORD_COUNT * 4 { - return None; - } - - let mut words = [0u32; PREAMBLE_U32_WORD_COUNT]; - for (index, chunk) in bytes[..PREAMBLE_U32_WORD_COUNT * 4] - .chunks_exact(4) - .enumerate() - { - words[index] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - } - Some(words) -} - -fn probe_early_content_layout( - bytes: &[u8], - ascii_run: &SmpAsciiPreview, -) -> Option { - let search_start = ascii_run.offset + ascii_run.byte_len; - let first_post_text_nonzero_offset = find_next_nonzero_offset(bytes, search_start)?; - let zero_pad_after_text_len = first_post_text_nonzero_offset.saturating_sub(search_start); - let first_zero_run_after_block = find_zero_run( - bytes, - first_post_text_nonzero_offset, - EARLY_ZERO_RUN_THRESHOLD, - ) - .unwrap_or(bytes.len()); - let first_post_text_block = &bytes[first_post_text_nonzero_offset..first_zero_run_after_block]; - let secondary_nonzero_offset = find_next_nonzero_offset(bytes, first_zero_run_after_block); - let trailing_zero_pad_after_first_block_len = secondary_nonzero_offset - .map(|offset| offset.saturating_sub(first_zero_run_after_block)) - .unwrap_or_else(|| bytes.len().saturating_sub(first_zero_run_after_block)); - let secondary_aligned_word_window_offset = secondary_nonzero_offset.map(|offset| offset & !0x3); - let secondary_aligned_word_window_words = secondary_aligned_word_window_offset - .map(|offset| read_u32_window(bytes, offset, EARLY_ALIGNED_WORD_WINDOW_COUNT)) - .unwrap_or_default(); - let secondary_preview_hex = secondary_nonzero_offset - .map(|offset| { - hex_encode(&bytes[offset..bytes.len().min(offset + EARLY_PREVIEW_BYTE_LIMIT)]) - }) - .unwrap_or_default(); - - Some(SmpEarlyContentProbe { - first_post_text_nonzero_offset, - zero_pad_after_text_len, - first_post_text_block_len: first_post_text_block.len(), - first_post_text_block_hex: hex_encode(first_post_text_block), - trailing_zero_pad_after_first_block_len, - secondary_nonzero_offset, - secondary_aligned_word_window_offset, - secondary_aligned_word_window_hex_words: secondary_aligned_word_window_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - secondary_aligned_word_window_words, - secondary_preview_hex, - }) -} - -fn classify_secondary_variant_probe( - probe: &SmpEarlyContentProbe, -) -> Option { - let aligned_window_offset = probe.secondary_aligned_word_window_offset?; - let words = probe.secondary_aligned_word_window_words.clone(); - if words.is_empty() { - return None; - } - - let mut evidence = Vec::new(); - let variant_family = match words.as_slice() { - [0x001e0000, 0x86a00100, 0x03000001, 0xf0000100, ..] => { - evidence.push("leading word 0x001e0000".to_string()); - evidence.push("anchor word 0x86a00100".to_string()); - evidence.push("third/fourth words 0x03000001 and 0xf0000100".to_string()); - "rt3-gms-family-v1".to_string() - } - [0x000a0000, 0x49f00100, 0x00000002, 0xa0000000, ..] => { - evidence.push("leading word 0x000a0000".to_string()); - evidence.push("anchor word 0x49f00100".to_string()); - evidence.push("third/fourth words 0x00000002 and 0xa0000000".to_string()); - "rt3-gmx-family-v1".to_string() - } - [0x001c0000, 0x86a00100, 0x00000001, 0xa0000000, ..] => { - evidence.push("leading word 0x001c0000".to_string()); - evidence.push("anchor word 0x86a00100".to_string()); - evidence.push("third/fourth words 0x00000001 and 0xa0000000".to_string()); - "rt3-105-gms-family-v1".to_string() - } - [0x00190000, 0x86a00100, 0x00000001, 0xa0000000, ..] => { - evidence.push("leading word 0x00190000".to_string()); - evidence.push("anchor word 0x86a00100".to_string()); - evidence.push("third/fourth words 0x00000001 and 0xa0000000".to_string()); - "rt3-105-gmx-family-v1".to_string() - } - [0x00130000, 0x86a00100, 0x21000001, 0xa0000100, ..] => { - evidence.push("leading word 0x00130000".to_string()); - evidence.push("anchor word 0x86a00100".to_string()); - evidence.push("third/fourth words 0x21000001 and 0xa0000100".to_string()); - "rt3-105-gms-scenario-family-v1".to_string() - } - [0x00140000, 0x93e00100, 0x00000004, 0xa0000000, ..] => { - evidence.push("leading word 0x00140000".to_string()); - evidence.push("anchor word 0x93e00100".to_string()); - evidence.push("third/fourth words 0x00000004 and 0xa0000000".to_string()); - "rt3-map-secondary-family-v1".to_string() - } - [0x00010000, 0x49f00100, 0x00000002, 0xa0000000, ..] => { - evidence.push("leading word 0x00010000".to_string()); - evidence.push("anchor word 0x49f00100".to_string()); - evidence.push("third/fourth words 0x00000002 and 0xa0000000".to_string()); - "rt3-105-gms-alt-family-v1".to_string() - } - [0x86a00100, 0x00000001, 0xa0000000, 0x00000186, ..] => { - evidence.push("window starts directly on 0x86a00100".to_string()); - evidence.push("likely same family with missing leading unaligned word".to_string()); - "rt3-family-unaligned-anchor".to_string() - } - _ => { - evidence.push(format!( - "unrecognized leading words: {}", - words - .iter() - .take(4) - .map(|word| format!("0x{word:08x}")) - .collect::>() - .join(", ") - )); - "unknown".to_string() - } - }; - - Some(SmpSecondaryVariantProbe { - aligned_window_offset, - hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(), - words, - variant_family, - variant_evidence: evidence, - }) -} - -fn classify_container_profile( - file_extension_hint: Option<&str>, - header_variant_probe: Option<&SmpHeaderVariantProbe>, - secondary_variant_probe: Option<&SmpSecondaryVariantProbe>, -) -> Option { - let header_family = header_variant_probe.map(|probe| probe.variant_family.as_str())?; - let secondary_family = secondary_variant_probe.map(|probe| probe.variant_family.as_str())?; - let extension = file_extension_hint.unwrap_or(""); - - let (profile_family, profile_evidence, is_known_profile) = - match (extension, header_family, secondary_family) { - ("gms", "rt3-classic-gms-header-v1", "rt3-gms-family-v1") => ( - "rt3-classic-save-container-v1".to_string(), - vec![ - "extension .gms".to_string(), - "classic save header family".to_string(), - "classic save secondary window family".to_string(), - ], - true, - ), - ("gmx", "rt3-classic-gmx-header-v1", "rt3-gmx-family-v1") => ( - "rt3-classic-sandbox-container-v1".to_string(), - vec![ - "extension .gmx".to_string(), - "classic sandbox header family".to_string(), - "classic sandbox secondary window family".to_string(), - ], - true, - ), - ("gms", "rt3-105-common-header-v1", "rt3-105-gms-family-v1") => ( - "rt3-105-save-container-v1".to_string(), - vec![ - "extension .gms".to_string(), - "1.05 common header family".to_string(), - "1.05 save secondary window family".to_string(), - ], - true, - ), - ("gms", "rt3-105-scenario-save-header-v1", "rt3-105-gms-scenario-family-v1") => ( - "rt3-105-scenario-save-container-v1".to_string(), - vec![ - "extension .gms".to_string(), - "1.05 scenario-save header family".to_string(), - "1.05 scenario-save secondary window family".to_string(), - ], - true, - ), - ("gms", "rt3-105-alt-save-header-v1", "rt3-105-gms-alt-family-v1") => ( - "rt3-105-alt-save-container-v1".to_string(), - vec![ - "extension .gms".to_string(), - "1.05 alternate-save header family".to_string(), - "1.05 alternate-save secondary window family".to_string(), - ], - true, - ), - ("gmx", "rt3-105-gmx-header-v1", "rt3-105-gmx-family-v1") => ( - "rt3-105-sandbox-container-v1".to_string(), - vec![ - "extension .gmx".to_string(), - "1.05 sandbox header family".to_string(), - "1.05 sandbox secondary window family".to_string(), - ], - true, - ), - ("gmp", "rt3-105-common-header-v1", "rt3-family-unaligned-anchor") => ( - "rt3-105-map-container-v1".to_string(), - vec![ - "extension .gmp".to_string(), - "1.05 common header family".to_string(), - "map-style secondary unaligned anchor".to_string(), - ], - true, - ), - ("gmp", "rt3-105-scenario-save-header-v1", "unknown") => ( - "rt3-105-scenario-map-container-v1".to_string(), - vec![ - "extension .gmp".to_string(), - "1.05 scenario-map header family".to_string(), - "fixed candidate-availability table range present despite unknown early secondary window".to_string(), - ], - true, - ), - ("gmp", "rt3-105-alt-save-header-v1", "unknown") => ( - "rt3-105-alt-map-container-v1".to_string(), - vec![ - "extension .gmp".to_string(), - "1.05 alternate-map header family".to_string(), - "fixed candidate-availability table range present despite unknown early secondary window".to_string(), - ], - true, - ), - ("gmp", "rt3-map-header-family", "rt3-family-unaligned-anchor") => ( - "rt3-map-container-family".to_string(), - vec![ - "extension .gmp".to_string(), - "map header family".to_string(), - "map-style secondary unaligned anchor".to_string(), - ], - true, - ), - ("gmp", "rt3-map-header-family", "rt3-map-secondary-family-v1") => ( - "rt3-map-container-family".to_string(), - vec![ - "extension .gmp".to_string(), - "map header family".to_string(), - "observed map secondary window family".to_string(), - ], - true, - ), - (_, header_family, secondary_family) => ( - "unknown".to_string(), - vec![ - format!( - "extension {}", - if extension.is_empty() { - "" - } else { - extension - } - ), - format!("header family {header_family}"), - format!("secondary family {secondary_family}"), - ], - false, - ), - }; - - Some(SmpContainerProfile { - profile_family, - profile_evidence, - is_known_profile, - }) -} - -fn parse_save_bootstrap_block( - container_profile: Option<&SmpContainerProfile>, - secondary_variant_probe: Option<&SmpSecondaryVariantProbe>, -) -> Option { - let profile = container_profile?; - let secondary = secondary_variant_probe?; - let words = &secondary.words; - if words.len() < 8 { - return None; - } - - let supported = matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ); - if !supported { - return None; - } - - Some(SmpSaveBootstrapBlock { - profile_family: profile.profile_family.clone(), - aligned_window_offset: secondary.aligned_window_offset, - leading_word: words[0], - leading_word_hex: format!("0x{:08x}", words[0]), - anchor_word: words[1], - anchor_word_hex: format!("0x{:08x}", words[1]), - descriptor_word_2: words[2], - descriptor_word_2_hex: format!("0x{:08x}", words[2]), - descriptor_word_3: words[3], - descriptor_word_3_hex: format!("0x{:08x}", words[3]), - descriptor_word_4: words[4], - descriptor_word_4_hex: format!("0x{:08x}", words[4]), - descriptor_word_5: words[5], - descriptor_word_5_hex: format!("0x{:08x}", words[5]), - descriptor_word_6: words[6], - descriptor_word_6_hex: format!("0x{:08x}", words[6]), - descriptor_word_7: words[7], - descriptor_word_7_hex: format!("0x{:08x}", words[7]), - }) -} - -fn parse_runtime_anchor_cycle_block( - bytes: &[u8], - container_profile: Option<&SmpContainerProfile>, - secondary_variant_probe: Option<&SmpSecondaryVariantProbe>, -) -> Option { - let profile = container_profile?; - let secondary = secondary_variant_probe?; - let supported = matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-classic-sandbox-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - | "rt3-105-sandbox-container-v1" - ); - if !supported { - return None; - } - - let cycle_start_offset = secondary.aligned_window_offset + 0x1c; - let cycle_words = read_u32_window(bytes, cycle_start_offset, 9); - if cycle_words.len() < 9 { - return None; - } - - let mut full_cycle_count = 0usize; - let mut cursor = cycle_start_offset; - while read_u32_window(bytes, cursor, cycle_words.len()) == cycle_words { - full_cycle_count += 1; - cursor += cycle_words.len() * 4; - } - - if full_cycle_count == 0 { - return None; - } - - let mut partial_cycle_word_count = 0usize; - while partial_cycle_word_count < cycle_words.len() { - let offset = cursor + partial_cycle_word_count * 4; - if read_u32_at(bytes, offset) == Some(cycle_words[partial_cycle_word_count]) { - partial_cycle_word_count += 1; - } else { - break; - } - } - - let trailer_offset = cursor + partial_cycle_word_count * 4; - let trailer_words = read_u32_window(bytes, trailer_offset, 16); - - Some(SmpRuntimeAnchorCycleBlock { - profile_family: profile.profile_family.clone(), - cycle_start_offset, - cycle_hex_words: cycle_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - cycle_words, - full_cycle_count, - partial_cycle_word_count, - trailer_offset, - trailer_hex_words: trailer_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - trailer_words, - }) -} - -fn parse_save_anchor_run_block( - bytes: &[u8], - container_profile: Option<&SmpContainerProfile>, - save_bootstrap_block: Option<&SmpSaveBootstrapBlock>, -) -> Option { - let profile = container_profile?; - let bootstrap = save_bootstrap_block?; - let supported = matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ); - if !supported { - return None; - } - - let cycle_start_offset = bootstrap.aligned_window_offset + 0x1c; - let cycle_words = read_u32_window(bytes, cycle_start_offset, 9); - if cycle_words.len() < 9 { - return None; - } - - let mut full_cycle_count = 0usize; - let mut cursor = cycle_start_offset; - while read_u32_window(bytes, cursor, cycle_words.len()) == cycle_words { - full_cycle_count += 1; - cursor += cycle_words.len() * 4; - } - - if full_cycle_count == 0 { - return None; - } - - let mut partial_cycle_word_count = 0usize; - while partial_cycle_word_count < cycle_words.len() { - let offset = cursor + partial_cycle_word_count * 4; - if read_u32_at(bytes, offset) == Some(cycle_words[partial_cycle_word_count]) { - partial_cycle_word_count += 1; - } else { - break; - } - } - - let trailer_offset = cursor + partial_cycle_word_count * 4; - let trailer_words = read_u32_window(bytes, trailer_offset, 12); - - Some(SmpSaveAnchorRunBlock { - profile_family: profile.profile_family.clone(), - cycle_start_offset, - cycle_hex_words: cycle_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - cycle_words, - full_cycle_count, - partial_cycle_word_count, - trailer_offset, - trailer_hex_words: trailer_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - trailer_words, - }) -} - -fn parse_runtime_trailer_block( - container_profile: Option<&SmpContainerProfile>, - runtime_anchor_cycle_block: Option<&SmpRuntimeAnchorCycleBlock>, -) -> Option { - let profile = container_profile?; - let anchor = runtime_anchor_cycle_block?; - let words = &anchor.trailer_words; - if words.len() < 16 { - return None; - } - - let trailer_family = match profile.profile_family.as_str() { - "rt3-classic-save-container-v1" - if words[..6] - == [ - 0x00020000, 0x00030000, 0x00010000, 0x00010000, 0x00010000, 0x00020000, - ] => - { - "rt3-classic-save-trailer-v1" - } - "rt3-classic-sandbox-container-v1" - if words[..6] - == [ - 0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, - ] => - { - "rt3-classic-sandbox-trailer-v1" - } - "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - if words[..6] - == [ - 0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, - ] => - { - "rt3-105-save-trailer-v1" - } - "rt3-105-sandbox-container-v1" - if words[..6] - == [ - 0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, - ] => - { - "rt3-105-sandbox-trailer-v1" - } - _ => "unknown", - } - .to_string(); - - let tag_chunk_id_u16 = (words[6] >> 16) as u16; - let length_high_u16 = (words[7] >> 16) as u16; - let selector_high_u16 = (words[8] >> 16) as u16; - let descriptor_high_u16 = (words[10] >> 16) as u16; - let tag_chunk_id_grounded_alignment = - classify_runtime_trailer_chunk_id_grounded_alignment(tag_chunk_id_u16).map(str::to_string); - - let mut trailer_evidence = vec![ - format!("container profile {}", profile.profile_family), - format!( - "prefix words {}", - words[..6] - .iter() - .map(|word| format!("0x{word:08x}")) - .collect::>() - .join(", ") - ), - format!("high-16 chunk id 0x{tag_chunk_id_u16:04x} from trailer word 6"), - format!("high-16 span 0x{length_high_u16:04x} from trailer word 7"), - format!("high-16 selector 0x{selector_high_u16:04x} from trailer word 8"), - format!("high-16 descriptor 0x{descriptor_high_u16:04x} from trailer word 10"), - ]; - if let Some(alignment) = &tag_chunk_id_grounded_alignment { - trailer_evidence.push(alignment.clone()); - } - - Some(SmpRuntimeTrailerBlock { - profile_family: profile.profile_family.clone(), - trailer_family, - trailer_evidence, - trailer_offset: anchor.trailer_offset, - prefix_words_0_to_5: words[..6].to_vec(), - prefix_hex_words_0_to_5: words[..6] - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - tag_word_6: words[6], - tag_word_6_hex: format!("0x{:08x}", words[6]), - tag_chunk_id_u16, - tag_chunk_id_hex: format!("0x{tag_chunk_id_u16:04x}"), - tag_chunk_id_grounded_alignment, - length_word_7: words[7], - length_word_7_hex: format!("0x{:08x}", words[7]), - length_high_u16, - length_high_hex: format!("0x{length_high_u16:04x}"), - selector_word_8: words[8], - selector_word_8_hex: format!("0x{:08x}", words[8]), - selector_high_u16, - selector_high_hex: format!("0x{selector_high_u16:04x}"), - layout_word_9: words[9], - layout_word_9_hex: format!("0x{:08x}", words[9]), - descriptor_word_10: words[10], - descriptor_word_10_hex: format!("0x{:08x}", words[10]), - descriptor_high_u16, - descriptor_high_hex: format!("0x{descriptor_high_u16:04x}"), - descriptor_word_11: words[11], - descriptor_word_11_hex: format!("0x{:08x}", words[11]), - counter_word_12: words[12], - counter_word_12_hex: format!("0x{:08x}", words[12]), - offset_word_13: words[13], - offset_word_13_hex: format!("0x{:08x}", words[13]), - span_word_14: words[14], - span_word_14_hex: format!("0x{:08x}", words[14]), - mode_word_15: words[15], - mode_word_15_hex: format!("0x{:08x}", words[15]), - words: words.to_vec(), - hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(), - }) -} - -fn classify_runtime_trailer_chunk_id_grounded_alignment( - tag_chunk_id_u16: u16, -) -> Option<&'static str> { - match tag_chunk_id_u16 { - 0x2ee1 => Some( - "High-16 chunk id 0x2ee1 matches the disassembly-grounded map-style bundle family already read by shell_setup_load_selected_profile_bundle_into_payload_record.", - ), - _ => None, - } -} - -fn parse_runtime_post_span_probe( - bytes: &[u8], - runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>, -) -> Option { - let trailer = runtime_trailer_block?; - let span_target_offset = trailer.trailer_offset + trailer.length_high_u16 as usize; - let next_nonzero_offset = find_next_nonzero_offset(bytes, span_target_offset); - let header_candidates = - collect_runtime_post_span_header_candidates(bytes, span_target_offset, 0x8000); - let next_aligned_candidate_offset = header_candidates.first().map(|candidate| candidate.offset); - let next_aligned_candidate_words = header_candidates - .first() - .map(|candidate| candidate.words.clone()) - .unwrap_or_default(); - let grounded_progress_hits = - find_grounded_progress_high16_hits(bytes, span_target_offset, 0x8000); - - Some(SmpRuntimePostSpanProbe { - profile_family: trailer.profile_family.clone(), - span_target_offset, - next_nonzero_offset, - next_aligned_candidate_offset, - next_aligned_candidate_hex_words: next_aligned_candidate_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - next_aligned_candidate_words, - header_candidates, - grounded_progress_hits, - }) -} - -fn parse_classic_rehydrate_profile_probe( - bytes: &[u8], - runtime_post_span_probe: Option<&SmpRuntimePostSpanProbe>, -) -> Option { - let post_span = runtime_post_span_probe?; - if post_span.profile_family != "rt3-classic-save-container-v1" { - return None; - } - - let progress_32dc_offset = - parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x32dc)?; - let progress_3714_offset = - parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x3714)?; - let progress_3715_offset = - parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x3715)?; - let packed_profile_offset = progress_3714_offset + 4; - let packed_profile_len = progress_3715_offset.checked_sub(packed_profile_offset)?; - if packed_profile_len != 0x108 { - return None; - } - - let ascii_runs = - collect_ascii_previews_in_range(bytes, packed_profile_offset, progress_3715_offset, 4); - let packed_profile_block = - parse_classic_packed_profile_block(bytes, packed_profile_offset, packed_profile_len)?; - - Some(SmpClassicRehydrateProfileProbe { - profile_family: post_span.profile_family.clone(), - progress_32dc_offset, - progress_3714_offset, - progress_3715_offset, - packed_profile_offset, - packed_profile_len, - packed_profile_len_hex: format!("0x{packed_profile_len:03x}"), - packed_profile_block, - ascii_runs, - }) -} - -fn parse_classic_packed_profile_block( - bytes: &[u8], - packed_profile_offset: usize, - packed_profile_len: usize, -) -> Option { - let block_end = packed_profile_offset.checked_add(packed_profile_len)?; - if block_end > bytes.len() || packed_profile_len != 0x108 { - return None; - } - - let leading_word_0 = read_u32_at(bytes, packed_profile_offset)?; - let trailing_zero_word_count_after_leading_word = (1..4) - .take_while(|index| { - read_u32_at(bytes, packed_profile_offset + (index * 4)).is_some_and(|word| word == 0) - }) - .count(); - let map_path_offset = 0x13; - let display_name_offset = 0x46; - let stable_nonzero_word_offsets = [0x00usize, 0x10, 0x78, 0x7c, 0x84, 0x88]; - let stable_nonzero_words = stable_nonzero_word_offsets - .iter() - .filter_map(|relative_offset| { - let value = read_u32_at(bytes, packed_profile_offset + relative_offset)?; - if value == 0 { - return None; - } - - Some(SmpPackedProfileWordLane { - relative_offset: *relative_offset, - relative_offset_hex: format!("0x{relative_offset:02x}"), - value, - value_hex: format!("0x{value:08x}"), - }) - }) - .collect::>(); - - Some(SmpClassicPackedProfileBlock { - relative_len: packed_profile_len, - relative_len_hex: format!("0x{packed_profile_len:03x}"), - leading_word_0, - leading_word_0_hex: format!("0x{leading_word_0:08x}"), - trailing_zero_word_count_after_leading_word, - map_path_offset, - map_path: read_c_string_in_range(bytes, packed_profile_offset + map_path_offset, block_end), - display_name_offset, - display_name: read_c_string_in_range( - bytes, - packed_profile_offset + display_name_offset, - block_end, - ), - profile_byte_0x77: bytes[packed_profile_offset + 0x77], - profile_byte_0x77_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x77]), - profile_byte_0x82: bytes[packed_profile_offset + 0x82], - profile_byte_0x82_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x82]), - profile_byte_0x97: bytes[packed_profile_offset + 0x97], - profile_byte_0x97_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x97]), - profile_byte_0xc5: bytes[packed_profile_offset + 0xc5], - profile_byte_0xc5_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0xc5]), - stable_nonzero_words, - }) -} - -fn parse_rt3_105_packed_profile_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - header_variant_probe: Option<&SmpHeaderVariantProbe>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - let profile_family = if container_profile.is_some_and(|profile| { - matches!( - profile.profile_family.as_str(), - "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ) - }) { - container_profile - .expect("checked above") - .profile_family - .clone() - } else if file_extension_hint == Some("gms") - && header_variant_probe.is_some_and(|probe| { - matches!( - probe.variant_family.as_str(), - "rt3-105-common-header-v1" - | "rt3-105-scenario-save-header-v1" - | "rt3-105-alt-save-header-v1" - | "rt3-map-header-family" - ) - }) - { - "rt3-105-save-analog-block-inferred".to_string() - } else { - return None; - }; - - if file_extension_hint != Some("gms") { - return None; - } - - let map_path_offset = find_c_string_with_suffix_in_range(bytes, 0x7000, 0x9000, ".gmp")?; - let packed_profile_offset = map_path_offset.checked_sub(0x10)?; - let packed_profile_len = 0x108usize; - let block_end = packed_profile_offset.checked_add(packed_profile_len)?; - if block_end > bytes.len() { - return None; - } - - let packed_profile_block = - parse_rt3_105_packed_profile_block(bytes, packed_profile_offset, packed_profile_len)?; - let ascii_runs = collect_ascii_previews_in_range(bytes, packed_profile_offset, block_end, 4); - - Some(SmpRt3105PackedProfileProbe { - profile_family, - packed_profile_offset, - packed_profile_len, - packed_profile_len_hex: format!("0x{packed_profile_len:03x}"), - packed_profile_block, - ascii_runs, - }) -} - -fn parse_rt3_105_post_span_bridge_probe( - runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>, - runtime_post_span_probe: Option<&SmpRuntimePostSpanProbe>, - rt3_105_packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>, -) -> Option { - let trailer = runtime_trailer_block?; - let post_span = runtime_post_span_probe?; - let packed_profile = rt3_105_packed_profile_probe?; - let supported = matches!( - trailer.profile_family.as_str(), - "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - | "rt3-105-save-analog-block-inferred" - ); - if !supported || trailer.profile_family != post_span.profile_family { - return None; - } - - let next_candidate_high_u16_words = post_span - .header_candidates - .first() - .map(|candidate| candidate.high_u16_words.clone()) - .unwrap_or_default(); - let next_candidate_high_hex_words = next_candidate_high_u16_words - .iter() - .map(|word| format!("0x{word:04x}")) - .collect::>(); - let next_candidate_offset = post_span.next_aligned_candidate_offset; - let next_candidate_delta_from_span_target = - next_candidate_offset.and_then(|offset| offset.checked_sub(post_span.span_target_offset)); - let packed_profile_delta_from_span_target = packed_profile - .packed_profile_offset - .checked_sub(post_span.span_target_offset)?; - let next_candidate_delta_from_packed_profile = next_candidate_offset - .map(|offset| offset as i64 - packed_profile.packed_profile_offset as i64); - - let mut bridge_evidence = vec![ - format!("profile family {}", trailer.profile_family), - format!("selector high {}", trailer.selector_high_hex), - format!("descriptor high {}", trailer.descriptor_high_hex), - format!( - "packed profile sits +0x{packed_profile_delta_from_span_target:x} from span target" - ), - ]; - if let Some(delta) = next_candidate_delta_from_span_target { - bridge_evidence.push(format!("next candidate sits +0x{delta:x} from span target")); - } - if let Some(delta) = next_candidate_delta_from_packed_profile { - bridge_evidence.push(format!( - "next candidate is {delta:+#x} relative to packed profile" - )); - } - - let bridge_family = match ( - trailer.selector_high_u16, - trailer.descriptor_high_u16, - next_candidate_high_u16_words.as_slice(), - ) { - (0x7110, 0x7801 | 0x7401, [0x6200, 0x0000, 0xfff7, 0x5515, ..]) => { - bridge_evidence.push(format!( - "selector/descriptor pair 0x7110 -> 0x{:04x}", - trailer.descriptor_high_u16 - )); - bridge_evidence.push( - "next candidate begins with high-16 lanes 0x6200/0x0000/0xfff7/0x5515" - .to_string(), - ); - "rt3-105-save-post-span-bridge-v1" - } - (0x54cd, 0x5901, [0x1500, 0x0100, 0x4100, 0x0200, ..]) => { - bridge_evidence.push("selector/descriptor pair 0x54cd -> 0x5901".to_string()); - bridge_evidence.push( - "next candidate begins with high-16 lanes 0x1500/0x0100/0x4100/0x0200" - .to_string(), - ); - "rt3-105-alt-save-post-span-bridge-v1" - } - (0x0001, 0x0186, [0x0186, 0x0006, 0x0006, 0x0001, ..]) => { - bridge_evidence.push("selector/descriptor pair 0x0001 -> 0x0186".to_string()); - bridge_evidence.push( - "next candidate remains in the local cycle neighborhood with 0x0186/0x0006/0x0006/0x0001" - .to_string(), - ); - "rt3-105-scenario-post-span-bridge-v1" - } - _ => "unknown", - } - .to_string(); - - Some(SmpRt3105PostSpanBridgeProbe { - profile_family: trailer.profile_family.clone(), - bridge_family, - bridge_evidence, - span_target_offset: post_span.span_target_offset, - next_candidate_offset, - next_candidate_delta_from_span_target, - packed_profile_offset: packed_profile.packed_profile_offset, - packed_profile_delta_from_span_target, - next_candidate_delta_from_packed_profile, - selector_high_u16: trailer.selector_high_u16, - selector_high_hex: trailer.selector_high_hex.clone(), - descriptor_high_u16: trailer.descriptor_high_u16, - descriptor_high_hex: trailer.descriptor_high_hex.clone(), - next_candidate_high_u16_words, - next_candidate_high_hex_words, - }) -} - -fn parse_rt3_105_save_bridge_payload_probe( - bytes: &[u8], - bridge_probe: Option<&SmpRt3105PostSpanBridgeProbe>, -) -> Option { - let bridge = bridge_probe?; - if bridge.bridge_family != "rt3-105-save-post-span-bridge-v1" { - return None; - } - - let primary_block_offset = bridge.next_candidate_offset?; - let primary_block_word_count = 8usize; - let primary_words = read_u32_window(bytes, primary_block_offset, primary_block_word_count); - if primary_words.len() < primary_block_word_count { - return None; - } - - let secondary_block_delta_from_primary = 0x1808usize; - let secondary_block_offset = primary_block_offset + secondary_block_delta_from_primary; - let secondary_block_end_offset = bridge.packed_profile_offset; - let secondary_block_len = secondary_block_end_offset.checked_sub(secondary_block_offset)?; - let secondary_preview_word_count = 32usize; - let secondary_words = - read_u32_window(bytes, secondary_block_offset, secondary_preview_word_count); - if secondary_words.len() < secondary_preview_word_count { - return None; - } - - let primary_signature_matches = primary_words - == [ - 0x62000000, 0x00000000, 0xfff70000, 0x55150000, 0x55550000, 0x00000000, 0xfff70000, - 0x54550000, - ]; - let secondary_prefix_matches = secondary_words.starts_with(&[ - 0x00050000, 0x00050005, 0xfff70000, 0x54540000, 0x545400f9, 0x00f900f9, 0x00f94008, - 0x00001555, - ]); - - let mut evidence = vec![ - "bridge family rt3-105-save-post-span-bridge-v1".to_string(), - format!("primary block offset 0x{primary_block_offset:08x}"), - format!("secondary block offset 0x{secondary_block_offset:08x}"), - format!("secondary block delta from primary 0x{secondary_block_delta_from_primary:x}"), - format!("secondary block end offset 0x{secondary_block_end_offset:08x}"), - format!("secondary block span 0x{secondary_block_len:x} bytes"), - ]; - if primary_signature_matches { - evidence.push( - "primary 8-word bridge block matches the observed 0x6200/0xfff7/0x5515/0x5555 spine" - .to_string(), - ); - } - if secondary_prefix_matches { - evidence.push( - "secondary preview matches the observed 0x0005/0xfff7/0x5454 dense block prefix" - .to_string(), - ); - } - - Some(SmpRt3105SaveBridgePayloadProbe { - profile_family: bridge.profile_family.clone(), - bridge_family: bridge.bridge_family.clone(), - primary_block_offset, - primary_block_len: primary_block_word_count * 4, - primary_block_len_hex: format!("0x{:02x}", primary_block_word_count * 4), - primary_hex_words: primary_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - primary_words, - secondary_block_offset, - secondary_block_delta_from_primary, - secondary_block_delta_from_primary_hex: format!("0x{secondary_block_delta_from_primary:x}"), - secondary_block_end_offset, - secondary_block_len, - secondary_block_len_hex: format!("0x{secondary_block_len:x}"), - secondary_preview_word_count, - secondary_hex_words: secondary_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - secondary_words, - evidence, - }) -} - -fn parse_save_world_selection_context_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - if file_extension_hint != Some("gms") { - return None; - } - let profile = container_profile?; - let supported = matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ); - if !supported { - return None; - } - - for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) { - let payload_offset = chunk_tag_offset + 4; - let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?; - if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) { - continue; - } - let selected_company_id_offset = - payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET; - let selected_chairman_profile_id_offset = - payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET; - let chairman_slot_selector_offset = - payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET; - let campaign_override_flag_offset = - payload_offset + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET; - let chairman_role_gate_offset = - payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET; - let selected_company_id = read_u32_at(bytes, selected_company_id_offset)?; - let selected_chairman_profile_id = read_u32_at(bytes, selected_chairman_profile_id_offset)?; - let chairman_slot_selectors = bytes - .get( - chairman_slot_selector_offset - ..chairman_slot_selector_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT, - )? - .to_vec(); - let campaign_override_flag = *bytes.get(campaign_override_flag_offset)?; - let chairman_role_gate_bytes = (0..RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT) - .map(|slot_index| { - bytes - .get( - chairman_role_gate_offset - + slot_index * RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE, - ) - .copied() - }) - .collect::>>()?; - return Some(SmpSaveWorldSelectionContextProbe { - profile_family: profile.profile_family.clone(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-selected-company-and-chairman-context".to_string(), - chunk_tag_offset, - payload_offset, - payload_len: RT3_SAVE_WORLD_BLOCK_LEN, - payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), - selected_company_id_offset, - selected_company_id, - selected_company_id_hex: format!("0x{selected_company_id:08x}"), - selected_chairman_profile_id_offset, - selected_chairman_profile_id, - selected_chairman_profile_id_hex: format!("0x{selected_chairman_profile_id:08x}"), - chairman_slot_selector_offset, - chairman_slot_selectors, - campaign_override_flag_offset, - campaign_override_flag, - campaign_override_flag_hex: format!("0x{campaign_override_flag:02x}"), - chairman_role_gate_offset, - chairman_role_gate_bytes, - evidence: vec![ - format!( - "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" - ), - format!( - "next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span" - ), - format!( - "selected company id comes from payload +0x{:x} ([world+0x21])", - RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET - ), - format!( - "selected chairman profile id comes from payload +0x{:x} ([world+0x25])", - RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET - ), - format!( - "16 chairman slot selector bytes come from payload +0x{:x} ([world+0x87])", - RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET - ), - format!( - "campaign override flag comes from payload +0x{:x} ([world+0xc5])", - RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET - ), - format!( - "chairman role-gate bytes come from payload +0x{:x} + slot*0x{:x} ([world+0x0bc3+slot*9])", - RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET, - RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE - ), - ], - }); - } - - None -} - -fn parse_save_world_issue_37_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - if file_extension_hint != Some("gms") { - return None; - } - let profile = container_profile?; - let supported = matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ); - if !supported { - return None; - } - - for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) { - let payload_offset = chunk_tag_offset + 4; - let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?; - if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) { - continue; - } - let issue_value_lane = build_save_dword_candidate( - bytes, - payload_offset, - "issue_0x37_value", - RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET, - )?; - let issue_37_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET, - )?; - let issue_38_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 1, - )?; - let issue_39_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 2, - )?; - let issue_3a_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 3, - )?; - let multiplier_lane = build_save_dword_candidate( - bytes, - payload_offset, - "issue_0x37_multiplier", - RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET, - )?; - let issue_opinion_base_terms_raw_i32 = build_save_i32_term_strip( - bytes, - payload_offset, - RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_BASE_TERMS_OFFSET, - RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT, - )?; - return Some(SmpSaveWorldIssue37Probe { - profile_family: profile.profile_family.clone(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-save-world-issue-0x37".to_string(), - chunk_tag_offset, - payload_offset, - payload_len: RT3_SAVE_WORLD_BLOCK_LEN, - payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), - issue_37_raw_u8, - issue_37_raw_hex: format!("0x{issue_37_raw_u8:02x}"), - issue_38_raw_u8, - issue_38_raw_hex: format!("0x{issue_38_raw_u8:02x}"), - issue_39_raw_u8, - issue_39_raw_hex: format!("0x{issue_39_raw_u8:02x}"), - issue_3a_raw_u8, - issue_3a_raw_hex: format!("0x{issue_3a_raw_u8:02x}"), - issue_value_lane, - multiplier_lane, - issue_opinion_base_terms_raw_i32, - evidence: vec![ - format!( - "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" - ), - format!( - "next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span" - ), - format!( - "issue value lane uses payload +0x{:x} ([world+0x2d]); atlas notes tie 0x004339b0 to the clamped 0..4 issue-0x37 setter there", - RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET - ), - format!( - "multiplier lane uses payload +0x{:x} ([world+0x29]); atlas notes tie 0x004339b0 to one companion scalar at that lane before company share-price refresh", - RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET - ), - format!( - "the adjacent byte strip at payload +0x{:x}..+0x{:x} carries raw issue slots 0x37..0x3a as {:02x} {:02x} {:02x} {:02x}", - RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET, - RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 3, - issue_37_raw_u8, - issue_38_raw_u8, - issue_39_raw_u8, - issue_3a_raw_u8 - ), - ], - }); - } - - None -} - -fn parse_save_world_finance_neighborhood_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - if file_extension_hint != Some("gms") { - return None; - } - let profile = container_profile?; - let supported = matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ); - if !supported { - return None; - } - - for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) { - let payload_offset = chunk_tag_offset + 4; - let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?; - if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) { - continue; - } - - let current_calendar_tuple_word_lane = build_save_dword_candidate( - bytes, - payload_offset, - "current_calendar_tuple_word", - RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, - )?; - let packed_year_word_raw_u16 = read_u16_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, - )?; - let partial_year_progress_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET + 2, - )?; - let current_calendar_tuple_word_2_lane = build_save_dword_candidate( - bytes, - payload_offset, - "current_calendar_tuple_word_2", - RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET, - )?; - let absolute_counter_lane = build_save_dword_candidate( - bytes, - payload_offset, - "absolute_calendar_counter", - RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET, - )?; - let absolute_counter_mirror_lane = build_save_dword_candidate( - bytes, - payload_offset, - "absolute_calendar_counter_mirror", - RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET, - )?; - let stock_policy_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET, - )?; - let bond_policy_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET, - )?; - let bankruptcy_policy_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, - )?; - let dividend_policy_raw_u8 = read_u8_at( - bytes, - payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET, - )?; - let building_density_growth_setting_lane = build_save_dword_candidate( - bytes, - payload_offset, - "building_density_growth_setting", - RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET, - )?; - let dword_candidates = - build_save_world_finance_neighborhood_candidates(bytes, payload_offset)?; - - return Some(SmpSaveWorldFinanceNeighborhoodProbe { - profile_family: profile.profile_family.clone(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-save-world-finance-neighborhood".to_string(), - chunk_tag_offset, - payload_offset, - payload_len: RT3_SAVE_WORLD_BLOCK_LEN, - payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), - packed_year_word_raw_u16, - packed_year_word_raw_hex: format!("0x{packed_year_word_raw_u16:04x}"), - partial_year_progress_raw_u8, - partial_year_progress_raw_hex: format!("0x{partial_year_progress_raw_u8:02x}"), - current_calendar_tuple_word_lane, - current_calendar_tuple_word_2_lane, - absolute_counter_lane, - absolute_counter_mirror_lane, - stock_policy_raw_u8, - stock_policy_raw_hex: format!("0x{stock_policy_raw_u8:02x}"), - bond_policy_raw_u8, - bond_policy_raw_hex: format!("0x{bond_policy_raw_u8:02x}"), - bankruptcy_policy_raw_u8, - bankruptcy_policy_raw_hex: format!("0x{bankruptcy_policy_raw_u8:02x}"), - dividend_policy_raw_u8, - dividend_policy_raw_hex: format!("0x{dividend_policy_raw_u8:02x}"), - building_density_growth_setting_lane, - dword_candidates, - evidence: vec![ - format!( - "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" - ), - format!( - "next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span" - ), - format!( - "payload +0x{:x}/+0x{:x}/+0x{:x} carry the saved world calendar tuple and absolute counter lanes that later company stock-issue cooldown readers compare against", - RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET, - RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET, - RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET - ), - format!( - "payload +0x{:x}/+0x{:x}/+0x{:x}/+0x{:x} carry the stock, bond, bankruptcy, and dividend finance-policy bytes mirrored from scenario offsets 0x4a87/0x4a8b/0x4a8f/0x4a93", - RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET, - RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET, - RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, - RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET - ), - format!( - "payload +0x{:x} carries the fixed-world building-density growth setting mirrored from `[world+0x4c7c]`, which the annual repurchase and dividend policy helpers both read directly", - RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET - ), - "finance-neighborhood candidates cover the fixed dword strip around the grounded world calendar tuple, absolute-counter, selection-context, and issue-0x37 lanes so broader finance reader closure can build on one rehosted owner surface.".to_string(), - ], - }); - } - - None -} - -fn build_save_world_finance_neighborhood_candidates( - bytes: &[u8], - payload_offset: usize, -) -> Option> { - (0..RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS) - .map(|index| { - let relative_offset = - RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET + index * 4; - let label = RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_CANDIDATE_FIELDS - .iter() - .find(|(_, named_offset)| *named_offset == relative_offset) - .map(|(name, _)| (*name).to_string()) - .unwrap_or_else(|| format!("finance_neighborhood_word_{:02}", index + 1)); - build_save_dword_candidate(bytes, payload_offset, &label, relative_offset) - }) - .collect() -} - -fn parse_save_world_economic_tuning_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - if file_extension_hint != Some("gms") { - return None; - } - let profile = container_profile?; - let supported = matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ); - if !supported { - return None; - } - - for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) { - let payload_offset = chunk_tag_offset + 4; - let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?; - if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) { - continue; - } - let mirror_lane = build_save_dword_candidate( - bytes, - payload_offset, - "economic_tuning_mirror_lane_0", - RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET, - )?; - let tuning_lanes = RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS - .iter() - .enumerate() - .map(|(lane_index, relative_offset)| { - build_save_dword_candidate( - bytes, - payload_offset, - &format!("economic_tuning_lane_{lane_index}"), - *relative_offset, - ) - }) - .collect::>>()?; - return Some(SmpSaveWorldEconomicTuningProbe { - profile_family: profile.profile_family.clone(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-save-world-economic-tuning".to_string(), - chunk_tag_offset, - payload_offset, - payload_len: RT3_SAVE_WORLD_BLOCK_LEN, - payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), - mirror_lane, - tuning_lanes, - evidence: vec![ - format!( - "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" - ), - format!( - "next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span" - ), - format!( - "mirror lane uses payload +0x{:x} ([world+0x0bde])", - RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET - ), - format!( - "primary tuning lanes use payload offsets {} matching the documented [world+0x0be2..+0x0bf6] float block", - RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS - .iter() - .map(|offset| format!("0x{offset:x}")) - .collect::>() - .join(", ") - ), - "Current atlas evidence keeps this fixed six-float world tuning band separate from the issue-0x37 investor-confidence lane." - .to_string(), - ], - }); - } - - None -} - -fn parse_save_company_collection_header_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - parse_save_tagged_collection_header_probe( - bytes, - file_extension_hint, - container_profile, - 0x000061a9, - 0x000061aa, - 0x000061ab, - "save-company-tagged-header-counts", - "scenario-save-company-header-counts", - |header| { - header.direct_collection_flag == 1 - && header.live_id_bound >= 1 - && header.live_id_bound <= 0x20 - && header.live_record_count <= header.live_id_bound - && header.direct_record_stride >= 0x1000 - }, - vec![ - "save-side company collection uses tagged header family 0x61a9/0x61aa/0x61ab".to_string(), - "package-save per-company callback is currently grounded as a no-op stub, so this probe only claims header-level collection counts, not per-company payload".to_string(), - ], - ) -} - -fn parse_save_chairman_profile_collection_header_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - parse_save_tagged_collection_header_probe( - bytes, - file_extension_hint, - container_profile, - 0x00005209, - 0x0000520a, - 0x0000520b, - "save-chairman-profile-tagged-header-counts", - "scenario-save-chairman-profile-header-counts", - |header| { - header.direct_collection_flag == 1 - && header.live_id_bound >= 1 - && header.live_id_bound <= 0x20 - && header.live_record_count <= header.live_id_bound - && header.direct_record_stride >= 0x800 - && header.direct_record_stride <= 0x2000 - }, - vec![ - "save-side chairman/profile collection uses tagged header family 0x5209/0x520a/0x520b".to_string(), - "the direct-record chairman/profile family is the large-stride tagged collection with embedded name and biography payload, not the smaller train-side 0x5209 family".to_string(), - ], - ) -} - -fn parse_save_train_collection_header_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - parse_save_tagged_collection_header_probe( - bytes, - file_extension_hint, - container_profile, - 0x00005209, - 0x0000520a, - 0x0000520b, - "save-train-tagged-header-counts", - "scenario-save-train-header-counts", - |header| { - header.direct_collection_flag == 1 - && header.direct_record_stride >= 0x100 - && header.direct_record_stride <= 0x400 - && header.live_id_bound >= 0x10 - && header.live_id_bound <= 0x100 - && header.live_record_count >= 1 - && header.live_record_count <= header.live_id_bound - }, - vec![ - "save-side live train collection shares tagged header family 0x5209/0x520a/0x520b with other indexed direct-record bundles".to_string(), - "the grounded train-side candidate is the smaller direct-record family with stride 0x1d5 whose metadata payload carries Train N labels, distinct from the larger chairman/profile family and the non-direct region family".to_string(), - ], - ) -} - -fn parse_save_train_collection_directory_probe( - bytes: &[u8], - header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { - let header_probe = header_probe?; - if header_probe.source_kind != "save-train-tagged-header-counts" { - return None; - } - let metadata_payload = - bytes.get(header_probe.metadata_tag_offset + 4..header_probe.records_tag_offset)?; - let directory_root_byte_offset = - SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX.checked_mul(4)?; - let live_record_count = header_probe.live_record_count as usize; - let directory_len_dwords = - live_record_count.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT)?; - let directory_len_bytes = directory_len_dwords.checked_mul(4)?; - let directory_bytes = metadata_payload.get( - directory_root_byte_offset..directory_root_byte_offset.checked_add(directory_len_bytes)?, - )?; - let mut entries = Vec::with_capacity(live_record_count); - for index in 0..live_record_count { - let entry_offset = - index.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT * 4)?; - let payload_relative_offset = read_u32_at(directory_bytes, entry_offset)?; - let previous_live_entry_id = read_u32_at(directory_bytes, entry_offset + 4)?; - let next_live_entry_id = read_u32_at(directory_bytes, entry_offset + 8)?; - entries.push(SmpSaveTrainCollectionDirectoryEntryProbe { - live_entry_id: (index + 1) as u32, - payload_relative_offset, - payload_relative_offset_hex: format!("0x{payload_relative_offset:08x}"), - payload_absolute_offset: header_probe - .metadata_tag_offset - .checked_add(4)? - .checked_add(payload_relative_offset as usize)?, - previous_live_entry_id, - previous_live_entry_id_hex: format!("0x{previous_live_entry_id:08x}"), - next_live_entry_id, - next_live_entry_id_hex: format!("0x{next_live_entry_id:08x}"), - }); - } - let chain_head_live_entry_id = entries - .iter() - .find(|entry| entry.previous_live_entry_id == 0) - .map(|entry| entry.live_entry_id); - let chain_tail_live_entry_id = entries - .iter() - .find(|entry| entry.next_live_entry_id == 0) - .map(|entry| entry.live_entry_id); - let monotonic_offsets = entries - .windows(2) - .all(|window| window[0].payload_relative_offset < window[1].payload_relative_offset); - Some(SmpSaveTrainCollectionDirectoryProbe { - profile_family: header_probe.profile_family.clone(), - source_kind: "save-train-live-directory".to_string(), - semantic_family: "scenario-save-train-live-directory".to_string(), - metadata_tag_offset: header_probe.metadata_tag_offset, - records_tag_offset: header_probe.records_tag_offset, - close_tag_offset: header_probe.close_tag_offset, - directory_root_dword_index: SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX, - directory_entry_dword_count: SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT, - live_record_count: header_probe.live_record_count, - live_id_bound: header_probe.live_id_bound, - chain_head_live_entry_id, - chain_tail_live_entry_id, - entries, - evidence: vec![ - "save-side train metadata payload exposes a live-entry directory immediately after the first 16 dwords, with payload-relative offsets pointing into the later records span".to_string(), - format!( - "train live directory decodes {} triplets of (payload_relative_offset, prev_live_entry_id, next_live_entry_id) from metadata dword index {}", - header_probe.live_record_count, - SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX - ), - format!( - "decoded directory preserves a head/tail chain {:?}->{:?} and monotonic payload offsets={monotonic_offsets}", - chain_head_live_entry_id, chain_tail_live_entry_id - ), - ], - }) -} - -fn parse_save_region_collection_header_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - let probe = parse_save_tagged_collection_header_probe( - bytes, - file_extension_hint, - container_profile, - 0x00005209, - 0x0000520a, - 0x0000520b, - "save-region-tagged-header-counts", - "scenario-save-region-header-counts", - |header| { - header.direct_collection_flag == 0 - && header.direct_record_stride == 0x06 - && header.live_id_bound >= 0x80 - && header.live_id_bound <= 0x200 - && header.live_record_count >= 0x80 - && header.live_record_count <= header.live_id_bound - }, - vec![ - "save-side live region collection shares tagged header family 0x5209/0x520a/0x520b with trains and chairman profiles, but uses the larger non-direct indexed family".to_string(), - "the grounded region-side candidate is the non-direct 0x5209 family with live_id_bound/count in the 0x96/0x91 range and Marker09-style default stems in the records span, distinct from the smaller direct train family".to_string(), - ], - )?; - let records_preview = bytes - .get(probe.records_tag_offset + 4..probe.close_tag_offset) - .unwrap_or(&[]); - records_preview - .windows("Marker09".len()) - .any(|window| window == b"Marker09") - .then_some(probe) -} - -fn parse_save_region_record_triplet_probe( - bytes: &[u8], - header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { - let header_probe = header_probe?; - if header_probe.source_kind != "save-region-tagged-header-counts" { - return None; - } - let records_payload = - bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?; - let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG); - let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG); - let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG); - let record_count = header_probe.live_record_count as usize; - if name_offsets.len() != record_count - || policy_offsets.len() != record_count - || profile_offsets.len() != record_count - { - return None; - } - let region_payload_start_offsets = bytes - .get(header_probe.metadata_tag_offset + 4..header_probe.records_tag_offset) - .and_then(|metadata_payload| { - let directory_root_byte_offset = - SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX.checked_mul(4)?; - let directory_len_dwords = - record_count.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT)?; - let directory_len_bytes = directory_len_dwords.checked_mul(4)?; - let directory_bytes = metadata_payload.get( - directory_root_byte_offset - ..directory_root_byte_offset.checked_add(directory_len_bytes)?, - )?; - let records_payload_absolute_offset = header_probe.records_tag_offset.checked_add(4)?; - (0..record_count) - .map(|index| { - let entry_offset = index - .checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT * 4)?; - let payload_relative_offset = - read_u32_at(directory_bytes, entry_offset)? as usize; - let payload_absolute_offset = header_probe - .metadata_tag_offset - .checked_add(4)? - .checked_add(payload_relative_offset)?; - let payload_start = - payload_absolute_offset.checked_sub(records_payload_absolute_offset)?; - (payload_start <= records_payload.len()).then_some(payload_start) - }) - .collect::>>() - }) - .unwrap_or_else(|| name_offsets.clone()); - let mut entries = Vec::with_capacity(record_count); - for index in 0..record_count { - let record_payload_relative_offset = region_payload_start_offsets[index]; - let name_tag_relative_offset = name_offsets[index]; - let policy_tag_relative_offset = policy_offsets[index]; - let profile_tag_relative_offset = profile_offsets[index]; - let next_record_relative_offset = name_offsets - .get(index + 1) - .copied() - .unwrap_or(records_payload.len()); - if record_payload_relative_offset > name_tag_relative_offset { - return None; - } - if !(name_tag_relative_offset < policy_tag_relative_offset - && policy_tag_relative_offset < profile_tag_relative_offset - && profile_tag_relative_offset < next_record_relative_offset) - { - return None; - } - let pre_name_prefix = - records_payload.get(record_payload_relative_offset..name_tag_relative_offset)?; - let pre_name_prefix_dword_candidates = build_region_record_prefix_dword_candidates( - record_payload_relative_offset, - pre_name_prefix, - ); - let name_payload = - records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?; - let name = parse_save_len_prefixed_ascii_name(name_payload)?; - let policy_chunk_len = - profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?; - if policy_chunk_len != 0x1a { - return None; - } - let policy_payload = - records_payload.get(policy_tag_relative_offset + 4..profile_tag_relative_offset)?; - let policy_leading_f32_0 = f32::from_bits(read_u32_at(policy_payload, 0)?); - let policy_leading_f32_1 = f32::from_bits(read_u32_at(policy_payload, 4)?); - let policy_leading_f32_2 = f32::from_bits(read_u32_at(policy_payload, 8)?); - let mut policy_reserved_dwords = Vec::with_capacity(3); - let mut policy_reserved_dword_candidates = Vec::with_capacity(3); - for dword_index in 0..3 { - let reserved_relative_offset = 12 + dword_index * 4; - let raw_u32 = read_u32_at(policy_payload, reserved_relative_offset)?; - policy_reserved_dwords.push(raw_u32); - let relative_offset = record_payload_relative_offset - + policy_tag_relative_offset - + 4 - + reserved_relative_offset; - policy_reserved_dword_candidates.push(SmpSaveDwordCandidate { - label: format!("policy_reserved_word_{}", dword_index + 1), - relative_offset, - relative_offset_hex: format!("0x{relative_offset:x}"), - raw_u32, - raw_u32_hex: format!("0x{raw_u32:08x}"), - value_i32: raw_u32 as i32, - value_f32: f32::from_bits(raw_u32), - }); - } - let policy_trailing_word = read_u16_at(policy_payload, 24)?; - let profile_chunk_len = - next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?; - let profile_payload = - records_payload.get(profile_tag_relative_offset + 4..next_record_relative_offset)?; - let profile_collection = parse_save_region_profile_collection_probe(profile_payload); - entries.push(SmpSaveRegionRecordTripletEntryProbe { - record_index: index, - name, - record_payload_relative_offset, - record_payload_relative_offset_hex: format!("0x{record_payload_relative_offset:x}"), - name_tag_relative_offset, - policy_tag_relative_offset, - profile_tag_relative_offset, - pre_name_prefix_len: pre_name_prefix.len(), - pre_name_prefix_hex_bytes: pre_name_prefix - .iter() - .map(|byte| format!("0x{byte:02x}")) - .collect(), - pre_name_prefix_dword_candidates, - policy_chunk_len, - profile_chunk_len, - policy_leading_f32_0, - policy_leading_f32_1, - policy_leading_f32_2, - policy_reserved_dwords, - policy_reserved_dword_candidates, - policy_trailing_word, - policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"), - profile_collection, - }); - } - let zero_trailing_padding_record_count = entries - .iter() - .filter(|entry| { - entry - .profile_collection - .as_ref() - .is_some_and(|collection| collection.trailing_padding_len == 0) - }) - .count(); - let records_with_nonzero_pre_name_prefix = entries - .iter() - .filter(|entry| entry.pre_name_prefix_len != 0) - .count(); - let records_with_prefix_dword_candidates = entries - .iter() - .filter(|entry| !entry.pre_name_prefix_dword_candidates.is_empty()) - .count(); - let records_with_any_nonzero_policy_reserved_dword = entries - .iter() - .filter(|entry| { - entry - .policy_reserved_dwords - .iter() - .any(|raw_u32| *raw_u32 != 0) - }) - .count(); - let policy_reserved_nonzero_counts = (0..3) - .map(|dword_index| { - entries - .iter() - .filter(|entry| entry.policy_reserved_dwords[dword_index] != 0) - .count() - }) - .collect::>(); - let unique_nonzero_policy_reserved_triplets = entries - .iter() - .filter_map(|entry| { - let triplet = [ - entry.policy_reserved_dwords[0], - entry.policy_reserved_dwords[1], - entry.policy_reserved_dwords[2], - ]; - triplet - .iter() - .any(|raw_u32| *raw_u32 != 0) - .then_some(triplet) - }) - .collect::>() - .into_iter() - .collect::>(); - let unique_pre_name_prefix_lens = entries - .iter() - .map(|entry| entry.pre_name_prefix_len) - .collect::>() - .into_iter() - .collect::>(); - Some(SmpSaveRegionRecordTripletProbe { - profile_family: header_probe.profile_family.clone(), - source_kind: "save-region-record-triplets".to_string(), - semantic_family: "scenario-save-region-record-triplets".to_string(), - records_tag_offset: header_probe.records_tag_offset, - close_tag_offset: header_probe.close_tag_offset, - record_count, - entries, - evidence: vec![ - "save-side region records in the non-direct Marker09 family are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the records span".to_string(), - format!( - "decoded {} region record triplets with one len-prefixed name chunk, one fixed policy chunk, and one trailing profile payload chunk per record", - record_count - ), - "each fixed 0x55f2 policy chunk currently decodes as three leading f32 lanes, three reserved dwords, and one trailing u16 word".to_string(), - "the trailing 0x55f3 payload also carries an embedded direct profile collection with fixed 0x22-byte rows on grounded saves".to_string(), - format!( - "live-entry directory now also grounds the actual 0x520a payload starts: {} of {} records currently have nonzero bytes before the first 0x55f1 tag, with unique pre-name prefix lengths {:?}", - records_with_nonzero_pre_name_prefix, - record_count, - unique_pre_name_prefix_lens - ), - format!( - "structured pre-name prefix dword candidates are currently present on {} of {} decoded region records", - records_with_prefix_dword_candidates, - record_count - ), - format!( - "fixed 0x55f2 policy reserved dwords are nonzero on {} of {} decoded region records, with per-word nonzero counts {:?} and unique nonzero triplets {:?}", - records_with_any_nonzero_policy_reserved_dword, - record_count, - policy_reserved_nonzero_counts, - unique_nonzero_policy_reserved_triplets - ), - format!( - "on grounded saves the 0x55f3 payload is fully consumed by that embedded profile collection: all {} decoded records currently have zero trailing padding beyond the direct profile rows", - zero_trailing_padding_record_count - ), - ], - }) -} - -fn build_region_record_prefix_dword_candidates( - record_payload_relative_offset: usize, - prefix_bytes: &[u8], -) -> Vec { - prefix_bytes - .chunks_exact(4) - .enumerate() - .map(|(index, chunk)| { - let raw_u32 = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - let relative_offset = record_payload_relative_offset + index * 4; - SmpSaveDwordCandidate { - label: format!("pre_name_prefix_word_{}", index + 1), - relative_offset, - relative_offset_hex: format!("0x{relative_offset:x}"), - raw_u32, - raw_u32_hex: format!("0x{raw_u32:08x}"), - value_i32: raw_u32 as i32, - value_f32: f32::from_bits(raw_u32), - } - }) - .collect() -} - -fn parse_save_region_queued_notice_record_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - region_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { - if file_extension_hint != Some("gms") { - return None; - } - let profile = container_profile?; - let max_region_id = region_header_probe - .map(|probe| probe.live_id_bound) - .unwrap_or(0x1000); - let entries = find_u32_le_offsets(bytes, SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED) - .into_iter() - .filter_map(|payload_seed_offset| { - let node_base_offset = payload_seed_offset.checked_sub(4)?; - let _node_bytes = bytes - .get(node_base_offset..node_base_offset + SAVE_REGION_QUEUED_NOTICE_NODE_LEN)?; - let next_link_raw = read_u32_at(bytes, node_base_offset)?; - let kind = read_u32_at(bytes, node_base_offset + 8)?; - let promotion_latch_dword = read_u32_at(bytes, node_base_offset + 12)?; - let region_id = read_u32_at(bytes, node_base_offset + 16)?; - let amount = read_u32_at(bytes, node_base_offset + 20)?; - let trailing_sentinel_i32_0 = read_i32_at(bytes, node_base_offset + 24)?; - let trailing_sentinel_i32_1 = read_i32_at(bytes, node_base_offset + 28)?; - if !(kind == SAVE_REGION_QUEUED_NOTICE_NODE_KIND - && promotion_latch_dword == 0 - && region_id >= 1 - && region_id <= max_region_id - && amount > 0 - && trailing_sentinel_i32_0 == -1 - && trailing_sentinel_i32_1 == -1) - { - return None; - } - Some(SmpSaveRegionQueuedNoticeRecordEntryProbe { - node_base_offset, - payload_seed_offset, - next_link_raw, - next_link_raw_hex: format!("0x{next_link_raw:08x}"), - payload_seed_dword: SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED, - payload_seed_dword_hex: format!("0x{SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED:08x}"), - kind, - kind_hex: format!("0x{kind:08x}"), - promotion_latch_dword, - promotion_latch_dword_hex: format!("0x{promotion_latch_dword:08x}"), - region_id, - region_id_hex: format!("0x{region_id:08x}"), - amount, - amount_hex: format!("0x{amount:08x}"), - trailing_sentinel_i32_0, - trailing_sentinel_i32_0_hex: format!("0x{:08x}", trailing_sentinel_i32_0 as u32), - trailing_sentinel_i32_1, - trailing_sentinel_i32_1_hex: format!("0x{:08x}", trailing_sentinel_i32_1 as u32), - }) - }) - .collect::>(); - if entries.is_empty() { - return None; - } - Some(SmpSaveRegionQueuedNoticeRecordProbe { - profile_family: profile.profile_family.clone(), - source_kind: "save-region-queued-notice-records".to_string(), - semantic_family: "scenario-save-region-queued-notice-records".to_string(), - payload_seed_dword: SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED, - payload_seed_dword_hex: format!("0x{SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED:08x}"), - entries, - evidence: vec![ - "save-side scan searches for the grounded region queued-notice payload seed 0x005c87a8 and validates the full 0x20-byte node shape from the atlas-backed queue owner".to_string(), - "accepted nodes require kind=7, promotion-latch dword=0, a bounded live region id, a positive amount, and trailing sentinel dwords -1/-1".to_string(), - ], - }) -} - -fn parse_save_region_fixed_row_run_candidate_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - region_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { - if file_extension_hint != Some("gms") { - return None; - } - let profile = container_profile?; - let region_header_probe = region_header_probe?; - let target_row_count = region_header_probe.live_record_count as usize; - if target_row_count == 0 { - return None; - } - let scan_end_offset = region_header_probe.metadata_tag_offset; - let row_span_len = target_row_count.checked_mul(SAVE_REGION_FIXED_ROW_STRIDE)?; - let scan_bytes = bytes.get(..scan_end_offset)?; - let mut candidates = find_u32_le_offsets(scan_bytes, region_header_probe.live_record_count) - .into_iter() - .filter_map(|count_offset| { - let rows_offset = count_offset.checked_add(4)?; - let rows_end_offset = rows_offset.checked_add(row_span_len)?; - if rows_end_offset > scan_end_offset { - return None; - } - let rows_bytes = bytes.get(rows_offset..rows_end_offset)?; - let mut dword_lane_summaries = - Vec::with_capacity(SAVE_REGION_FIXED_ROW_DWORD_LANE_COUNT); - let mut best_probable_density_lane = None::<(usize, usize)>; - for lane_index in 0..SAVE_REGION_FIXED_ROW_DWORD_LANE_COUNT { - let relative_offset = lane_index * 4; - let mut zero_count = 0usize; - let mut nonzero_count = 0usize; - let mut probable_normal_f32_count = 0usize; - let mut small_unsigned_count = 0usize; - let mut distinct_values = BTreeSet::new(); - let mut sample_values_hex = Vec::new(); - for row_index in 0..target_row_count { - let row_offset = row_index * SAVE_REGION_FIXED_ROW_STRIDE + relative_offset; - let raw_u32 = read_u32_at(rows_bytes, row_offset)?; - if raw_u32 == 0 { - zero_count += 1; - } else { - nonzero_count += 1; - } - if probable_normal_f32_string(raw_u32).is_some() { - probable_normal_f32_count += 1; - } - if raw_u32 <= 1024 { - small_unsigned_count += 1; - } - if distinct_values.insert(raw_u32) && sample_values_hex.len() < 6 { - sample_values_hex.push(format!("0x{raw_u32:08x}")); - } - } - if best_probable_density_lane - .is_none_or(|(_, best_count)| probable_normal_f32_count > best_count) - { - best_probable_density_lane = Some((relative_offset, probable_normal_f32_count)); - } - dword_lane_summaries.push(SmpSaveFixedRowRunDwordLaneSummary { - relative_offset, - relative_offset_hex: format!("0x{relative_offset:x}"), - zero_count, - nonzero_count, - distinct_value_count: distinct_values.len(), - probable_normal_f32_count, - small_unsigned_count, - sample_values_hex, - }); - } - let mut trailing_values = BTreeSet::new(); - let mut trailing_byte_zero_count = 0usize; - let mut trailing_byte_nonzero_count = 0usize; - let mut trailing_byte_sample_values_hex = Vec::new(); - for row_index in 0..target_row_count { - let value = *rows_bytes.get(row_index * SAVE_REGION_FIXED_ROW_STRIDE + 0x28)?; - if value == 0 { - trailing_byte_zero_count += 1; - } else { - trailing_byte_nonzero_count += 1; - } - if trailing_values.insert(value) && trailing_byte_sample_values_hex.len() < 8 { - trailing_byte_sample_values_hex.push(format!("0x{value:02x}")); - } - } - let shape_signature = build_save_region_fixed_row_run_candidate_shape_signature( - &dword_lane_summaries, - trailing_byte_zero_count, - trailing_values.len(), - target_row_count, - ); - let shape_family_signature = - build_save_region_fixed_row_run_candidate_shape_family_signature( - &dword_lane_summaries, - trailing_byte_zero_count, - trailing_values.len(), - target_row_count, - ); - Some(SmpSaveRegionFixedRowRunCandidate { - count_offset, - count_offset_hex: format!("0x{count_offset:x}"), - row_count: target_row_count, - row_stride: SAVE_REGION_FIXED_ROW_STRIDE, - row_stride_hex: format!("0x{:x}", SAVE_REGION_FIXED_ROW_STRIDE), - rows_offset, - rows_offset_hex: format!("0x{rows_offset:x}"), - rows_end_offset, - rows_end_offset_hex: format!("0x{rows_end_offset:x}"), - distance_to_region_metadata_tag: scan_end_offset.saturating_sub(rows_end_offset), - distance_to_region_metadata_tag_hex: format!( - "0x{:x}", - scan_end_offset.saturating_sub(rows_end_offset) - ), - dword_lane_summaries, - shape_signature, - shape_family_signature, - trailing_byte_zero_count, - trailing_byte_nonzero_count, - trailing_byte_distinct_value_count: trailing_values.len(), - trailing_byte_sample_values_hex, - best_probable_density_lane_relative_offset_hex: best_probable_density_lane - .filter(|(_, count)| *count != 0) - .map(|(relative_offset, _)| format!("0x{relative_offset:x}")), - }) - }) - .collect::>(); - candidates.sort_by_key(|candidate| { - ( - Reverse( - candidate - .dword_lane_summaries - .iter() - .map(|summary| summary.probable_normal_f32_count) - .max() - .unwrap_or_default(), - ), - candidate.distance_to_region_metadata_tag, - candidate.count_offset, - ) - }); - candidates.truncate(SAVE_REGION_FIXED_ROW_CANDIDATE_PROBE_LIMIT); - let candidate_count = candidates.len(); - let best_candidate_offset_hex = candidates - .first() - .map(|candidate| candidate.rows_offset_hex.clone()); - Some(SmpSaveRegionFixedRowRunCandidateProbe { - profile_family: profile.profile_family.clone(), - source_kind: "save-region-fixed-row-run-candidates".to_string(), - semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), - target_row_count, - target_row_stride: SAVE_REGION_FIXED_ROW_STRIDE, - target_row_stride_hex: format!("0x{:x}", SAVE_REGION_FIXED_ROW_STRIDE), - scan_start_offset: 0, - scan_start_offset_hex: "0x0".to_string(), - scan_end_offset, - scan_end_offset_hex: format!("0x{scan_end_offset:x}"), - candidates, - evidence: vec![ - format!( - "candidate scan looks for pre-region-header counted runs keyed to the grounded live region count {} with fixed row stride 0x{:x}", - target_row_count, SAVE_REGION_FIXED_ROW_STRIDE - ), - format!( - "current scan range ends at region metadata tag offset 0x{:x}, because the atlas restore order places the fixed rows before the tagged 0x5209/0x520a/0x520b region collection", - scan_end_offset - ), - format!( - "kept {} highest-signal candidates after sorting by probable-f32 lane density and proximity to the region metadata tag; best candidate rows offset is {:?}", - candidate_count, best_candidate_offset_hex - ), - ], - }) -} - -fn build_save_region_fixed_row_run_candidate_shape_signature( - dword_lane_summaries: &[SmpSaveFixedRowRunDwordLaneSummary], - trailing_byte_zero_count: usize, - trailing_byte_distinct_value_count: usize, - row_count: usize, -) -> String { - fn pick_lane_terms( - summaries: &[SmpSaveFixedRowRunDwordLaneSummary], - score: F, - include: P, - row_count: usize, - max_terms: usize, - ) -> Vec - where - F: Fn(&SmpSaveFixedRowRunDwordLaneSummary) -> usize, - P: Fn(&SmpSaveFixedRowRunDwordLaneSummary) -> bool, - { - let high_signal_threshold = row_count.saturating_mul(3) / 4; - let mut picked = summaries - .iter() - .filter(|summary| include(summary)) - .filter(|summary| score(summary) >= high_signal_threshold) - .map(|summary| { - ( - summary.relative_offset, - format!("{}:{}", summary.relative_offset_hex, score(summary)), - ) - }) - .collect::>(); - if picked.is_empty() { - picked = summaries - .iter() - .filter(|summary| include(summary)) - .filter_map(|summary| { - let value = score(summary); - (value != 0).then(|| { - ( - summary.relative_offset, - format!("{}:{}", summary.relative_offset_hex, value), - ) - }) - }) - .collect::>(); - picked.sort_by_key(|(relative_offset, term)| { - let value = term - .split(':') - .nth(1) - .and_then(|part| part.parse::().ok()) - .unwrap_or_default(); - (Reverse(value), *relative_offset) - }); - picked.truncate(max_terms); - } - picked.into_iter().map(|(_, term)| term).collect() - } - - let probable_terms = pick_lane_terms( - dword_lane_summaries, - |summary| summary.probable_normal_f32_count, - |_| true, - row_count, - 3, - ); - let small_terms = pick_lane_terms( - dword_lane_summaries, - |summary| summary.small_unsigned_count, - |summary| summary.nonzero_count != 0, - row_count, - 2, - ); - let zero_terms = pick_lane_terms( - dword_lane_summaries, - |summary| summary.zero_count, - |summary| summary.zero_count != row_count, - row_count, - 2, - ); - - format!( - "pf32=[{}]|small=[{}]|zero=[{}]|trail={}/{}", - probable_terms.join(","), - small_terms.join(","), - zero_terms.join(","), - trailing_byte_zero_count, - trailing_byte_distinct_value_count - ) -} - -fn build_save_region_fixed_row_run_candidate_shape_family_signature( - dword_lane_summaries: &[SmpSaveFixedRowRunDwordLaneSummary], - trailing_byte_zero_count: usize, - trailing_byte_distinct_value_count: usize, - row_count: usize, -) -> String { - let dense_pf32_offsets = dword_lane_summaries - .iter() - .filter(|summary| summary.probable_normal_f32_count >= row_count.saturating_mul(3) / 4) - .map(|summary| summary.relative_offset_hex.clone()) - .collect::>(); - let partial_zero_offsets = dword_lane_summaries - .iter() - .filter(|summary| { - summary.zero_count != 0 - && summary.zero_count != row_count - && summary.zero_count >= row_count.saturating_mul(5) / 100 - }) - .map(|summary| summary.relative_offset_hex.clone()) - .collect::>(); - let small_nonzero_offsets = dword_lane_summaries - .iter() - .filter(|summary| { - summary.nonzero_count != 0 - && summary.small_unsigned_count >= row_count.saturating_mul(8) / 100 - }) - .map(|summary| summary.relative_offset_hex.clone()) - .collect::>(); - - format!( - "dense_pf32=[{}]|small_nonzero=[{}]|partial_zero=[{}]|trail_bucket={}/{}", - dense_pf32_offsets.join(","), - small_nonzero_offsets.join(","), - partial_zero_offsets.join(","), - trailing_byte_zero_count / 8, - trailing_byte_distinct_value_count / 8 - ) -} - -fn parse_save_placed_structure_record_triplet_probe( - bytes: &[u8], - header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { - let header_probe = header_probe?; - if header_probe.source_kind != "save-placed-structure-tagged-header-counts" { - return None; - } - let records_payload = - bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?; - let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG); - let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG); - let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG); - let record_count = header_probe.live_record_count as usize; - if name_offsets.len() != record_count - || policy_offsets.len() != record_count - || profile_offsets.len() != record_count - { - return None; - } - let mut entries = Vec::with_capacity(record_count); - for index in 0..record_count { - let name_tag_relative_offset = name_offsets[index]; - let policy_tag_relative_offset = policy_offsets[index]; - let profile_tag_relative_offset = profile_offsets[index]; - let next_record_relative_offset = name_offsets - .get(index + 1) - .copied() - .unwrap_or(records_payload.len()); - if !(name_tag_relative_offset < policy_tag_relative_offset - && policy_tag_relative_offset < profile_tag_relative_offset - && profile_tag_relative_offset < next_record_relative_offset) - { - return None; - } - let name_payload = - records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?; - let (primary_name, secondary_name) = parse_save_len_prefixed_ascii_name_pair(name_payload)?; - let policy_chunk_len = - profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?; - if policy_chunk_len != 0x1a { - return None; - } - let policy_payload = - records_payload.get(policy_tag_relative_offset + 4..profile_tag_relative_offset)?; - let policy_f32_lane_0 = f32::from_bits(read_u32_at(policy_payload, 0)?); - let policy_f32_lane_1 = f32::from_bits(read_u32_at(policy_payload, 4)?); - let policy_f32_lane_2 = f32::from_bits(read_u32_at(policy_payload, 8)?); - let policy_f32_lane_3 = f32::from_bits(read_u32_at(policy_payload, 12)?); - let policy_f32_lane_4 = f32::from_bits(read_u32_at(policy_payload, 16)?); - let policy_reserved_dword = read_u32_at(policy_payload, 20)?; - let policy_trailing_word = read_u16_at(policy_payload, 24)?; - let profile_chunk_len = - next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?; - let profile_payload = - records_payload.get(profile_tag_relative_offset + 4..next_record_relative_offset)?; - let profile_open_marker = read_u32_at(profile_payload, 0)?; - if profile_open_marker != 0x00005dc1 { - return None; - } - let (profile_repeated_primary_name, profile_repeated_secondary_name) = - parse_save_len_prefixed_ascii_name_pair(profile_payload.get(4..)?)?; - let mut trailer_offset = 4usize; - let repeated_primary_len = *profile_payload.get(trailer_offset)? as usize; - trailer_offset += 1 + repeated_primary_len; - while matches!(profile_payload.get(trailer_offset), Some(0)) { - trailer_offset += 1; - } - let repeated_secondary_len = *profile_payload.get(trailer_offset)? as usize; - trailer_offset += 1 + repeated_secondary_len; - let mut matched_footer = None; - for candidate_offset in [trailer_offset, trailer_offset + 1] { - if let ( - Some(profile_payload_dword), - Some(profile_sentinel_i32), - Some(profile_close_marker), - ) = ( - read_u32_at(profile_payload, candidate_offset), - read_i32_at(profile_payload, candidate_offset + 4), - read_u32_at(profile_payload, candidate_offset + 8), - ) { - if profile_close_marker == 0x00005dc2 { - matched_footer = Some(( - candidate_offset, - profile_payload_dword, - profile_sentinel_i32, - profile_close_marker, - )); - break; - } - } - } - let ( - profile_footer_relative_offset, - profile_payload_dword, - profile_sentinel_i32, - profile_close_marker, - ) = matched_footer?; - let profile_pre_footer_padding = profile_payload - .get(trailer_offset..profile_footer_relative_offset)? - .iter() - .map(|byte| format!("0x{byte:02x}")) - .collect::>(); - let profile_companion_byte_u8 = if profile_pre_footer_padding.len() == 1 { - profile_payload.get(trailer_offset).copied() - } else { - None - }; - let (profile_status_kind, farm_growth_stage_index) = - derive_save_placed_structure_profile_status( - &primary_name, - &secondary_name, - profile_sentinel_i32, - ); - entries.push(SmpSavePlacedStructureRecordTripletEntryProbe { - record_index: index, - primary_name, - secondary_name, - name_tag_relative_offset, - policy_tag_relative_offset, - profile_tag_relative_offset, - policy_chunk_len, - profile_chunk_len, - policy_f32_lane_0, - policy_f32_lane_1, - policy_f32_lane_2, - policy_f32_lane_3, - policy_f32_lane_4, - policy_reserved_dword, - policy_trailing_word, - policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"), - profile_open_marker, - profile_open_marker_hex: format!("0x{profile_open_marker:08x}"), - profile_repeated_primary_name, - profile_repeated_secondary_name, - profile_footer_relative_offset, - profile_footer_relative_offset_hex: format!("0x{profile_footer_relative_offset:x}"), - profile_pre_footer_padding_len: profile_pre_footer_padding.len(), - profile_pre_footer_padding_hex_bytes: profile_pre_footer_padding, - profile_companion_byte_hex: profile_companion_byte_u8 - .map(|byte| format!("0x{byte:02x}")), - profile_companion_byte_u8, - profile_payload_dword, - profile_payload_dword_hex: format!("0x{profile_payload_dword:08x}"), - profile_sentinel_i32, - profile_status_kind: profile_status_kind.to_string(), - farm_growth_stage_index, - profile_close_marker, - profile_close_marker_hex: format!("0x{profile_close_marker:08x}"), - }); - } - let farm_growth_stage_entry_count = entries - .iter() - .filter(|entry| entry.farm_growth_stage_index.is_some()) - .count(); - Some(SmpSavePlacedStructureRecordTripletProbe { - profile_family: header_probe.profile_family.clone(), - source_kind: "save-placed-structure-record-triplets".to_string(), - semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), - records_tag_offset: header_probe.records_tag_offset, - close_tag_offset: header_probe.close_tag_offset, - record_count, - entries, - evidence: vec![ - "save-side placed-structure records are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the tagged records span".to_string(), - "the 0x55f1 chunk currently exposes two len-prefixed structure-name stems before the fixed 0x55f2 policy row".to_string(), - "each fixed placed-structure 0x55f2 policy chunk currently decodes as five f32-like lanes, one reserved dword, and one trailing u16 word".to_string(), - format!( - "the compact 0x55f3 footer status lane behaves like a farm growth-stage bucket on grounded saves: {farm_growth_stage_entry_count} entries expose nonnegative 0..11 values and all observed non-farm families stay at -1" - ), - ], - }) -} - -fn derive_save_placed_structure_profile_status( - primary_name: &str, - secondary_name: &str, - raw_status: i32, -) -> (&'static str, Option) { - let looks_like_farm = primary_name.starts_with("Farm") || secondary_name.contains("Farm"); - if raw_status == -1 { - return ("unset", None); - } - if looks_like_farm && (0..=11).contains(&raw_status) { - return ("farm_growth_stage_bucket", Some(raw_status as u8)); - } - ("opaque_nondefault", None) -} - -fn parse_save_placed_structure_collection_header_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - parse_save_tagged_collection_header_probe( - bytes, - file_extension_hint, - container_profile, - 0x000036b1, - 0x000036b2, - 0x000036b3, - "save-placed-structure-tagged-header-counts", - "scenario-save-placed-structure-header-counts", - |header| { - header.direct_collection_flag == 0 - && header.direct_record_stride >= 1 - && header.direct_record_stride <= 0x20 - && header.live_id_bound >= 0x100 - && header.live_record_count >= 0x100 - && header.live_record_count <= header.live_id_bound - }, - vec![ - "save-side placed-structure collection uses tagged family 0x36b1/0x36b2/0x36b3 beneath the wider local-runtime and route-entry rebuild owners".to_string(), - "current evidence only grounds header-level placed-structure collection counts here; direct record-body reconstruction still needs the later per-entry load/save slot study.".to_string(), - ], - ) -} - -fn parse_save_placed_structure_dynamic_side_buffer_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - #[derive(Clone)] - struct EmbeddedNameRow { - name_tag_relative_offset: usize, - prefix_leading_dword: u32, - prefix_trailing_word: u16, - prefix_separator_byte: u8, - name_payload_relative_offset: usize, - name_payload_len: usize, - primary_name: Option, - secondary_name: Option, - tertiary_name: Option, - } - - #[derive(Default)] - struct PrefixPatternAccumulator { - count: usize, - first_name_tag_relative_offset: usize, - first_primary_name: Option, - first_secondary_name: Option, - section_like_primary_name_count: usize, - cap_like_primary_name_count: usize, - other_primary_name_count: usize, - } - - #[derive(Default)] - struct NamePairAccumulator { - count: usize, - first_name_tag_relative_offset: usize, - prefix_counts: BTreeMap<(u32, u16, u8), usize>, - } - - #[derive(Clone)] - struct PayloadEnvelopeRow { - name_tag_relative_offset: usize, - primary_name: Option, - secondary_name: Option, - name_payload_end_relative_offset: Option, - policy_tag_relative_offset: Option, - profile_tag_relative_offset: Option, - next_name_tag_relative_offset: Option, - name_to_policy_gap_len: Option, - policy_chunk_len: Option, - profile_chunk_len_to_next_name_or_end: Option, - short_profile_first_flag_byte: Option, - short_profile_second_flag_byte: Option, - } - - if file_extension_hint != Some("gms") { - return None; - } - let profile = container_profile?; - if !matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ) { - return None; - } - - let metadata_offsets = find_u32_le_offsets(bytes, 0x000038a5); - let records_offsets = find_u32_le_offsets(bytes, 0x000038a6); - let close_offsets = find_u32_le_offsets(bytes, 0x000038a7); - for metadata_tag_offset in metadata_offsets { - let Some(records_tag_offset) = records_offsets - .iter() - .copied() - .find(|offset| *offset > metadata_tag_offset) - else { - continue; - }; - let Some(close_tag_offset) = close_offsets - .iter() - .copied() - .find(|offset| *offset > records_tag_offset) - else { - continue; - }; - let Some(payload) = bytes.get(metadata_tag_offset + 4..records_tag_offset) else { - continue; - }; - if payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { - continue; - } - let Some(header_words) = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT) - .map(|index| read_u32_at(payload, index * 4)) - .collect::>>() - else { - continue; - }; - let Some(header_words): Option<[u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT]> = - header_words.try_into().ok() - else { - continue; - }; - let summary = IndexedCollectionHeaderSummary { - metadata_tag_offset, - records_tag_offset, - close_tag_offset, - direct_collection_flag: header_words[0], - direct_record_stride: header_words[1], - live_id_bound: header_words[4], - live_record_count: header_words[5], - header_words, - }; - if !(summary.direct_collection_flag == 0 - && summary.direct_record_stride == 0x06 - && summary.header_words.get(2) == Some(&1000) - && summary.header_words.get(3) == Some(&500) - && summary.header_words.get(6) == Some(&0) - && summary.header_words.get(7) == Some(&1) - && summary.live_id_bound >= 0x100 - && summary.live_id_bound <= 0x1000 - && summary.live_record_count >= 0x100 - && summary.live_record_count <= summary.live_id_bound) - { - continue; - } - let Some(records_payload) = bytes.get(records_tag_offset + 4..close_tag_offset) else { - continue; - }; - let embedded_name_tag_offsets = - find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG); - let Some(&first_embedded_name_tag_relative_offset) = embedded_name_tag_offsets.first() - else { - continue; - }; - let Some(prefix_payload) = records_payload.get(..first_embedded_name_tag_relative_offset) - else { - continue; - }; - if prefix_payload.len() < 7 { - continue; - } - let Some(owner_shared_dword) = read_u32_at(prefix_payload, 0) else { - continue; - }; - let owner_shared_dword_relative_offset = 0usize; - let first_record_child_count_after_owner_shared = read_u16_at(records_payload, 4); - let first_record_saved_primary_child_byte_after_owner_shared = - read_u8_at(records_payload, 6); - let first_record_first_name_tag_relative_offset_after_owner_shared = (3usize..=16usize) - .find(|offset| { - read_u16_at(records_payload, 4 + *offset) == Some(SAVE_REGION_RECORD_NAME_TAG) - }); - let prefix_leading_dword = owner_shared_dword; - let Some(prefix_trailing_word) = read_u16_at(prefix_payload, 4) else { - continue; - }; - let Some(prefix_separator_byte) = prefix_payload.get(6).copied() else { - continue; - }; - let mut parsed_embedded_names = None; - for relative_name_offset in [4usize, 6usize] { - let Some(name_payload) = records_payload - .get(first_embedded_name_tag_relative_offset + relative_name_offset..) - else { - continue; - }; - if let Some(names) = parse_save_len_prefixed_ascii_name_triplet(name_payload) { - parsed_embedded_names = Some(names); - break; - } - } - let Some(( - first_embedded_primary_name, - first_embedded_secondary_name, - first_embedded_tertiary_name, - )) = parsed_embedded_names - else { - continue; - }; - let embedded_name_rows = embedded_name_tag_offsets - .iter() - .copied() - .filter_map(|name_tag_relative_offset| { - let prefix_payload = records_payload.get(..name_tag_relative_offset)?; - if prefix_payload.len() < 7 { - return None; - } - let prefix_leading_dword = read_u32_at(prefix_payload, prefix_payload.len() - 7)?; - let prefix_trailing_word = read_u16_at(prefix_payload, prefix_payload.len() - 3)?; - let prefix_separator_byte = *prefix_payload.last()?; - let mut parsed_names = None; - for relative_name_offset in [4usize, 6usize] { - let Some(name_payload) = - records_payload.get(name_tag_relative_offset + relative_name_offset..) - else { - continue; - }; - if let Some((names, name_payload_len)) = - parse_save_len_prefixed_ascii_name_triplet_and_consumed_len(name_payload) - { - parsed_names = Some((relative_name_offset, name_payload_len, names)); - break; - } - } - let (name_payload_relative_offset, name_payload_len, names) = - parsed_names.unwrap_or((4usize, 0usize, Default::default())); - let (primary_name, secondary_name, tertiary_name) = names; - Some(EmbeddedNameRow { - name_tag_relative_offset, - prefix_leading_dword, - prefix_trailing_word, - prefix_separator_byte, - name_payload_relative_offset, - name_payload_len, - primary_name: (!primary_name.is_empty()).then_some(primary_name), - secondary_name: (!secondary_name.is_empty()).then_some(secondary_name), - tertiary_name, - }) - }) - .collect::>(); - let policy_tag_offsets = - find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG); - let profile_tag_offsets = - find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG); - let payload_envelope_rows = embedded_name_rows - .iter() - .enumerate() - .map(|(row_index, row)| { - let next_name_tag_relative_offset = embedded_name_tag_offsets - .get(row_index + 1) - .copied() - .or(Some(records_payload.len())); - let name_payload_end_relative_offset = Some( - row.name_tag_relative_offset - + row.name_payload_relative_offset - + row.name_payload_len, - ); - let policy_tag_relative_offset = - policy_tag_offsets.iter().copied().find(|offset| { - *offset > row.name_tag_relative_offset - && next_name_tag_relative_offset - .is_none_or(|next_name| *offset < next_name) - }); - let profile_tag_relative_offset = policy_tag_relative_offset.and_then(|policy| { - profile_tag_offsets.iter().copied().find(|offset| { - *offset > policy - && next_name_tag_relative_offset - .is_none_or(|next_name| *offset < next_name) - }) - }); - let name_to_policy_gap_len = name_payload_end_relative_offset - .zip(policy_tag_relative_offset) - .and_then( - |(name_payload_end_relative_offset, policy_tag_relative_offset)| { - policy_tag_relative_offset.checked_sub(name_payload_end_relative_offset) - }, - ); - let policy_chunk_len = policy_tag_relative_offset - .zip(profile_tag_relative_offset) - .and_then( - |(policy_tag_relative_offset, profile_tag_relative_offset)| { - profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4) - }, - ); - let profile_chunk_len_to_next_name_or_end = - profile_tag_relative_offset.and_then(|profile_tag_relative_offset| { - next_name_tag_relative_offset.and_then(|next_name_tag_relative_offset| { - next_name_tag_relative_offset - .checked_sub(profile_tag_relative_offset + 4) - }) - }); - let short_profile_first_flag_byte = - profile_tag_relative_offset.and_then(|profile_tag_relative_offset| { - records_payload - .get(profile_tag_relative_offset + 4) - .copied() - }); - let short_profile_second_flag_byte = - profile_tag_relative_offset.and_then(|profile_tag_relative_offset| { - records_payload - .get(profile_tag_relative_offset + 5) - .copied() - }); - PayloadEnvelopeRow { - name_tag_relative_offset: row.name_tag_relative_offset, - primary_name: row.primary_name.clone(), - secondary_name: row.secondary_name.clone(), - name_payload_end_relative_offset, - policy_tag_relative_offset, - profile_tag_relative_offset, - next_name_tag_relative_offset, - name_to_policy_gap_len, - policy_chunk_len, - profile_chunk_len_to_next_name_or_end, - short_profile_first_flag_byte, - short_profile_second_flag_byte, - } - }) - .collect::>(); - let embedded_name_row_samples = embedded_name_rows - .iter() - .take(8) - .enumerate() - .map( - |(sample_index, row)| SmpSavePlacedStructureDynamicSideBufferSampleEntry { - sample_index, - name_tag_relative_offset: row.name_tag_relative_offset, - prefix_leading_dword: row.prefix_leading_dword, - prefix_leading_dword_hex: format!("0x{:08x}", row.prefix_leading_dword), - prefix_trailing_word: row.prefix_trailing_word, - prefix_trailing_word_hex: format!("0x{:04x}", row.prefix_trailing_word), - prefix_separator_byte: row.prefix_separator_byte, - prefix_separator_byte_hex: format!("0x{:02x}", row.prefix_separator_byte), - primary_name: row.primary_name.clone(), - secondary_name: row.secondary_name.clone(), - tertiary_name: row.tertiary_name.clone(), - }, - ) - .collect::>(); - let mut compact_prefix_pattern_map = - BTreeMap::<(u32, u16, u8), PrefixPatternAccumulator>::new(); - let mut name_pair_map = BTreeMap::<(String, String), NamePairAccumulator>::new(); - for row in &embedded_name_rows { - let entry = compact_prefix_pattern_map - .entry(( - row.prefix_leading_dword, - row.prefix_trailing_word, - row.prefix_separator_byte, - )) - .or_insert_with(|| PrefixPatternAccumulator { - first_name_tag_relative_offset: row.name_tag_relative_offset, - first_primary_name: row.primary_name.clone(), - first_secondary_name: row.secondary_name.clone(), - ..Default::default() - }); - entry.count += 1; - match row.primary_name.as_deref() { - Some(name) if name.ends_with("_Section.3dp") => { - entry.section_like_primary_name_count += 1; - } - Some(name) if name.ends_with("_Cap.3dp") => { - entry.cap_like_primary_name_count += 1; - } - _ => { - entry.other_primary_name_count += 1; - } - } - if let (Some(primary_name), Some(secondary_name)) = - (row.primary_name.as_ref(), row.secondary_name.as_ref()) - { - let entry = name_pair_map - .entry((primary_name.clone(), secondary_name.clone())) - .or_insert_with(|| NamePairAccumulator { - first_name_tag_relative_offset: row.name_tag_relative_offset, - ..Default::default() - }); - entry.count += 1; - *entry - .prefix_counts - .entry(( - row.prefix_leading_dword, - row.prefix_trailing_word, - row.prefix_separator_byte, - )) - .or_default() += 1; - } - } - let prefix_leading_dword_matching_embedded_profile_tag_count = embedded_name_rows - .iter() - .filter(|row| row.prefix_leading_dword == u32::from(SAVE_REGION_RECORD_PROFILE_TAG)) - .count(); - let mut compact_prefix_pattern_summaries = compact_prefix_pattern_map - .into_iter() - .map( - |( - (prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), - accumulator, - )| { - SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary { - prefix_leading_dword, - prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), - prefix_trailing_word, - prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), - prefix_separator_byte, - prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), - count: accumulator.count, - first_name_tag_relative_offset: accumulator.first_name_tag_relative_offset, - prefix_leading_dword_matches_embedded_profile_tag: prefix_leading_dword - == u32::from(SAVE_REGION_RECORD_PROFILE_TAG), - section_like_primary_name_count: accumulator - .section_like_primary_name_count, - cap_like_primary_name_count: accumulator.cap_like_primary_name_count, - other_primary_name_count: accumulator.other_primary_name_count, - first_primary_name: accumulator.first_primary_name, - first_secondary_name: accumulator.first_secondary_name, - } - }, - ) - .collect::>(); - compact_prefix_pattern_summaries.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| { - left.first_name_tag_relative_offset - .cmp(&right.first_name_tag_relative_offset) - }) - .then_with(|| left.prefix_leading_dword.cmp(&right.prefix_leading_dword)) - .then_with(|| left.prefix_trailing_word.cmp(&right.prefix_trailing_word)) - .then_with(|| left.prefix_separator_byte.cmp(&right.prefix_separator_byte)) - }); - let mut name_pair_summaries = name_pair_map - .into_iter() - .filter_map(|((primary_name, secondary_name), accumulator)| { - let dominant_prefix = accumulator.prefix_counts.iter().max_by( - |(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }, - )?; - let ( - dominant_prefix_leading_dword, - dominant_prefix_trailing_word, - dominant_prefix_separator_byte, - ) = *dominant_prefix.0; - let dominant_prefix_count = *dominant_prefix.1; - Some(SmpSavePlacedStructureDynamicSideBufferNamePairSummary { - primary_name, - secondary_name, - count: accumulator.count, - first_name_tag_relative_offset: accumulator.first_name_tag_relative_offset, - unique_compact_prefix_pattern_count: accumulator.prefix_counts.len(), - dominant_prefix_leading_dword, - dominant_prefix_leading_dword_hex: format!( - "0x{dominant_prefix_leading_dword:08x}" - ), - dominant_prefix_trailing_word, - dominant_prefix_trailing_word_hex: format!( - "0x{dominant_prefix_trailing_word:04x}" - ), - dominant_prefix_separator_byte, - dominant_prefix_separator_byte_hex: format!( - "0x{dominant_prefix_separator_byte:02x}" - ), - dominant_prefix_count, - }) - }) - .collect::>(); - name_pair_summaries.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| { - left.first_name_tag_relative_offset - .cmp(&right.first_name_tag_relative_offset) - }) - .then_with(|| left.primary_name.cmp(&right.primary_name)) - .then_with(|| left.secondary_name.cmp(&right.secondary_name)) - }); - let row_count_with_policy_tag_before_next_name = payload_envelope_rows - .iter() - .filter(|row| row.policy_tag_relative_offset.is_some()) - .count(); - let row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope = payload_envelope_rows - .iter() - .filter(|row| { - row.policy_tag_relative_offset.is_some() - && row.profile_tag_relative_offset.is_some() - }) - .count(); - let row_count_missing_policy_tag_before_next_name = payload_envelope_rows - .iter() - .filter(|row| row.policy_tag_relative_offset.is_none()) - .count(); - let row_count_missing_profile_tag_after_policy = payload_envelope_rows - .iter() - .filter(|row| { - row.policy_tag_relative_offset.is_some() - && row.profile_tag_relative_offset.is_none() - }) - .count(); - let mut policy_chunk_len_counts = BTreeMap::::new(); - let mut profile_chunk_len_counts = BTreeMap::::new(); - for row in &payload_envelope_rows { - if let Some(policy_chunk_len) = row.policy_chunk_len { - *policy_chunk_len_counts.entry(policy_chunk_len).or_default() += 1; - } - if let Some(profile_chunk_len_to_next_name_or_end) = - row.profile_chunk_len_to_next_name_or_end - { - *profile_chunk_len_counts - .entry(profile_chunk_len_to_next_name_or_end) - .or_default() += 1; - } - } - let unique_policy_chunk_lens = policy_chunk_len_counts.keys().copied().collect::>(); - let unique_profile_chunk_lens = - profile_chunk_len_counts.keys().copied().collect::>(); - let dominant_policy_chunk_len = policy_chunk_len_counts - .iter() - .max_by(|(left_len, left_count), (right_len, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_len.cmp(left_len)) - }) - .map(|(len, count)| (*len, *count)); - let dominant_profile_chunk_len = profile_chunk_len_counts - .iter() - .max_by(|(left_len, left_count), (right_len, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_len.cmp(left_len)) - }) - .map(|(len, count)| (*len, *count)); - let short_profile_flag_rows = payload_envelope_rows - .iter() - .filter(|row| row.profile_chunk_len_to_next_name_or_end == Some(6)) - .filter_map(|row| { - Some(( - row.name_tag_relative_offset, - row.primary_name.clone(), - row.secondary_name.clone(), - row.short_profile_first_flag_byte?, - row.short_profile_second_flag_byte?, - )) - }) - .collect::>(); - let mut short_profile_flag_pair_counts = BTreeMap::<(u8, u8), usize>::new(); - let mut short_profile_first_flag_counts = BTreeMap::::new(); - let mut short_profile_second_flag_counts = BTreeMap::::new(); - for (_, _, _, first_flag_byte, second_flag_byte) in &short_profile_flag_rows { - *short_profile_flag_pair_counts - .entry((*first_flag_byte, *second_flag_byte)) - .or_default() += 1; - *short_profile_first_flag_counts - .entry(*first_flag_byte) - .or_default() += 1; - *short_profile_second_flag_counts - .entry(*second_flag_byte) - .or_default() += 1; - } - let dominant_short_profile_flag_pair = short_profile_flag_pair_counts - .iter() - .max_by(|(left_pair, left_count), (right_pair, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_pair.cmp(left_pair)) - }) - .map(|((first_flag_byte, second_flag_byte), count)| { - SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPair { - first_flag_byte: *first_flag_byte, - first_flag_byte_hex: format!("0x{first_flag_byte:02x}"), - second_flag_byte: *second_flag_byte, - second_flag_byte_hex: format!("0x{second_flag_byte:02x}"), - count: *count, - } - }); - let dominant_short_profile_first_flag = short_profile_first_flag_counts - .iter() - .max_by(|(left_byte, left_count), (right_byte, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_byte.cmp(left_byte)) - }) - .map(|(byte, count)| (*byte, *count)); - let dominant_short_profile_second_flag = short_profile_second_flag_counts - .iter() - .max_by(|(left_byte, left_count), (right_byte, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_byte.cmp(left_byte)) - }) - .map(|(byte, count)| (*byte, *count)); - let short_profile_flag_pair_summary = Some( - SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSummary { - row_count_with_0x06_profile_span: short_profile_flag_rows.len(), - unique_flag_pair_count: short_profile_flag_pair_counts.len(), - dominant_first_flag_byte: dominant_short_profile_first_flag.map(|(byte, _)| byte), - dominant_first_flag_byte_hex: dominant_short_profile_first_flag - .map(|(byte, _)| format!("0x{byte:02x}")), - dominant_first_flag_byte_count: dominant_short_profile_first_flag - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_second_flag_byte: dominant_short_profile_second_flag.map(|(byte, _)| byte), - dominant_second_flag_byte_hex: dominant_short_profile_second_flag - .map(|(byte, _)| format!("0x{byte:02x}")), - dominant_second_flag_byte_count: dominant_short_profile_second_flag - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_flag_pair: dominant_short_profile_flag_pair, - sample_rows: short_profile_flag_rows - .iter() - .take(8) - .enumerate() - .map( - |( - sample_index, - ( - name_tag_relative_offset, - primary_name, - secondary_name, - first_flag_byte, - second_flag_byte, - ), - )| { - SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSample { - sample_index, - name_tag_relative_offset: *name_tag_relative_offset, - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - first_flag_byte: *first_flag_byte, - first_flag_byte_hex: format!("0x{first_flag_byte:02x}"), - second_flag_byte: *second_flag_byte, - second_flag_byte_hex: format!("0x{second_flag_byte:02x}"), - } - }, - ) - .collect(), - }, - ); - let classify_side_buffer_mode_family = - |primary_name: Option<&str>, secondary_name: Option<&str>| -> &'static str { - let name = primary_name.or(secondary_name).unwrap_or_default(); - if name.starts_with("Bridge") { - "bridge" - } else if name.starts_with("Tunnel") { - "tunnel" - } else if name.starts_with("BallastCap") { - "ballast_cap" - } else if name.starts_with("TrackCap") { - "track_cap" - } else if name.starts_with("Overpass") { - "overpass" - } else if name.is_empty() { - "unknown" - } else { - "other" - } - }; - let fixed_policy_rows = payload_envelope_rows - .iter() - .filter(|row| row.policy_chunk_len == Some(0x1a)) - .filter_map(|row| { - let embedded_name_row = embedded_name_rows - .iter() - .find(|entry| entry.name_tag_relative_offset == row.name_tag_relative_offset)?; - let policy_tag_relative_offset = row.policy_tag_relative_offset?; - let policy_payload = records_payload - .get(policy_tag_relative_offset + 4..policy_tag_relative_offset + 4 + 0x1a)?; - let first_triplet_dwords = [ - read_u32_at(policy_payload, 0)?, - read_u32_at(policy_payload, 4)?, - read_u32_at(policy_payload, 8)?, - ]; - let second_triplet_dwords = [ - read_u32_at(policy_payload, 12)?, - read_u32_at(policy_payload, 16)?, - read_u32_at(policy_payload, 20)?, - ]; - let trailing_word = read_u16_at(policy_payload, 24)?; - Some(( - row.name_tag_relative_offset, - row.primary_name.clone(), - row.secondary_name.clone(), - embedded_name_row.prefix_leading_dword, - embedded_name_row.prefix_trailing_word, - embedded_name_row.prefix_separator_byte, - first_triplet_dwords, - second_triplet_dwords, - trailing_word, - )) - }) - .collect::>(); - let mut fixed_policy_trailing_word_counts = BTreeMap::::new(); - for (_, _, _, _, _, _, _, _, trailing_word) in &fixed_policy_rows { - *fixed_policy_trailing_word_counts - .entry(*trailing_word) - .or_default() += 1; - } - let dominant_fixed_policy_trailing_word = fixed_policy_trailing_word_counts - .iter() - .max_by(|(left_word, left_count), (right_word, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_word.cmp(left_word)) - }) - .map(|(word, count)| (*word, *count)); - let mut fixed_policy_compact_prefix_groups = BTreeMap::< - (u32, u16, u8), - Vec<( - usize, - Option, - Option, - [u32; 3], - [u32; 3], - u16, - )>, - >::new(); - for ( - name_tag_relative_offset, - primary_name, - secondary_name, - prefix_leading_dword, - prefix_trailing_word, - prefix_separator_byte, - first_triplet_dwords, - second_triplet_dwords, - trailing_word, - ) in &fixed_policy_rows - { - fixed_policy_compact_prefix_groups - .entry(( - *prefix_leading_dword, - *prefix_trailing_word, - *prefix_separator_byte, - )) - .or_default() - .push(( - *name_tag_relative_offset, - primary_name.clone(), - secondary_name.clone(), - *first_triplet_dwords, - *second_triplet_dwords, - *trailing_word, - )); - } - let fixed_policy_compact_prefix_correlations = fixed_policy_compact_prefix_groups - .into_iter() - .map( - |((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), rows)| { - let mut name_pair_counts = - BTreeMap::<(Option, Option), usize>::new(); - let mut mode_family_counts = BTreeMap::::new(); - let mut policy_tuple_counts = - BTreeMap::<([u32; 3], [u32; 3], u16), usize>::new(); - for ( - _name_tag_relative_offset, - primary_name, - secondary_name, - first_triplet_dwords, - second_triplet_dwords, - trailing_word, - ) in &rows - { - *name_pair_counts - .entry((primary_name.clone(), secondary_name.clone())) - .or_default() += 1; - *mode_family_counts - .entry( - classify_side_buffer_mode_family( - primary_name.as_deref(), - secondary_name.as_deref(), - ) - .to_string(), - ) - .or_default() += 1; - *policy_tuple_counts - .entry(( - *first_triplet_dwords, - *second_triplet_dwords, - *trailing_word, - )) - .or_default() += 1; - } - let dominant_name_pair = name_pair_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|((primary_name, secondary_name), count)| { - (primary_name.clone(), secondary_name.clone(), *count) - }); - let dominant_mode_family = mode_family_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(mode_family, count)| (mode_family.clone(), *count)); - let dominant_policy_tuple = policy_tuple_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map( - |( - (first_triplet_dwords, second_triplet_dwords, trailing_word), - count, - )| { - ( - *first_triplet_dwords, - *second_triplet_dwords, - *trailing_word, - *count, - ) - }, - ); - SmpSavePlacedStructureDynamicSideBufferFixedPolicyCompactPrefixCorrelation { - prefix_leading_dword, - prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), - prefix_trailing_word, - prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), - prefix_separator_byte, - prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), - row_count: rows.len(), - unique_policy_tuple_count: policy_tuple_counts.len(), - dominant_primary_name: dominant_name_pair - .as_ref() - .and_then(|(primary_name, _, _)| primary_name.clone()), - dominant_secondary_name: dominant_name_pair - .as_ref() - .and_then(|(_, secondary_name, _)| secondary_name.clone()), - dominant_name_pair_count: dominant_name_pair - .map(|(_, _, count)| count) - .unwrap_or_default(), - dominant_mode_family: dominant_mode_family - .as_ref() - .map(|(mode_family, _)| mode_family.clone()), - dominant_mode_family_count: dominant_mode_family - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_first_triplet_dwords_hex: dominant_policy_tuple - .as_ref() - .map(|(first_triplet_dwords, _, _, _)| { - first_triplet_dwords - .iter() - .map(|value| format!("0x{value:08x}")) - .collect() - }) - .unwrap_or_default(), - dominant_second_triplet_dwords_hex: dominant_policy_tuple - .as_ref() - .map(|(_, second_triplet_dwords, _, _)| { - second_triplet_dwords - .iter() - .map(|value| format!("0x{value:08x}")) - .collect() - }) - .unwrap_or_default(), - dominant_trailing_word: dominant_policy_tuple - .map(|(_, _, trailing_word, _)| trailing_word), - dominant_trailing_word_hex: dominant_policy_tuple - .map(|(_, _, trailing_word, _)| format!("0x{trailing_word:04x}")), - dominant_policy_tuple_count: dominant_policy_tuple - .map(|(_, _, _, count)| count) - .unwrap_or_default(), - mode_family_counts: mode_family_counts - .into_iter() - .map(|(mode_family, count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family, - count, - } - }) - .collect(), - sample_rows: rows - .iter() - .take(8) - .enumerate() - .map( - |( - sample_index, - ( - name_tag_relative_offset, - primary_name, - secondary_name, - first_triplet_dwords, - second_triplet_dwords, - trailing_word, - ), - )| { - SmpSavePlacedStructureDynamicSideBufferFixedPolicySample { - sample_index, - name_tag_relative_offset: *name_tag_relative_offset, - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - first_triplet_dwords_hex: first_triplet_dwords - .iter() - .map(|value| format!("0x{value:08x}")) - .collect(), - second_triplet_dwords_hex: second_triplet_dwords - .iter() - .map(|value| format!("0x{value:08x}")) - .collect(), - trailing_word: *trailing_word, - trailing_word_hex: format!("0x{trailing_word:04x}"), - } - }, - ) - .collect(), - } - }, - ) - .take(8) - .collect::>(); - let fixed_policy_summary = - Some(SmpSavePlacedStructureDynamicSideBufferFixedPolicySummary { - row_count_with_0x1a_policy_chunk: fixed_policy_rows.len(), - unique_trailing_word_count: fixed_policy_trailing_word_counts.len(), - dominant_trailing_word: dominant_fixed_policy_trailing_word.map(|(word, _)| word), - dominant_trailing_word_hex: dominant_fixed_policy_trailing_word - .map(|(word, _)| format!("0x{word:04x}")), - dominant_trailing_word_count: dominant_fixed_policy_trailing_word - .map(|(_, count)| count) - .unwrap_or_default(), - compact_prefix_correlations: fixed_policy_compact_prefix_correlations, - sample_rows: fixed_policy_rows - .iter() - .take(8) - .enumerate() - .map( - |( - sample_index, - ( - name_tag_relative_offset, - primary_name, - secondary_name, - _prefix_leading_dword, - _prefix_trailing_word, - _prefix_separator_byte, - first_triplet_dwords, - second_triplet_dwords, - trailing_word, - ), - )| { - SmpSavePlacedStructureDynamicSideBufferFixedPolicySample { - sample_index, - name_tag_relative_offset: *name_tag_relative_offset, - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - first_triplet_dwords_hex: first_triplet_dwords - .iter() - .map(|value| format!("0x{value:08x}")) - .collect(), - second_triplet_dwords_hex: second_triplet_dwords - .iter() - .map(|value| format!("0x{value:08x}")) - .collect(), - trailing_word: *trailing_word, - trailing_word_hex: format!("0x{trailing_word:04x}"), - } - }, - ) - .collect(), - }); - let name_prelude_candidate_rows = embedded_name_rows - .iter() - .enumerate() - .filter_map(|(row_index, row)| { - let candidate_offset = row.name_tag_relative_offset.checked_sub(3)?; - let child_count_candidate = read_u16_at(records_payload, candidate_offset)?; - let saved_primary_child_byte_candidate = - read_u8_at(records_payload, candidate_offset + 2)?; - Some(( - row.name_tag_relative_offset, - row.primary_name.clone(), - row.secondary_name.clone(), - row.prefix_leading_dword, - row.prefix_trailing_word, - row.prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - row_index.checked_sub(1).and_then(|previous_index| { - payload_envelope_rows - .get(previous_index) - .and_then(|row| row.profile_chunk_len_to_next_name_or_end) - }), - row_index.checked_sub(1).and_then(|previous_index| { - payload_envelope_rows.get(previous_index).and_then(|row| { - row.short_profile_first_flag_byte - .zip(row.short_profile_second_flag_byte) - }) - }), - )) - }) - .collect::>(); - let mut name_prelude_candidate_pattern_counts = BTreeMap::<(u16, u8), usize>::new(); - let mut name_prelude_child_count_counts = BTreeMap::::new(); - let mut name_prelude_saved_primary_counts = BTreeMap::::new(); - for (_, _, _, _, _, _, child_count_candidate, saved_primary_child_byte_candidate, _, _) in - &name_prelude_candidate_rows - { - *name_prelude_candidate_pattern_counts - .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) - .or_default() += 1; - *name_prelude_child_count_counts - .entry(*child_count_candidate) - .or_default() += 1; - *name_prelude_saved_primary_counts - .entry(*saved_primary_child_byte_candidate) - .or_default() += 1; - } - let dominant_name_prelude_candidate_pattern = name_prelude_candidate_pattern_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map( - |((child_count_candidate, saved_primary_child_byte_candidate), count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), - saved_primary_child_byte_candidate: *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - count: *count, - } - }, - ); - let dominant_name_prelude_child_count = name_prelude_child_count_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(value, count)| (*value, *count)); - let dominant_name_prelude_saved_primary = name_prelude_saved_primary_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(value, count)| (*value, *count)); - let mut name_prelude_pattern_groups = BTreeMap::< - (u16, u8), - Vec<(usize, Option, Option, Option)>, - >::new(); - for ( - name_tag_relative_offset, - primary_name, - secondary_name, - _prefix_leading_dword, - _prefix_trailing_word, - _prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - previous_span, - _previous_short_profile_flag_pair, - ) in &name_prelude_candidate_rows - { - name_prelude_pattern_groups - .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) - .or_default() - .push(( - *name_tag_relative_offset, - primary_name.clone(), - secondary_name.clone(), - *previous_span, - )); - } - let candidate_pattern_correlations = name_prelude_pattern_groups - .into_iter() - .map( - |( - (child_count_candidate, saved_primary_child_byte_candidate), - rows, - )| { - let mut name_pair_counts = - BTreeMap::<(Option, Option), usize>::new(); - let mut profile_span_counts = BTreeMap::::new(); - let mut mode_family_counts = BTreeMap::::new(); - for (_, primary_name, secondary_name, previous_span) in &rows { - *name_pair_counts - .entry((primary_name.clone(), secondary_name.clone())) - .or_default() += 1; - *mode_family_counts - .entry( - classify_side_buffer_mode_family( - primary_name.as_deref(), - secondary_name.as_deref(), - ) - .to_string(), - ) - .or_default() += 1; - if let Some(previous_span) = previous_span { - *profile_span_counts.entry(*previous_span).or_default() += 1; - } - } - let dominant_name_pair = name_pair_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|((primary_name, secondary_name), count)| { - (primary_name.clone(), secondary_name.clone(), *count) - }); - let dominant_profile_span = profile_span_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(span, count)| (*span, *count)); - let dominant_mode_family = mode_family_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(mode_family, count)| (mode_family.clone(), *count)); - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePatternCorrelation { - child_count_candidate, - child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), - saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - row_count: rows.len(), - unique_name_pair_count: name_pair_counts.len(), - unique_profile_span_count: profile_span_counts.len(), - dominant_primary_name: dominant_name_pair - .as_ref() - .and_then(|(primary_name, _, _)| primary_name.clone()), - dominant_secondary_name: dominant_name_pair - .as_ref() - .and_then(|(_, secondary_name, _)| secondary_name.clone()), - dominant_name_pair_count: dominant_name_pair - .map(|(_, _, count)| count) - .unwrap_or_default(), - dominant_profile_span: dominant_profile_span - .map(|(profile_span, _)| profile_span), - dominant_profile_span_count: dominant_profile_span - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_mode_family: dominant_mode_family - .as_ref() - .map(|(mode_family, _)| mode_family.clone()), - dominant_mode_family_count: dominant_mode_family - .map(|(_, count)| count) - .unwrap_or_default(), - mode_family_counts: mode_family_counts - .into_iter() - .map(|(mode_family, count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family, - count, - } - }) - .collect(), - sample_rows: rows - .iter() - .take(8) - .enumerate() - .map( - |( - sample_index, - ( - name_tag_relative_offset, - primary_name, - secondary_name, - previous_profile_chunk_len_to_next_name_or_end, - ), - )| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSample { - sample_index, - name_tag_relative_offset: *name_tag_relative_offset, - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - child_count_candidate, - child_count_candidate_hex: format!( - "0x{child_count_candidate:04x}" - ), - saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - previous_profile_chunk_len_to_next_name_or_end: - *previous_profile_chunk_len_to_next_name_or_end, - } - }, - ) - .collect(), - } - }, - ) - .take(8) - .collect::>(); - let mut name_prelude_profile_span_groups = BTreeMap::< - usize, - Vec<(usize, Option, Option, u32, u16, u8, u16, u8)>, - >::new(); - for ( - name_tag_relative_offset, - primary_name, - secondary_name, - prefix_leading_dword, - prefix_trailing_word, - prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - previous_span, - _previous_short_profile_flag_pair, - ) in &name_prelude_candidate_rows - { - if let Some(previous_span) = previous_span { - name_prelude_profile_span_groups - .entry(*previous_span) - .or_default() - .push(( - *name_tag_relative_offset, - primary_name.clone(), - secondary_name.clone(), - *prefix_leading_dword, - *prefix_trailing_word, - *prefix_separator_byte, - *child_count_candidate, - *saved_primary_child_byte_candidate, - )); - } - } - let profile_span_correlations = name_prelude_profile_span_groups - .into_iter() - .map(|(previous_profile_chunk_len_to_next_name_or_end, rows)| { - let mut pattern_counts = BTreeMap::<(u16, u8), usize>::new(); - let mut child_count_counts = BTreeMap::::new(); - let mut saved_primary_counts = BTreeMap::::new(); - let mut mode_family_counts = BTreeMap::::new(); - let mut prefix_counts = BTreeMap::<(u32, u16, u8), usize>::new(); - for ( - _name_tag_relative_offset, - primary_name, - secondary_name, - prefix_leading_dword, - prefix_trailing_word, - prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - ) in &rows - { - *pattern_counts - .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) - .or_default() += 1; - *child_count_counts - .entry(*child_count_candidate) - .or_default() += 1; - *saved_primary_counts - .entry(*saved_primary_child_byte_candidate) - .or_default() += 1; - *prefix_counts - .entry(( - *prefix_leading_dword, - *prefix_trailing_word, - *prefix_separator_byte, - )) - .or_default() += 1; - *mode_family_counts - .entry( - classify_side_buffer_mode_family( - primary_name.as_deref(), - secondary_name.as_deref(), - ) - .to_string(), - ) - .or_default() += 1; - } - let dominant_pattern = pattern_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map( - |((child_count_candidate, saved_primary_child_byte_candidate), count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), - saved_primary_child_byte_candidate: - *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - count: *count, - } - }, - ); - let dominant_child_count = child_count_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(value, count)| (*value, *count)); - let dominant_saved_primary = saved_primary_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(value, count)| (*value, *count)); - let dominant_mode_family = mode_family_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(mode_family, count)| (mode_family.clone(), *count)); - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { - previous_profile_chunk_len_to_next_name_or_end, - row_count: rows.len(), - dominant_child_count_candidate: dominant_child_count.map(|(value, _)| value), - dominant_child_count_candidate_count: dominant_child_count - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_saved_primary_child_byte_candidate: dominant_saved_primary - .map(|(value, _)| value), - dominant_saved_primary_child_byte_candidate_hex: dominant_saved_primary - .map(|(value, _)| format!("0x{value:02x}")), - dominant_saved_primary_child_byte_candidate_count: dominant_saved_primary - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_candidate_pattern: dominant_pattern, - dominant_mode_family: dominant_mode_family - .as_ref() - .map(|(mode_family, _)| mode_family.clone()), - dominant_mode_family_count: dominant_mode_family - .map(|(_, count)| count) - .unwrap_or_default(), - mode_family_counts: mode_family_counts - .into_iter() - .map(|(mode_family, count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family, - count, - } - }) - .collect(), - compact_prefix_pattern_summaries: prefix_counts - .into_iter() - .map( - |( - (prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), - count, - )| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { - prefix_leading_dword, - prefix_leading_dword_hex: format!( - "0x{prefix_leading_dword:08x}" - ), - prefix_trailing_word, - prefix_trailing_word_hex: format!( - "0x{prefix_trailing_word:04x}" - ), - prefix_separator_byte, - prefix_separator_byte_hex: format!( - "0x{prefix_separator_byte:02x}" - ), - count, - } - }, - ) - .collect(), - sample_rows: rows - .iter() - .take(8) - .enumerate() - .map( - |( - sample_index, - ( - name_tag_relative_offset, - primary_name, - secondary_name, - prefix_leading_dword, - prefix_trailing_word, - prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - ), - )| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { - sample_index, - name_tag_relative_offset: *name_tag_relative_offset, - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - prefix_leading_dword: *prefix_leading_dword, - prefix_leading_dword_hex: format!( - "0x{prefix_leading_dword:08x}" - ), - prefix_trailing_word: *prefix_trailing_word, - prefix_trailing_word_hex: format!( - "0x{prefix_trailing_word:04x}" - ), - prefix_separator_byte: *prefix_separator_byte, - prefix_separator_byte_hex: format!( - "0x{prefix_separator_byte:02x}" - ), - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: format!( - "0x{child_count_candidate:04x}" - ), - saved_primary_child_byte_candidate: - *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - } - }, - ) - .collect(), - } - }) - .take(8) - .collect::>(); - let mut name_prelude_compact_prefix_groups = BTreeMap::< - (u32, u16, u8), - Vec<( - usize, - Option, - Option, - u16, - u8, - Option, - Option<(u8, u8)>, - )>, - >::new(); - for ( - name_tag_relative_offset, - primary_name, - secondary_name, - prefix_leading_dword, - prefix_trailing_word, - prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - previous_span, - previous_short_profile_flag_pair, - ) in &name_prelude_candidate_rows - { - name_prelude_compact_prefix_groups - .entry(( - *prefix_leading_dword, - *prefix_trailing_word, - *prefix_separator_byte, - )) - .or_default() - .push(( - *name_tag_relative_offset, - primary_name.clone(), - secondary_name.clone(), - *child_count_candidate, - *saved_primary_child_byte_candidate, - *previous_span, - *previous_short_profile_flag_pair, - )); - } - let compact_prefix_correlations = name_prelude_compact_prefix_groups - .into_iter() - .map( - |( - (prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), - rows, - )| { - let mut name_pair_counts = - BTreeMap::<(Option, Option), usize>::new(); - let mut profile_span_counts = BTreeMap::::new(); - let mut candidate_pattern_counts = BTreeMap::<(u16, u8), usize>::new(); - let mut mode_family_counts = BTreeMap::::new(); - let mut flag_pair_counts = BTreeMap::<(u8, u8), usize>::new(); - for ( - _name_tag_relative_offset, - primary_name, - secondary_name, - child_count_candidate, - saved_primary_child_byte_candidate, - previous_span, - previous_short_profile_flag_pair, - ) in &rows - { - *name_pair_counts - .entry((primary_name.clone(), secondary_name.clone())) - .or_default() += 1; - if let Some(previous_span) = previous_span { - *profile_span_counts.entry(*previous_span).or_default() += 1; - } - *candidate_pattern_counts - .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) - .or_default() += 1; - *mode_family_counts - .entry( - classify_side_buffer_mode_family( - primary_name.as_deref(), - secondary_name.as_deref(), - ) - .to_string(), - ) - .or_default() += 1; - if let Some((first_flag_byte, second_flag_byte)) = - previous_short_profile_flag_pair - { - *flag_pair_counts - .entry((*first_flag_byte, *second_flag_byte)) - .or_default() += 1; - } - } - let dominant_name_pair = name_pair_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|((primary_name, secondary_name), count)| { - (primary_name.clone(), secondary_name.clone(), *count) - }); - let dominant_profile_span = profile_span_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(span, count)| (*span, *count)); - let dominant_candidate_pattern = candidate_pattern_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map( - |( - (child_count_candidate, saved_primary_child_byte_candidate), - count, - )| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: format!( - "0x{child_count_candidate:04x}" - ), - saved_primary_child_byte_candidate: - *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - count: *count, - } - }, - ); - let dominant_mode_family = mode_family_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(mode_family, count)| (mode_family.clone(), *count)); - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { - prefix_leading_dword, - prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), - prefix_trailing_word, - prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), - prefix_separator_byte, - prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), - row_count: rows.len(), - unique_name_pair_count: name_pair_counts.len(), - unique_profile_span_count: profile_span_counts.len(), - dominant_primary_name: dominant_name_pair - .as_ref() - .and_then(|(primary_name, _, _)| primary_name.clone()), - dominant_secondary_name: dominant_name_pair - .as_ref() - .and_then(|(_, secondary_name, _)| secondary_name.clone()), - dominant_name_pair_count: dominant_name_pair - .map(|(_, _, count)| count) - .unwrap_or_default(), - dominant_profile_span: dominant_profile_span - .map(|(profile_span, _)| profile_span), - dominant_profile_span_count: dominant_profile_span - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_candidate_pattern, - dominant_mode_family: dominant_mode_family - .as_ref() - .map(|(mode_family, _)| mode_family.clone()), - dominant_mode_family_count: dominant_mode_family - .map(|(_, count)| count) - .unwrap_or_default(), - mode_family_counts: mode_family_counts - .into_iter() - .map(|(mode_family, count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family, - count, - } - }) - .collect(), - name_pair_summaries: name_pair_counts - .into_iter() - .map(|((primary_name, secondary_name), count)| { - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { - primary_name, - secondary_name, - count, - } - }) - .collect(), - profile_span_counts: profile_span_counts - .into_iter() - .map( - |(previous_profile_chunk_len_to_next_name_or_end, count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { - previous_profile_chunk_len_to_next_name_or_end, - count, - } - }, - ) - .collect(), - rows_with_previous_short_profile_flag_pair: flag_pair_counts - .values() - .copied() - .sum(), - previous_short_profile_flag_pair_counts: flag_pair_counts - .into_iter() - .map(|((first_flag_byte, second_flag_byte), count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { - first_flag_byte, - first_flag_byte_hex: format!("0x{first_flag_byte:02x}"), - second_flag_byte, - second_flag_byte_hex: format!("0x{second_flag_byte:02x}"), - count, - } - }) - .collect(), - sample_rows: rows - .iter() - .take(8) - .enumerate() - .map( - |( - sample_index, - ( - name_tag_relative_offset, - primary_name, - secondary_name, - child_count_candidate, - saved_primary_child_byte_candidate, - previous_profile_chunk_len_to_next_name_or_end, - _previous_short_profile_flag_pair, - ), - )| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { - sample_index, - name_tag_relative_offset: *name_tag_relative_offset, - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: format!( - "0x{child_count_candidate:04x}" - ), - saved_primary_child_byte_candidate: - *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - previous_profile_chunk_len_to_next_name_or_end: - *previous_profile_chunk_len_to_next_name_or_end, - } - }, - ) - .collect(), - } - }, - ) - .take(8) - .collect::>(); - let name_prelude_candidate_summary = Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSummary { - row_count_with_candidate_window: name_prelude_candidate_rows.len(), - unique_candidate_pattern_count: name_prelude_candidate_pattern_counts.len(), - dominant_child_count_candidate: dominant_name_prelude_child_count - .map(|(value, _)| value), - dominant_child_count_candidate_count: dominant_name_prelude_child_count - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_saved_primary_child_byte_candidate: dominant_name_prelude_saved_primary - .map(|(value, _)| value), - dominant_saved_primary_child_byte_candidate_hex: - dominant_name_prelude_saved_primary.map(|(value, _)| format!("0x{value:02x}")), - dominant_saved_primary_child_byte_candidate_count: - dominant_name_prelude_saved_primary - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_candidate_pattern: dominant_name_prelude_candidate_pattern.clone(), - candidate_pattern_correlations, - profile_span_correlations, - compact_prefix_correlations, - sample_rows: name_prelude_candidate_rows - .iter() - .take(8) - .enumerate() - .map( - |( - sample_index, - ( - name_tag_relative_offset, - primary_name, - secondary_name, - _prefix_leading_dword, - _prefix_trailing_word, - _prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - previous_profile_chunk_len_to_next_name_or_end, - _previous_short_profile_flag_pair, - ), - )| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSample { - sample_index, - name_tag_relative_offset: *name_tag_relative_offset, - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: format!("0x{child_count_candidate:04x}"), - saved_primary_child_byte_candidate: - *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - previous_profile_chunk_len_to_next_name_or_end: - *previous_profile_chunk_len_to_next_name_or_end, - } - }, - ) - .collect(), - }, - ); - let dominant_profile_span_class_summary = dominant_profile_chunk_len - .map(|(dominant_profile_span_len, _)| { - let dominant_rows = embedded_name_rows - .iter() - .zip(payload_envelope_rows.iter()) - .filter_map(|(name_row, envelope_row)| { - (envelope_row.profile_chunk_len_to_next_name_or_end - == Some(dominant_profile_span_len)) - .then(|| { - let candidate_offset = - name_row.name_tag_relative_offset.checked_sub(3); - let child_count_candidate = candidate_offset - .and_then(|offset| read_u16_at(records_payload, offset)); - let saved_primary_child_byte_candidate = candidate_offset - .and_then(|offset| read_u8_at(records_payload, offset + 2)); - ( - name_row.name_tag_relative_offset, - name_row.primary_name.clone(), - name_row.secondary_name.clone(), - name_row.prefix_leading_dword, - name_row.prefix_trailing_word, - name_row.prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - ) - }) - }) - .collect::>(); - let mut dominant_name_pair_counts = - BTreeMap::<(Option, Option), usize>::new(); - let mut dominant_prefix_counts = BTreeMap::<(u32, u16, u8), usize>::new(); - let mut dominant_candidate_pattern_counts = BTreeMap::<(u16, u8), usize>::new(); - for ( - _, - primary_name, - secondary_name, - prefix_leading_dword, - prefix_trailing_word, - prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - ) in &dominant_rows - { - *dominant_name_pair_counts - .entry((primary_name.clone(), secondary_name.clone())) - .or_default() += 1; - *dominant_prefix_counts - .entry(( - *prefix_leading_dword, - *prefix_trailing_word, - *prefix_separator_byte, - )) - .or_default() += 1; - if let (Some(child_count_candidate), Some(saved_primary_child_byte_candidate)) = - (child_count_candidate, saved_primary_child_byte_candidate) - { - *dominant_candidate_pattern_counts - .entry((*child_count_candidate, *saved_primary_child_byte_candidate)) - .or_default() += 1; - } - } - let dominant_name_pair = dominant_name_pair_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|((primary_name, secondary_name), count)| { - (primary_name.clone(), secondary_name.clone(), *count) - }); - let dominant_prefix = dominant_prefix_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map( - |((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), count)| { - ( - *prefix_leading_dword, - *prefix_trailing_word, - *prefix_separator_byte, - *count, - ) - }, - ); - let dominant_candidate_pattern = dominant_candidate_pattern_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map( - |((child_count_candidate, saved_primary_child_byte_candidate), count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: format!( - "0x{child_count_candidate:04x}" - ), - saved_primary_child_byte_candidate: - *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - count: *count, - } - }, - ); - let name_pair_summaries = dominant_name_pair_counts - .iter() - .map(|((primary_name, secondary_name), count)| { - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - count: *count, - } - }) - .take(8) - .collect::>(); - let compact_prefix_pattern_summaries = dominant_prefix_counts - .iter() - .map( - |((prefix_leading_dword, prefix_trailing_word, prefix_separator_byte), count)| { - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanPrefixSummary { - prefix_leading_dword: *prefix_leading_dword, - prefix_leading_dword_hex: format!( - "0x{prefix_leading_dword:08x}" - ), - prefix_trailing_word: *prefix_trailing_word, - prefix_trailing_word_hex: format!( - "0x{prefix_trailing_word:04x}" - ), - prefix_separator_byte: *prefix_separator_byte, - prefix_separator_byte_hex: format!( - "0x{prefix_separator_byte:02x}" - ), - count: *count, - } - }, - ) - .take(8) - .collect::>(); - let candidate_pattern_summaries = dominant_candidate_pattern_counts - .iter() - .map( - |((child_count_candidate, saved_primary_child_byte_candidate), count)| { - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: format!( - "0x{child_count_candidate:04x}" - ), - saved_primary_child_byte_candidate: - *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: format!( - "0x{saved_primary_child_byte_candidate:02x}" - ), - count: *count, - } - }, - ) - .take(8) - .collect::>(); - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanClassSummary { - profile_chunk_len_to_next_name_or_end: dominant_profile_span_len, - row_count: dominant_rows.len(), - unique_name_pair_count: dominant_name_pair_counts.len(), - unique_compact_prefix_pattern_count: dominant_prefix_counts.len(), - dominant_candidate_pattern, - dominant_primary_name: dominant_name_pair - .as_ref() - .and_then(|(primary_name, _, _)| primary_name.clone()), - dominant_secondary_name: dominant_name_pair - .as_ref() - .and_then(|(_, secondary_name, _)| secondary_name.clone()), - dominant_name_pair_count: dominant_name_pair - .map(|(_, _, count)| count) - .unwrap_or_default(), - dominant_prefix_leading_dword: dominant_prefix - .map(|(prefix_leading_dword, _, _, _)| prefix_leading_dword), - dominant_prefix_leading_dword_hex: dominant_prefix.map( - |(prefix_leading_dword, _, _, _)| format!("0x{prefix_leading_dword:08x}"), - ), - dominant_prefix_trailing_word: dominant_prefix - .map(|(_, prefix_trailing_word, _, _)| prefix_trailing_word), - dominant_prefix_trailing_word_hex: dominant_prefix.map( - |(_, prefix_trailing_word, _, _)| format!("0x{prefix_trailing_word:04x}"), - ), - dominant_prefix_separator_byte: dominant_prefix - .map(|(_, _, prefix_separator_byte, _)| prefix_separator_byte), - dominant_prefix_separator_byte_hex: dominant_prefix.map( - |(_, _, prefix_separator_byte, _)| format!("0x{prefix_separator_byte:02x}"), - ), - dominant_prefix_count: dominant_prefix - .map(|(_, _, _, count)| count) - .unwrap_or_default(), - sample_rows: dominant_rows - .iter() - .take(8) - .enumerate() - .map( - |( - sample_index, - ( - name_tag_relative_offset, - primary_name, - secondary_name, - prefix_leading_dword, - prefix_trailing_word, - prefix_separator_byte, - child_count_candidate, - saved_primary_child_byte_candidate, - ), - )| { - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanSample { - sample_index, - name_tag_relative_offset: *name_tag_relative_offset, - primary_name: primary_name.clone(), - secondary_name: secondary_name.clone(), - prefix_leading_dword: *prefix_leading_dword, - prefix_leading_dword_hex: format!( - "0x{prefix_leading_dword:08x}" - ), - prefix_trailing_word: *prefix_trailing_word, - prefix_trailing_word_hex: format!( - "0x{prefix_trailing_word:04x}" - ), - prefix_separator_byte: *prefix_separator_byte, - prefix_separator_byte_hex: format!( - "0x{prefix_separator_byte:02x}" - ), - child_count_candidate: *child_count_candidate, - child_count_candidate_hex: child_count_candidate - .map(|value| format!("0x{value:04x}")), - saved_primary_child_byte_candidate: - *saved_primary_child_byte_candidate, - saved_primary_child_byte_candidate_hex: - saved_primary_child_byte_candidate - .map(|value| format!("0x{value:02x}")), - } - }, - ) - .collect(), - name_pair_summaries, - compact_prefix_pattern_summaries, - candidate_pattern_summaries, - } - }); - let payload_envelope_summary = Some( - SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSummary { - row_count_with_policy_tag_before_next_name, - row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, - row_count_missing_policy_tag_before_next_name, - row_count_missing_profile_tag_after_policy, - unique_policy_chunk_lens, - unique_profile_chunk_lens, - dominant_policy_chunk_len: dominant_policy_chunk_len.map(|(len, _)| len), - dominant_policy_chunk_len_count: dominant_policy_chunk_len - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_profile_chunk_len: dominant_profile_chunk_len.map(|(len, _)| len), - dominant_profile_chunk_len_count: dominant_profile_chunk_len - .map(|(_, count)| count) - .unwrap_or_default(), - short_profile_flag_pair_summary: short_profile_flag_pair_summary.clone(), - fixed_policy_summary: fixed_policy_summary.clone(), - name_prelude_candidate_summary: name_prelude_candidate_summary.clone(), - dominant_profile_span_class_summary: dominant_profile_span_class_summary.clone(), - sample_rows: payload_envelope_rows - .iter() - .take(8) - .enumerate() - .map(|(sample_index, row)| { - SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSample { - sample_index, - name_tag_relative_offset: row.name_tag_relative_offset, - primary_name: row.primary_name.clone(), - secondary_name: row.secondary_name.clone(), - name_payload_end_relative_offset: row.name_payload_end_relative_offset, - policy_tag_relative_offset: row.policy_tag_relative_offset, - profile_tag_relative_offset: row.profile_tag_relative_offset, - next_name_tag_relative_offset: row.next_name_tag_relative_offset, - name_to_policy_gap_len: row.name_to_policy_gap_len, - policy_chunk_len: row.policy_chunk_len, - profile_chunk_len_to_next_name_or_end: row - .profile_chunk_len_to_next_name_or_end, - } - }) - .collect(), - }, - ); - let bitset_len = ((usize::try_from(summary.live_id_bound).ok()?).saturating_add(8)) / 8; - let live_entry_prelude_summary = payload - .get( - INDEXED_COLLECTION_SERIALIZED_HEADER_LEN - ..INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + bitset_len, - ) - .and_then(|bitset| { - let live_entry_ids = - decode_live_entry_ids_from_tombstone_bitset(bitset, summary.live_id_bound)?; - if live_entry_ids.len() != usize::try_from(summary.live_record_count).ok()? { - return None; - } - #[derive(Clone)] - struct LiveEntryPreludeRow { - live_entry_id: u32, - payload_relative_offset: usize, - child_count: u16, - saved_primary_child_byte: u8, - first_payload_dword: u32, - first_name_tag_relative_offset: Option, - first_primary_name: Option, - first_secondary_name: Option, - first_tertiary_name: Option, - } - let mut rows = Vec::new(); - let mut cursor = 0usize; - for live_entry_id in live_entry_ids.iter().copied() { - let payload_len = usize::from(read_u16_at(records_payload, cursor)?); - let payload_relative_offset = cursor.checked_add(2)?; - let payload_end = payload_relative_offset.checked_add(payload_len)?; - let payload_bytes = - records_payload.get(payload_relative_offset..payload_end)?; - let child_count = read_u16_at(payload_bytes, 0)?; - let saved_primary_child_byte = read_u8_at(payload_bytes, 2)?; - let first_payload_dword = read_u32_at(payload_bytes, 0)?; - let first_name_tag_relative_offset = (0usize..=16usize).find(|offset| { - read_u16_at(payload_bytes, *offset) == Some(SAVE_REGION_RECORD_NAME_TAG) - }); - let (first_primary_name, first_secondary_name, first_tertiary_name) = - first_name_tag_relative_offset - .and_then(|relative_offset| { - let name_payload = - payload_bytes.get(relative_offset.checked_add(4)?..)?; - parse_save_len_prefixed_ascii_name_triplet(name_payload) - }) - .unwrap_or_default(); - rows.push(LiveEntryPreludeRow { - live_entry_id, - payload_relative_offset, - child_count, - saved_primary_child_byte, - first_payload_dword, - first_name_tag_relative_offset, - first_primary_name: (!first_primary_name.is_empty()) - .then_some(first_primary_name), - first_secondary_name: (!first_secondary_name.is_empty()) - .then_some(first_secondary_name), - first_tertiary_name, - }); - cursor = payload_end; - } - let payload_relative_offset_monotonic = rows.windows(2).all(|window| { - window[0].payload_relative_offset < window[1].payload_relative_offset - }); - let mut child_count_counts = BTreeMap::::new(); - let mut saved_primary_child_byte_counts = BTreeMap::::new(); - let mut first_name_tag_relative_offset_counts = BTreeMap::::new(); - for row in &rows { - *child_count_counts.entry(row.child_count).or_default() += 1; - *saved_primary_child_byte_counts - .entry(row.saved_primary_child_byte) - .or_default() += 1; - if let Some(first_name_tag_relative_offset) = row.first_name_tag_relative_offset - { - *first_name_tag_relative_offset_counts - .entry(first_name_tag_relative_offset) - .or_default() += 1; - } - } - let dominant_child_count = child_count_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(value, count)| (*value, *count)); - let dominant_saved_primary_child_byte = saved_primary_child_byte_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(value, count)| (*value, *count)); - let dominant_first_name_tag_relative_offset = first_name_tag_relative_offset_counts - .iter() - .max_by(|(left_key, left_count), (right_key, right_count)| { - left_count - .cmp(right_count) - .then_with(|| right_key.cmp(left_key)) - }) - .map(|(value, count)| (*value, *count)); - Some( - SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSummary { - live_entry_directory_row_count: rows.len(), - decoded_live_entry_id_count: live_entry_ids.len(), - payload_relative_offset_monotonic, - rows_with_payload_pointer_inside_records_span: rows.len(), - rows_with_zero_child_count: rows - .iter() - .filter(|row| row.child_count == 0) - .count(), - rows_with_nonzero_child_count: rows - .iter() - .filter(|row| row.child_count != 0) - .count(), - rows_with_first_name_tag_after_prelude: rows - .iter() - .filter(|row| row.first_name_tag_relative_offset.is_some()) - .count(), - rows_with_first_name_tag_at_offset_3: rows - .iter() - .filter(|row| row.first_name_tag_relative_offset == Some(3)) - .count(), - unique_child_count_values: child_count_counts.keys().copied().collect(), - unique_first_name_tag_relative_offsets: - first_name_tag_relative_offset_counts - .keys() - .copied() - .collect(), - dominant_child_count: dominant_child_count.map(|(value, _)| value), - dominant_child_count_count: dominant_child_count - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_saved_primary_child_byte: dominant_saved_primary_child_byte - .map(|(value, _)| value), - dominant_saved_primary_child_byte_hex: dominant_saved_primary_child_byte - .map(|(value, _)| format!("0x{value:02x}")), - dominant_saved_primary_child_byte_count: dominant_saved_primary_child_byte - .map(|(_, count)| count) - .unwrap_or_default(), - dominant_first_name_tag_relative_offset: - dominant_first_name_tag_relative_offset.map(|(value, _)| value), - dominant_first_name_tag_relative_offset_count: - dominant_first_name_tag_relative_offset - .map(|(_, count)| count) - .unwrap_or_default(), - sample_rows: rows - .iter() - .take(8) - .enumerate() - .map(|(sample_index, row)| { - SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSample { - sample_index, - live_entry_id: row.live_entry_id, - payload_relative_offset: row.payload_relative_offset as u32, - payload_relative_offset_hex: format!( - "0x{:08x}", - row.payload_relative_offset - ), - payload_relative_to_records: row.payload_relative_offset, - child_count: row.child_count, - child_count_hex: format!("0x{:04x}", row.child_count), - saved_primary_child_byte: row.saved_primary_child_byte, - saved_primary_child_byte_hex: format!( - "0x{:02x}", - row.saved_primary_child_byte - ), - first_payload_dword_hex: format!( - "0x{:08x}", - row.first_payload_dword - ), - first_name_tag_relative_offset: row - .first_name_tag_relative_offset, - first_primary_name: row.first_primary_name.clone(), - first_secondary_name: row.first_secondary_name.clone(), - first_tertiary_name: row.first_tertiary_name.clone(), - } - }) - .collect(), - }, - ) - }); - let unique_embedded_name_pair_count = name_pair_summaries.len(); - let dominant_compact_prefix_pattern = compact_prefix_pattern_summaries.first().cloned(); - let decoded_embedded_name_row_count = embedded_name_rows - .iter() - .filter(|row| row.primary_name.is_some() && row.secondary_name.is_some()) - .count(); - let decoded_embedded_name_row_with_tertiary_name_count = embedded_name_rows - .iter() - .filter(|row| { - row.primary_name.is_some() - && row.secondary_name.is_some() - && row.tertiary_name.is_some() - }) - .count(); - return Some(SmpSavePlacedStructureDynamicSideBufferProbe { - profile_family: profile.profile_family.clone(), - source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(), - semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records".to_string(), - metadata_tag_offset, - records_tag_offset, - close_tag_offset, - records_span_len: close_tag_offset.saturating_sub(records_tag_offset + 4), - direct_record_stride: summary.direct_record_stride, - direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride), - live_id_bound: summary.live_id_bound, - live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound), - live_record_count: summary.live_record_count, - live_record_count_hex: format!("0x{:08x}", summary.live_record_count), - owner_shared_dword, - owner_shared_dword_hex: format!("0x{owner_shared_dword:08x}"), - owner_shared_dword_relative_offset, - owner_shared_dword_matches_first_compact_prefix_leading_dword: - owner_shared_dword == prefix_leading_dword, - first_record_child_count_after_owner_shared, - first_record_child_count_after_owner_shared_hex: first_record_child_count_after_owner_shared - .map(|value| format!("0x{value:04x}")), - first_record_saved_primary_child_byte_after_owner_shared, - first_record_saved_primary_child_byte_after_owner_shared_hex: - first_record_saved_primary_child_byte_after_owner_shared - .map(|value| format!("0x{value:02x}")), - first_record_first_name_tag_relative_offset_after_owner_shared, - prefix_leading_dword, - prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"), - prefix_trailing_word, - prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"), - prefix_separator_byte, - prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"), - first_embedded_name_tag_relative_offset, - embedded_name_tag_count: embedded_name_tag_offsets.len(), - decoded_embedded_name_row_count, - decoded_embedded_name_row_with_tertiary_name_count, - unique_compact_prefix_pattern_count: compact_prefix_pattern_summaries.len(), - prefix_leading_dword_matching_embedded_profile_tag_count, - unique_embedded_name_pair_count, - first_embedded_primary_name: Some(first_embedded_primary_name.clone()), - first_embedded_secondary_name: Some(first_embedded_secondary_name.clone()), - first_embedded_tertiary_name: first_embedded_tertiary_name.clone(), - embedded_name_row_samples, - compact_prefix_pattern_summaries, - name_pair_summaries, - payload_envelope_summary, - live_entry_prelude_summary: live_entry_prelude_summary.clone(), - evidence: vec![ - "exact little-endian u32 tag family 0x38a5/0x38a6/0x38a7 appears as a separate save-side tagged collection on grounded saves".to_string(), - format!( - "direct disassembly now shows 0x00493be0 consuming shared owner-local dword 0x{owner_shared_dword:08x} from the 0x38a6 stream before iterating live infrastructure records" - ), - format!( - "grounded 0x38a6 record stream then starts the first infrastructure payload with child_count={}, saved_primary_child_byte={}, and first 0x55f1 at relative offset {:?} after that shared dword", - first_record_child_count_after_owner_shared.unwrap_or_default(), - first_record_saved_primary_child_byte_after_owner_shared - .map(|value| format!("0x{value:02x}")) - .as_deref() - .unwrap_or("0x00"), - first_record_first_name_tag_relative_offset_after_owner_shared - ), - "records payload begins with a compact 6-byte prefix plus one separator byte before the first embedded 0x55f1 name row".to_string(), - "first embedded 0x55f1 row decodes with placed-structure-style dual names, which makes this the strongest current candidate for the separate placed-structure dynamic side-buffer owner seam".to_string(), - format!( - "grounded first embedded names are {:?}/{:?}/{:?} with {} embedded 0x55f1 name rows in the tagged records span", - Some(first_embedded_primary_name), - Some(first_embedded_secondary_name), - first_embedded_tertiary_name, - embedded_name_tag_offsets.len() - ), - format!( - "{} of {} embedded name rows use compact leading dword 0x{:08x}, matching the placed-structure embedded profile tag", - prefix_leading_dword_matching_embedded_profile_tag_count, - embedded_name_rows.len(), - u32::from(SAVE_REGION_RECORD_PROFILE_TAG) - ), - format!( - "{decoded_embedded_name_row_count} decoded embedded name rows collapse into {} unique placed-structure name pairs; {} rows also expose a third embedded 0x55f1 string", - unique_embedded_name_pair_count - , - decoded_embedded_name_row_with_tertiary_name_count - ), - format!( - "{} of {} embedded 0x55f1 rows currently have a complete 0x55f1/0x55f2/0x55f3 envelope before the next name row; {} rows are still missing 0x55f2 and {} rows have 0x55f2 without a later 0x55f3", - row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, - embedded_name_rows.len(), - row_count_missing_policy_tag_before_next_name, - row_count_missing_profile_tag_after_policy - ), - dominant_policy_chunk_len - .map(|(policy_chunk_len, count)| { - format!( - "dominant embedded 0x55f2 policy chunk length is 0x{policy_chunk_len:x} bytes across {count} rows" - ) - }) - .unwrap_or_else(|| { - "no dominant embedded 0x55f2 policy chunk length was available" - .to_string() - }), - dominant_profile_chunk_len - .map(|(profile_chunk_len, count)| { - format!( - "dominant embedded 0x55f3 payload-to-next-name span is 0x{profile_chunk_len:x} bytes across {count} rows" - ) - }) - .unwrap_or_else(|| { - "no dominant embedded 0x55f3 payload-to-next-name span was available" - .to_string() - }), - short_profile_flag_pair_summary - .as_ref() - .and_then(|summary| summary.dominant_flag_pair.as_ref()) - .map(|pair| { - format!( - "direct disassembly now bounds the short trailing lane through 0x52ebd0/0x52ec50 as two serialized flag bytes folded into [this+0x20] bits 0x20/0x40; grounded 0x06-byte rows currently favor {}/{} across {} rows ({} rows total with the short span)", - pair.first_flag_byte_hex, - pair.second_flag_byte_hex, - pair.count, - short_profile_flag_pair_summary - .as_ref() - .map(|summary| summary.row_count_with_0x06_profile_span) - .unwrap_or_default() - ) - }) - .unwrap_or_else(|| { - "no dominant short trailing flag-byte pair was available".to_string() - }), - fixed_policy_summary - .as_ref() - .map(|summary| { - format!( - "direct disassembly now bounds the fixed 0x55f2 lane through 0x455870/0x455930 as six serialized dwords plus one trailing u16; grounded rows currently keep trailing word {} across {} of {} fixed-policy rows", - summary - .dominant_trailing_word_hex - .as_deref() - .unwrap_or("0x0000"), - summary.dominant_trailing_word_count, - summary.row_count_with_0x1a_policy_chunk - ) - }) - .unwrap_or_else(|| { - "no fixed 0x55f2 policy summary was available".to_string() - }), - live_entry_prelude_summary - .as_ref() - .map(|summary| { - format!( - "decoded {} live-entry directory rows with payload pointers inside the records span; dominant child count={} x{}, dominant saved primary-child byte={} x{}, dominant first 0x55f1 offset={} x{}, and {} rows start the first child callback immediately at payload +0x3", - summary.rows_with_payload_pointer_inside_records_span, - summary.dominant_child_count.unwrap_or_default(), - summary.dominant_child_count_count, - summary - .dominant_saved_primary_child_byte_hex - .as_deref() - .unwrap_or("0x00"), - summary.dominant_saved_primary_child_byte_count, - summary - .dominant_first_name_tag_relative_offset - .unwrap_or_default(), - summary.dominant_first_name_tag_relative_offset_count, - summary.rows_with_first_name_tag_at_offset_3 - ) - }) - .unwrap_or_else(|| { - "no live-entry prelude summary was available".to_string() - }), - dominant_compact_prefix_pattern - .map(|pattern| { - format!( - "dominant compact prefix pattern {} / {} / {} occurs {} times; section-like rows={}, cap-like rows={}, first names={:?}/{:?}", - pattern.prefix_leading_dword_hex, - pattern.prefix_trailing_word_hex, - pattern.prefix_separator_byte_hex, - pattern.count, - pattern.section_like_primary_name_count, - pattern.cap_like_primary_name_count, - pattern.first_primary_name, - pattern.first_secondary_name - ) - }) - .unwrap_or_else(|| { - "no dominant compact prefix pattern summary was available".to_string() - }), - ], - }); - } - None -} - -fn summarize_placed_structure_dynamic_side_buffer_alignment( - side_buffer: &SmpSavePlacedStructureDynamicSideBufferProbe, - triplets: &SmpSavePlacedStructureRecordTripletProbe, -) -> SmpSavePlacedStructureDynamicSideBufferAlignmentProbe { - let triplet_name_pairs = triplets - .entries - .iter() - .map(|entry| (entry.primary_name.clone(), entry.secondary_name.clone())) - .collect::>(); - let matched_name_pair_samples = side_buffer - .name_pair_summaries - .iter() - .filter(|summary| { - triplet_name_pairs - .contains(&(summary.primary_name.clone(), summary.secondary_name.clone())) - }) - .take(5) - .cloned() - .collect::>(); - let unmatched_side_buffer_name_pair_samples = side_buffer - .name_pair_summaries - .iter() - .filter(|summary| { - !triplet_name_pairs - .contains(&(summary.primary_name.clone(), summary.secondary_name.clone())) - }) - .take(5) - .cloned() - .collect::>(); - let side_buffer_rows_with_matching_triplet_name_pair_count = side_buffer - .name_pair_summaries - .iter() - .filter(|summary| { - triplet_name_pairs - .contains(&(summary.primary_name.clone(), summary.secondary_name.clone())) - }) - .map(|summary| summary.count) - .sum::(); - let unique_side_buffer_name_pair_count = side_buffer.name_pair_summaries.len(); - let overlapping_name_pair_count = side_buffer - .name_pair_summaries - .iter() - .filter(|summary| { - triplet_name_pairs - .contains(&(summary.primary_name.clone(), summary.secondary_name.clone())) - }) - .count(); - SmpSavePlacedStructureDynamicSideBufferAlignmentProbe { - unique_side_buffer_name_pair_count, - unique_triplet_name_pair_count: triplet_name_pairs.len(), - overlapping_name_pair_count, - side_buffer_row_count: side_buffer.decoded_embedded_name_row_count, - side_buffer_rows_with_matching_triplet_name_pair_count, - side_buffer_rows_without_matching_triplet_name_pair_count: side_buffer - .decoded_embedded_name_row_count - .saturating_sub(side_buffer_rows_with_matching_triplet_name_pair_count), - triplet_name_pairs_without_side_buffer_match_count: triplet_name_pairs - .len() - .saturating_sub(overlapping_name_pair_count), - matched_name_pair_samples, - unmatched_side_buffer_name_pair_samples, - evidence: vec![ - "placed-structure dynamic side-buffer alignment compares decoded 0x38a5 embedded name pairs against the grounded 0x36b1 triplet name-pair corpus".to_string(), - format!( - "{} of {} decoded side-buffer rows currently reuse name pairs already present in the placed-structure triplet owner seam", - side_buffer_rows_with_matching_triplet_name_pair_count, - side_buffer.decoded_embedded_name_row_count - ), - format!( - "{} of {} unique side-buffer name pairs overlap the grounded triplet name-pair corpus", - overlapping_name_pair_count, - unique_side_buffer_name_pair_count - ), - ], - } -} - -#[derive(Clone, Copy)] -struct IndexedCollectionHeaderSummary { - metadata_tag_offset: usize, - records_tag_offset: usize, - close_tag_offset: usize, - direct_collection_flag: u32, - direct_record_stride: u32, - live_id_bound: u32, - live_record_count: u32, - header_words: [u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT], -} - -fn parse_save_tagged_collection_header_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - metadata_tag: u32, - records_tag: u32, - close_tag: u32, - source_kind: &str, - semantic_family: &str, - predicate: impl Fn(IndexedCollectionHeaderSummary) -> bool, - mut evidence: Vec, -) -> Option { - if file_extension_hint != Some("gms") { - return None; - } - let profile = container_profile?; - if !matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ) { - return None; - } - - let metadata_offsets = find_u32_le_offsets(bytes, metadata_tag); - let records_offsets = find_u32_le_offsets(bytes, records_tag); - let close_offsets = find_u32_le_offsets(bytes, close_tag); - - let summary = metadata_offsets - .into_iter() - .filter_map(|metadata_tag_offset| { - let records_tag_offset = records_offsets - .iter() - .copied() - .find(|offset| *offset > metadata_tag_offset)?; - let close_tag_offset = close_offsets - .iter() - .copied() - .find(|offset| *offset > records_tag_offset)?; - let payload = bytes.get(metadata_tag_offset + 4..records_tag_offset)?; - if payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { - return None; - } - - let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT) - .map(|index| read_u32_at(payload, index * 4)) - .collect::>>()?; - let header_words: [u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT] = - header_words.try_into().ok()?; - let summary = IndexedCollectionHeaderSummary { - metadata_tag_offset, - records_tag_offset, - close_tag_offset, - direct_collection_flag: header_words[0], - direct_record_stride: header_words[1], - live_id_bound: header_words[4], - live_record_count: header_words[5], - header_words, - }; - predicate(summary).then_some(summary) - }) - .next()?; - - evidence.push(format!( - "exact little-endian u32 tag family 0x{metadata_tag:04x}/0x{records_tag:04x}/0x{close_tag:04x} appears at file offsets 0x{:x}/0x{:x}/0x{:x}", - summary.metadata_tag_offset, summary.records_tag_offset, summary.close_tag_offset - )); - evidence.push(format!( - "header words report direct_collection_flag={}, direct_record_stride=0x{:x}, live_id_bound={}, live_record_count={}", - summary.direct_collection_flag, - summary.direct_record_stride, - summary.live_id_bound, - summary.live_record_count - )); - - Some(SmpSaveTaggedCollectionHeaderProbe { - profile_family: profile.profile_family.clone(), - source_kind: source_kind.to_string(), - semantic_family: semantic_family.to_string(), - metadata_tag_offset: summary.metadata_tag_offset, - records_tag_offset: summary.records_tag_offset, - close_tag_offset: summary.close_tag_offset, - direct_collection_flag: summary.direct_collection_flag, - direct_collection_flag_hex: format!("0x{:08x}", summary.direct_collection_flag), - direct_record_stride: summary.direct_record_stride, - direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride), - live_id_bound: summary.live_id_bound, - live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound), - live_record_count: summary.live_record_count, - live_record_count_hex: format!("0x{:08x}", summary.live_record_count), - header_words: summary.header_words.to_vec(), - header_hex_words: summary - .header_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - evidence, - }) -} - -fn scan_save_unclassified_tagged_collection_header_probes( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Vec { - if file_extension_hint != Some("gms") { - return Vec::new(); - } - let Some(profile) = container_profile else { - return Vec::new(); - }; - if !matches!( - profile.profile_family.as_str(), - "rt3-classic-save-container-v1" - | "rt3-105-save-container-v1" - | "rt3-105-scenario-save-container-v1" - | "rt3-105-alt-save-container-v1" - ) { - return Vec::new(); - } - let known_metadata_tags = BTreeSet::from([ - RT3_SAVE_WORLD_BLOCK_CHUNK_TAG, - 0x000061a9, - 0x00005209, - 0x000036b1, - EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32, - ]); - let mut low_tag_offsets: BTreeMap> = BTreeMap::new(); - for offset in 0..bytes.len().saturating_sub(4) { - let Some(tag) = read_u32_at(bytes, offset) else { - continue; - }; - if (3..=0xffff).contains(&tag) { - low_tag_offsets.entry(tag).or_default().push(offset); - } - } - let mut probes = Vec::new(); - for (&metadata_tag, metadata_offsets) in &low_tag_offsets { - if known_metadata_tags.contains(&metadata_tag) { - continue; - } - let Some(records_offsets) = low_tag_offsets.get(&(metadata_tag + 1)) else { - continue; - }; - let Some(close_offsets) = low_tag_offsets.get(&(metadata_tag + 2)) else { - continue; - }; - let records_tag = metadata_tag + 1; - let close_tag = metadata_tag + 2; - for &metadata_tag_offset in metadata_offsets { - let mut header_words = [0u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT]; - let mut valid_header = true; - for (index, word) in header_words.iter_mut().enumerate() { - let Some(value) = read_u32_at(bytes, metadata_tag_offset + 4 + index * 4) else { - valid_header = false; - break; - }; - *word = value; - } - if !valid_header { - continue; - } - let summary = IndexedCollectionHeaderSummary { - metadata_tag_offset, - records_tag_offset: 0, - close_tag_offset: 0, - direct_collection_flag: header_words[0], - direct_record_stride: header_words[1], - live_id_bound: header_words[4], - live_record_count: header_words[5], - header_words, - }; - if !matches!(summary.direct_collection_flag, 0 | 1) - || summary.direct_record_stride == 0 - || summary.direct_record_stride > 0x2000 - || summary.live_id_bound == 0 - || summary.live_record_count == 0 - || summary.live_record_count > summary.live_id_bound - || summary.live_id_bound > 0x1000 - || summary.live_record_count > 0x1000 - { - continue; - } - let records_search_start = metadata_tag_offset + 4; - let records_index = - records_offsets.partition_point(|offset| *offset < records_search_start); - let Some(&records_tag_offset) = records_offsets.get(records_index) else { - continue; - }; - let close_search_start = records_tag_offset + 4; - let close_index = close_offsets.partition_point(|offset| *offset < close_search_start); - let Some(&close_tag_offset) = close_offsets.get(close_index) else { - continue; - }; - let records_span_len = close_tag_offset.saturating_sub(records_tag_offset + 4); - if records_span_len == 0 || records_span_len < summary.live_record_count as usize { - continue; - } - if probes - .iter() - .any(|probe: &SmpSaveUnclassifiedTaggedCollectionHeaderProbe| { - probe.metadata_tag_offset == metadata_tag_offset - && probe.records_tag_offset == records_tag_offset - && probe.close_tag_offset == close_tag_offset - }) - { - continue; - } - probes.push(SmpSaveUnclassifiedTaggedCollectionHeaderProbe { - profile_family: profile.profile_family.clone(), - source_kind: "save-unclassified-tagged-header-counts".to_string(), - semantic_family: "scenario-save-unclassified-tagged-header-counts".to_string(), - metadata_tag, - metadata_tag_hex: format!("0x{metadata_tag:08x}"), - records_tag, - records_tag_hex: format!("0x{records_tag:08x}"), - close_tag, - close_tag_hex: format!("0x{close_tag:08x}"), - metadata_tag_offset, - records_tag_offset, - close_tag_offset, - records_span_len, - direct_collection_flag: summary.direct_collection_flag, - direct_collection_flag_hex: format!("0x{:08x}", summary.direct_collection_flag), - direct_record_stride: summary.direct_record_stride, - direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride), - live_id_bound: summary.live_id_bound, - live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound), - live_record_count: summary.live_record_count, - live_record_count_hex: format!("0x{:08x}", summary.live_record_count), - header_words: summary.header_words.to_vec(), - header_hex_words: summary - .header_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect(), - evidence: vec![ - "generic save-side tagged collection scan over plausible low u32 metadata tags not yet claimed by the checked-in collection probes".to_string(), - "candidate uses adjacent metadata/records/close tags with a header that matches the grounded indexed-collection shape (flag, stride, live_id_bound, live_record_count)".to_string(), - ], - }); - } - } - probes.sort_by(|left, right| { - right - .live_record_count - .cmp(&left.live_record_count) - .then_with(|| left.metadata_tag.cmp(&right.metadata_tag)) - .then_with(|| left.metadata_tag_offset.cmp(&right.metadata_tag_offset)) - }); - probes.truncate(32); - probes -} - -fn filter_unclassified_tagged_collection_header_probes_outside_known_spans( - probes: Vec, - known_header_probes: &[Option<&SmpSaveTaggedCollectionHeaderProbe>], -) -> Vec { - probes - .into_iter() - .filter(|probe| { - !known_header_probes.iter().flatten().any(|known| { - probe.metadata_tag_offset >= known.metadata_tag_offset - && probe.close_tag_offset <= known.close_tag_offset - }) - }) - .collect() -} - -fn parse_save_len_prefixed_ascii_name(bytes: &[u8]) -> Option { - let len = *bytes.first()? as usize; - let text_bytes = bytes.get(1..1 + len)?; - let text = std::str::from_utf8(text_bytes).ok()?.trim_end_matches('\0'); - Some(text.to_string()) -} - -fn parse_save_varlen_ascii_name_at(bytes: &[u8], offset: usize) -> Option<(String, usize)> { - let first = *bytes.get(offset)?; - if first == 0 { - return None; - } - let (len, header_len) = if first <= 0x7f { - (first as usize, 1usize) - } else { - let second = *bytes.get(offset + 1)? as usize; - ((((first as usize) & 0x7f) << 8) | second, 2usize) - }; - let start = offset + header_len; - let end = start.checked_add(len)?; - let text = std::str::from_utf8(bytes.get(start..end)?) - .ok()? - .trim_end_matches('\0') - .to_string(); - Some((text, end)) -} - -fn parse_save_len_prefixed_ascii_name_pair(bytes: &[u8]) -> Option<(String, String)> { - let (first, second, _) = parse_save_len_prefixed_ascii_name_triplet(bytes)?; - Some((first, second)) -} - -fn parse_save_len_prefixed_ascii_name_triplet_and_consumed_len( - bytes: &[u8], -) -> Option<((String, String, Option), usize)> { - let (first, first_end) = parse_save_varlen_ascii_name_at(bytes, 0)?; - let mut second_len_offset = first_end; - while matches!(bytes.get(second_len_offset), Some(0)) { - second_len_offset += 1; - } - let (second, second_end) = parse_save_varlen_ascii_name_at(bytes, second_len_offset)?; - if first.is_empty() || second.is_empty() { - return None; - } - let mut third_len_offset = second_end; - while matches!(bytes.get(third_len_offset), Some(0)) { - third_len_offset += 1; - } - let (third, consumed_len) = parse_save_varlen_ascii_name_at(bytes, third_len_offset) - .map(|(text, end)| { - let third = (!text.is_empty()).then_some(text); - (third, end) - }) - .unwrap_or((None, second_end)); - Some(((first, second, third), consumed_len)) -} - -fn parse_save_len_prefixed_ascii_name_triplet( - bytes: &[u8], -) -> Option<(String, String, Option)> { - let ((first, second, third), _) = - parse_save_len_prefixed_ascii_name_triplet_and_consumed_len(bytes)?; - Some((first, second, third)) -} - -fn parse_save_fixed_ascii_name(bytes: &[u8]) -> Option { - let nul_index = bytes - .iter() - .position(|byte| *byte == 0) - .unwrap_or(bytes.len()); - let text = std::str::from_utf8(bytes.get(..nul_index)?).ok()?; - if text.is_empty() - || !text - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b' ' | b'-' | b'&' | b'/')) - { - return None; - } - Some(text.to_string()) -} - -fn parse_save_region_profile_collection_probe( - profile_payload: &[u8], -) -> Option { - let direct_collection_flag = read_u32_at(profile_payload, 0)?; - let entry_stride = read_u32_at(profile_payload, 4)?; - let header_word_2 = read_u32_at(profile_payload, 8)?; - let header_word_3 = read_u32_at(profile_payload, 12)?; - let live_id_bound = read_u32_at(profile_payload, 16)?; - let live_record_count = read_u32_at(profile_payload, 20)?; - let header_word_6 = read_u32_at(profile_payload, 24)?; - let header_word_7 = read_u32_at(profile_payload, 28)?; - if !(direct_collection_flag == 1 - && entry_stride == 0x22 - && header_word_2 == 2 - && header_word_3 == 2 - && live_record_count > 0 - && live_record_count < live_id_bound - && header_word_6 == 0 - && header_word_7 == 1) - { - return None; - } - let entry_stride = entry_stride as usize; - let live_record_count_usize = live_record_count as usize; - let rows_byte_len = live_record_count_usize.checked_mul(entry_stride)?; - let mut matched_probe = None; - for entry_start_relative_offset in 0x20..=0x80 { - if entry_start_relative_offset + rows_byte_len > profile_payload.len() { - break; - } - let mut entries = Vec::with_capacity(live_record_count_usize); - let mut matched = true; - for entry_index in 0..live_record_count_usize { - let row_relative_offset = entry_start_relative_offset + entry_index * entry_stride; - let row = - profile_payload.get(row_relative_offset..row_relative_offset + entry_stride)?; - let name = match parse_save_fixed_ascii_name(row.get(..12)?) { - Some(name) => name, - None => { - matched = false; - break; - } - }; - let trailing_weight_f32 = f32::from_bits(read_u32_at(row, entry_stride - 4)?); - if !trailing_weight_f32.is_finite() || trailing_weight_f32 < 0.0 { - matched = false; - break; - } - entries.push(SmpSaveRegionProfileEntryProbe { - entry_index, - row_relative_offset, - name, - trailing_weight_f32, - }); - } - if matched { - matched_probe = Some(SmpSaveRegionProfileCollectionProbe { - direct_collection_flag, - entry_stride: entry_stride as u32, - live_id_bound, - live_record_count, - entry_start_relative_offset, - trailing_padding_len: profile_payload.len() - - (entry_start_relative_offset + rows_byte_len), - entries, - }); - break; - } - } - matched_probe -} - -fn parse_rt3_105_save_name_table_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - bridge_payload_probe: Option<&SmpRt3105SaveBridgePayloadProbe>, -) -> Option { - let ( - profile_family, - source_kind, - header_offset, - entries_offset, - block_end_offset, - mut evidence, - ) = if let Some(payload) = bridge_payload_probe { - ( - payload.profile_family.clone(), - "save-bridge-secondary-block".to_string(), - payload.secondary_block_offset + 0x354, - payload.secondary_block_offset + 0x3b5, - payload.secondary_block_end_offset, - vec![ - "common-save bridge payload branch".to_string(), - format!( - "secondary block span 0x{:x}..0x{:x}", - payload.secondary_block_offset, payload.secondary_block_end_offset - ), - ], - ) - } else { - let profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()); - let extension = file_extension_hint.unwrap_or(""); - let source_kind = match extension { - "gmp" => "map-fixed-catalog-range", - "gms" => "save-fixed-catalog-range", - "gmx" => "sandbox-fixed-catalog-range", - _ => "fixed-catalog-range", - } - .to_string(); - ( - profile_family, - source_kind, - 0x6a70, - 0x6ad1, - 0x73c0, - vec![ - "fixed catalog range branch".to_string(), - "using observed shared 1.05 candidate-availability table offsets".to_string(), - ], - ) - }; - let entry_stride = 0x22usize; - if block_end_offset > bytes.len() { - return None; - } - if !matches_candidate_availability_table_header(bytes, header_offset) { - return None; - } - let observed_entry_capacity = read_u32_at(bytes, header_offset + 0x1c)? as usize; - let observed_entry_count = read_u32_at(bytes, header_offset + 0x20)? as usize; - let entries_len = observed_entry_count.checked_mul(entry_stride)?; - let entries_end_offset = entries_offset.checked_add(entries_len)?; - if observed_entry_count == 0 || observed_entry_capacity < observed_entry_count { - return None; - } - if entries_end_offset > block_end_offset || entries_end_offset > bytes.len() { - return None; - } - - let mut entries = Vec::with_capacity(observed_entry_count); - 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); - let text = std::str::from_utf8(&chunk[..nul_index]).ok()?.to_string(); - let trailer_word = read_u32_at(bytes, offset + entry_stride - 4)?; - entries.push(SmpRt3105SaveNameTableEntry { - index, - offset, - text, - availability_dword: trailer_word, - availability_dword_hex: format!("0x{trailer_word:08x}"), - trailer_word, - trailer_word_hex: format!("0x{trailer_word:08x}"), - }); - } - - let zero_trailer_entry_names = entries - .iter() - .filter(|entry| entry.trailer_word == 0) - .map(|entry| entry.text.clone()) - .collect::>(); - let zero_trailer_entry_count = zero_trailer_entry_names.len(); - let nonzero_trailer_entry_count = entries.len().saturating_sub(zero_trailer_entry_count); - let mut distinct_trailer_words = entries - .iter() - .map(|entry| entry.trailer_word) - .collect::>(); - distinct_trailer_words.sort_unstable(); - distinct_trailer_words.dedup(); - let distinct_trailer_hex_words = distinct_trailer_words - .iter() - .map(|word| format!("0x{word:08x}")) - .collect::>(); - let trailing_footer_hex = hex_encode(&bytes[entries_end_offset..block_end_offset]); - let footer = &bytes[entries_end_offset..block_end_offset]; - if footer.len() != 9 { - return None; - } - let footer_progress_word_0 = u32::from_le_bytes([footer[0], footer[1], footer[2], footer[3]]); - let footer_progress_word_1 = u32::from_le_bytes([footer[4], footer[5], footer[6], footer[7]]); - let footer_trailing_byte = footer[8]; - let mut footer_grounded_alignments = Vec::new(); - for value in [footer_progress_word_0, footer_progress_word_1] { - if let Some(alignment) = classify_name_table_footer_progress_alignment(value) { - footer_grounded_alignments.push(alignment.to_string()); - } - } - evidence.extend([ - format!("header offset 0x{header_offset:08x}"), - format!("entries offset 0x{entries_offset:08x}"), - format!("entry stride 0x{entry_stride:x}"), - format!("observed entry capacity {}", observed_entry_capacity), - format!("observed entry count {}", observed_entry_count), - format!("zero-trailer entries {}", zero_trailer_entry_count), - format!( - "trailing footer {} bytes after last entry", - block_end_offset - entries_end_offset - ), - ]); - let semantic_alignment = vec![ - "Matches the grounded scenario-side named candidate-availability table shape under 0x00437743.".to_string(), - "Entry layout matches 0x00434ea0/0x00434f20: name slot at +0x00..+0x1d and availability dword at +0x1e.".to_string(), - "The shared map/save range suggests this catalog is bundled in source map content and later mirrored into scenario state [state+0x66b2].".to_string(), - ]; - - Some(SmpRt3105SaveNameTableProbe { - profile_family, - source_kind, - semantic_family: "scenario-named-candidate-availability-table".to_string(), - semantic_alignment, - header_offset, - header_word_0: read_u32_at(bytes, header_offset)?, - header_word_0_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset)?), - header_word_1: read_u32_at(bytes, header_offset + 4)?, - header_word_1_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset + 4)?), - header_word_2: read_u32_at(bytes, header_offset + 8)?, - header_word_2_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset + 8)?), - entry_stride, - entry_stride_hex: format!("0x{entry_stride:x}"), - header_prefix_word_count: 11, - observed_entry_capacity, - observed_entry_count, - zero_trailer_entry_count, - nonzero_trailer_entry_count, - distinct_trailer_words, - distinct_trailer_hex_words, - zero_trailer_entry_names, - entries_offset, - entries_end_offset, - trailing_footer_hex, - footer_progress_word_0, - footer_progress_word_0_hex: format!("0x{footer_progress_word_0:08x}"), - footer_progress_word_1, - footer_progress_word_1_hex: format!("0x{footer_progress_word_1:08x}"), - footer_trailing_byte, - footer_trailing_byte_hex: format!("0x{footer_trailing_byte:02x}"), - footer_grounded_alignments, - entries, - evidence, - }) -} - -const RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE: usize = 0x41; -const RT3_105_SAVE_NAMED_LOCOMOTIVE_MIN_ENTRY_COUNT: usize = 8; -const RT3_105_SAVE_NAMED_LOCOMOTIVE_MAX_SEARCH_SPAN: usize = 0x4000; - -fn parse_rt3_105_save_named_locomotive_availability_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>, -) -> Option { - let packed_profile_probe = packed_profile_probe?; - let extension = file_extension_hint.unwrap_or(""); - let profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| packed_profile_probe.profile_family.clone()); - if !matches!(extension, "gms" | "gmx") || !profile_family.contains("save-container") { - return None; - } - - let search_start = packed_profile_probe - .packed_profile_offset - .checked_add(packed_profile_probe.packed_profile_len)?; - let search_end = search_start - .checked_add(RT3_105_SAVE_NAMED_LOCOMOTIVE_MAX_SEARCH_SPAN) - .map(|end| end.min(bytes.len())) - .unwrap_or(bytes.len()); - if search_end <= search_start + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE { - return None; - } - - let mut best_start = None; - let mut best_entries = Vec::new(); - for candidate_start in search_start..search_end { - let entries = parse_direct_named_locomotive_entries(bytes, candidate_start, search_end); - if entries.len() > best_entries.len() { - best_entries = entries; - best_start = Some(candidate_start); - } - } - - if best_entries.len() < RT3_105_SAVE_NAMED_LOCOMOTIVE_MIN_ENTRY_COUNT { - return None; - } - - let entries_offset = best_start?; - let entries_end_offset = entries_offset - .checked_add(best_entries.len() * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE)?; - let zero_availability_names = best_entries - .iter() - .filter(|entry| entry.availability_dword == 0) - .map(|entry| entry.text.clone()) - .collect::>(); - let zero_availability_count = zero_availability_names.len(); - let source_kind = match extension { - "gms" => "save-direct-locomotive-row-run", - "gmx" => "sandbox-direct-locomotive-row-run", - _ => "direct-locomotive-row-run", - } - .to_string(); - - let observed_entry_count = best_entries.len(); - - Some(SmpRt3105SaveNamedLocomotiveAvailabilityProbe { - profile_family, - source_kind, - semantic_family: "scenario-named-locomotive-availability-table".to_string(), - semantic_alignment: vec![ - "Matches the grounded `.smp` save-side locomotive-name-plus-dword row family restored into scenario state [world+0x66b6].".to_string(), - "Entry layout is one availability dword at +0x00 followed by one fixed-width locomotive name buffer at +0x04..+0x40.".to_string(), - "The recovered row order is treated conservatively as the live locomotive ordinal order later used by locomotives-page descriptor lowering.".to_string(), - ], - entries_offset, - entry_stride: RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE, - entry_stride_hex: format!("0x{:x}", RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE), - observed_entry_count, - zero_availability_count, - zero_availability_names, - entries_end_offset, - entries: best_entries, - evidence: vec![ - format!("search span 0x{search_start:08x}..0x{search_end:08x}"), - format!("entries offset 0x{entries_offset:08x}"), - format!( - "entry stride 0x{:x}", - RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE - ), - format!("observed entry count {observed_entry_count}"), - ], - }) -} - -fn parse_direct_named_locomotive_entries( - bytes: &[u8], - start_offset: usize, - search_end: usize, -) -> Vec { - let mut entries = Vec::new(); - let mut offset = start_offset; - while offset + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE <= bytes.len() && offset < search_end - { - let record = &bytes[offset..offset + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE]; - let Some(nul_index) = record[4..].iter().position(|byte| *byte == 0) else { - break; - }; - let name_bytes = &record[4..4 + nul_index]; - if name_bytes.is_empty() { - break; - } - let Ok(text) = std::str::from_utf8(name_bytes) else { - break; - }; - if !is_probable_named_locomotive_label(text) { - break; - } - if record[4 + nul_index + 1..].iter().any(|byte| *byte != 0) { - break; - } - - let availability_dword = u32::from_le_bytes([record[0], record[1], record[2], record[3]]); - entries.push(SmpRt3105SaveNameTableEntry { - index: entries.len(), - 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}"), - }); - offset += RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE; - } - entries -} - -fn is_probable_named_locomotive_label(text: &str) -> bool { - if text.is_empty() || text.len() > 40 { - return false; - } - text.bytes().all(|byte| { - byte.is_ascii_alphanumeric() || matches!(byte, b' ' | b'-' | b'/' | b'(' | b')' | b'.') - }) -} - -fn parse_special_conditions_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - let table_len = SPECIAL_CONDITION_COUNT.checked_mul(4)?; - let table_end = SPECIAL_CONDITIONS_OFFSET.checked_add(table_len)?; - if table_end > bytes.len() { - return None; - } - - let mut entries = Vec::with_capacity(SPECIAL_CONDITION_COUNT); - for definition in KNOWN_SPECIAL_CONDITION_DEFINITIONS { - let value = read_u32_at( - bytes, - SPECIAL_CONDITIONS_OFFSET + (definition.slot_index as usize) * 4, - )?; - if value > 1 { - return None; - } - entries.push(SmpSpecialConditionEntry { - slot_index: definition.slot_index, - hidden: definition.hidden, - label_id: definition.label_id, - help_id: definition.help_id, - label: definition.label.to_string(), - value, - value_hex: format!("0x{value:08x}"), - }); - } - - let hidden_sentinel = entries - .iter() - .find(|entry| entry.slot_index == SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT as u8)?; - if hidden_sentinel.value != 1 { - return None; - } - - let enabled_visible_labels = entries - .iter() - .filter(|entry| !entry.hidden && entry.value != 0) - .map(|entry| entry.label.clone()) - .collect::>(); - let source_kind = match file_extension_hint.unwrap_or("") { - "gmp" => "map-fixed-special-conditions-range", - "gms" => "save-fixed-special-conditions-range", - "gmx" => "sandbox-fixed-special-conditions-range", - _ => "fixed-special-conditions-range", - } - .to_string(); - let profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()); - let mut evidence = vec![ - format!("fixed 36-dword range at 0x{SPECIAL_CONDITIONS_OFFSET:04x}"), - "all observed lanes are boolean dwords".to_string(), - "hidden slot 35 carries the expected sentinel value 1".to_string(), - "slot metadata matches the grounded editor special-conditions table at 0x005f3ab0" - .to_string(), - ]; - if enabled_visible_labels.is_empty() { - evidence.push("no visible special conditions enabled in this file".to_string()); - } else { - evidence.push(format!( - "enabled visible conditions: {}", - enabled_visible_labels.join(", ") - )); - } - - Some(SmpSpecialConditionsProbe { - profile_family, - source_kind, - table_offset: SPECIAL_CONDITIONS_OFFSET, - table_len, - enabled_visible_count: enabled_visible_labels.len(), - enabled_visible_labels, - hidden_sentinel_slot_index: SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT as u8, - hidden_sentinel_value: hidden_sentinel.value, - hidden_sentinel_value_hex: hidden_sentinel.value_hex.clone(), - entries, - evidence, - }) -} - -fn parse_post_special_conditions_scalar_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - special_conditions_probe: Option<&SmpSpecialConditionsProbe>, -) -> Option { - special_conditions_probe?; - if POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET > bytes.len() { - return None; - } - - let dword_count = - (POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET) / 4; - let mut nonzero_lanes = Vec::new(); - for index in 0..dword_count { - let absolute_offset = POST_SPECIAL_CONDITIONS_SCALAR_OFFSET + index * 4; - let value = read_u32_at(bytes, absolute_offset)?; - if value == 0 { - continue; - } - nonzero_lanes.push(SmpPostSpecialConditionsScalarLane { - absolute_offset, - relative_offset: absolute_offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET, - absolute_offset_hex: format!("0x{absolute_offset:04x}"), - relative_offset_hex: format!( - "0x{:x}", - absolute_offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - ), - value, - value_hex: format!("0x{value:08x}"), - probable_f32_le: probable_normal_f32_string(value), - }); - } - - let source_kind = match file_extension_hint.unwrap_or("") { - "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 profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()); - let first_nonzero_offset = nonzero_lanes.first().map(|lane| lane.absolute_offset); - let last_nonzero_offset = nonzero_lanes.last().map(|lane| lane.absolute_offset); - let overlap_nonzero_relative_offset_hexes = nonzero_lanes - .iter() - .filter(|lane| lane.absolute_offset < POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET) - .map(|lane| lane.relative_offset_hex.clone()) - .collect::>(); - let tail_nonzero_lanes = nonzero_lanes - .iter() - .filter(|lane| lane.absolute_offset >= POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET) - .cloned() - .collect::>(); - let tail_first_nonzero_offset = tail_nonzero_lanes.first().map(|lane| lane.absolute_offset); - let tail_last_nonzero_offset = tail_nonzero_lanes.last().map(|lane| lane.absolute_offset); - let tail_nonzero_relative_offset_hexes = tail_nonzero_lanes - .iter() - .map(|lane| lane.relative_offset_hex.clone()) - .collect::>(); - let grounded_text_field_remaining_file_window = &bytes[POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - ..POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET]; - let mut grounded_text_field_remaining_nonzero_offsets = Vec::new(); - for (index, byte) in grounded_text_field_remaining_file_window.iter().enumerate() { - if *byte != 0 { - grounded_text_field_remaining_nonzero_offsets - .push(POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + index); - } - } - let grounded_text_field_remaining_first_nonzero_offset = - grounded_text_field_remaining_nonzero_offsets - .first() - .copied(); - let grounded_text_field_remaining_last_nonzero_offset = - grounded_text_field_remaining_nonzero_offsets - .last() - .copied(); - let mut evidence = vec![ - format!( - "fixed post-sentinel dword window at 0x{POST_SPECIAL_CONDITIONS_SCALAR_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}" - ), - "window starts immediately after the hidden special-conditions sentinel slot at 0x0df0" - .to_string(), - format!( - "leading overlap prefix 0x{POST_SPECIAL_CONDITIONS_SCALAR_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET:04x} aliases aligned runtime-rule band indices {}..{}", - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX, - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX - + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - - 1 - ), - format!("save-only tail begins at 0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET:04x}"), - format!( - "that tail is offset-aligned with live runtime object bytes [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET:04x}..+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET:04x}]" - ), - format!( - "the tail start lands on the grounded live field [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET:04x}], a 0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN:x}-byte status-text buffer written by win/lose and winner-announcement helpers" - ), - format!( - "current dword scan stops at 0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}, leaving one byte-aligned continuation window 0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET:04x} before the next clean live-field edge at [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET:04x}]" - ), - format!( - "the next exact grounded fields after that edge begin at [world+0x{POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}], and [world+0x{POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET:04x}], which map to file offsets 0x{POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET:04x}, 0x0f5d, 0x0f61, 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x}, and 0x0f6d" - ), - format!( - "the first grounded dword-sized fields after that edge are [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}] and [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET:04x}], which would land at file offsets 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x} and 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET:04x}" - ), - ]; - if nonzero_lanes.is_empty() { - evidence.push( - "all observed dwords in this post-sentinel window are zero for this file".to_string(), - ); - } else { - evidence.push(format!( - "observed {} nonzero dword lanes between {} and {}", - nonzero_lanes.len(), - nonzero_lanes - .first() - .map(|lane| lane.absolute_offset_hex.as_str()) - .unwrap_or("n/a"), - nonzero_lanes - .last() - .map(|lane| lane.absolute_offset_hex.as_str()) - .unwrap_or("n/a") - )); - if nonzero_lanes - .iter() - .all(|lane| lane.probable_f32_le.is_some()) - { - evidence.push( - "every nonzero lane in this window also decodes as a normal finite little-endian f32" - .to_string(), - ); - } - evidence.push(format!( - "{} nonzero lanes fall inside the aligned-band overlap prefix and {} fall inside the later tail", - overlap_nonzero_relative_offset_hexes.len(), - tail_nonzero_lanes.len() - )); - } - evidence.push( - "checked file bytes in the later tail are not yet validated as a byte-for-byte mirror of the live object, because the region aligned to [world+0x4b47] does not currently decode as preserved text in the checked saves" - .to_string(), - ); - if grounded_text_field_remaining_nonzero_offsets.is_empty() { - evidence.push( - "the remaining file window through the grounded text-field edge is all zero in this file" - .to_string(), - ); - } else { - evidence.push(format!( - "the remaining file window through the grounded text-field edge still has {} nonzero bytes between 0x{:04x} and 0x{:04x}", - grounded_text_field_remaining_nonzero_offsets.len(), - grounded_text_field_remaining_first_nonzero_offset.unwrap_or(0), - grounded_text_field_remaining_last_nonzero_offset.unwrap_or(0) - )); - } - - Some(SmpPostSpecialConditionsScalarProbe { - profile_family, - source_kind, - window_offset: POST_SPECIAL_CONDITIONS_SCALAR_OFFSET, - window_end_offset: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, - window_len: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET, - window_len_hex: format!( - "0x{:x}", - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - ), - dword_count, - overlap_end_offset: POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET, - overlap_end_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET:04x}" - ), - overlap_dword_count: (POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET - - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET) - / 4, - overlap_nonzero_dword_count: overlap_nonzero_relative_offset_hexes.len(), - overlap_nonzero_relative_offset_hexes, - tail_offset: POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET, - tail_offset_hex: format!("0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET:04x}"), - tail_len: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET, - tail_len_hex: format!( - "0x{:x}", - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET - ), - tail_dword_count: (POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET) - / 4, - tail_runtime_object_offset: POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET, - tail_runtime_object_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET:04x}" - ), - tail_runtime_object_end_offset: - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET, - tail_runtime_object_end_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET:04x}" - ), - tail_runtime_object_validated_byte_mirror: false, - tail_grounded_live_field_offset: - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET, - tail_grounded_live_field_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET:04x}" - ), - tail_grounded_live_field_name: "victory-or-outcome status text buffer".to_string(), - tail_grounded_live_field_copy_len: - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN, - tail_grounded_live_field_copy_len_hex: format!( - "0x{:x}", - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN - ), - tail_grounded_live_field_copy_end_offset: - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET, - tail_grounded_live_field_copy_end_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET:04x}" - ), - tail_window_cuts_through_grounded_live_field: - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET - < POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET, - tail_grounded_live_field_remaining_file_window_offset: - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, - tail_grounded_live_field_remaining_file_window_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}" - ), - tail_grounded_live_field_remaining_file_window_len: - POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET - - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, - tail_grounded_live_field_remaining_file_window_len_hex: format!( - "0x{:x}", - POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET - - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - ), - tail_grounded_live_field_remaining_file_window_nonzero_byte_count: - grounded_text_field_remaining_nonzero_offsets.len(), - tail_grounded_live_field_remaining_file_window_first_nonzero_offset: - grounded_text_field_remaining_first_nonzero_offset, - tail_grounded_live_field_remaining_file_window_first_nonzero_offset_hex: - grounded_text_field_remaining_first_nonzero_offset - .map(|offset| format!("0x{offset:04x}")), - tail_grounded_live_field_remaining_file_window_last_nonzero_offset: - grounded_text_field_remaining_last_nonzero_offset, - tail_grounded_live_field_remaining_file_window_last_nonzero_offset_hex: - grounded_text_field_remaining_last_nonzero_offset - .map(|offset| format!("0x{offset:04x}")), - tail_next_grounded_dword_field_offset: - POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET, - tail_next_grounded_dword_field_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}" - ), - tail_next_grounded_dword_field_file_offset: - POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET, - tail_next_grounded_dword_field_file_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x}" - ), - tail_second_grounded_dword_field_offset: - POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET, - tail_second_grounded_dword_field_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET:04x}" - ), - tail_second_grounded_dword_field_file_offset: - POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET, - tail_second_grounded_dword_field_file_offset_hex: format!( - "0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET:04x}" - ), - post_text_field_file_alignment_matches_grounded_dword_fields: false, - tail_nonzero_dword_count: tail_nonzero_lanes.len(), - tail_first_nonzero_offset, - tail_first_nonzero_offset_hex: tail_first_nonzero_offset - .map(|offset| format!("0x{offset:04x}")), - tail_last_nonzero_offset, - tail_last_nonzero_offset_hex: tail_last_nonzero_offset - .map(|offset| format!("0x{offset:04x}")), - tail_nonzero_relative_offset_hexes, - nonzero_dword_count: nonzero_lanes.len(), - first_nonzero_offset, - first_nonzero_offset_hex: first_nonzero_offset.map(|offset| format!("0x{offset:04x}")), - last_nonzero_offset, - last_nonzero_offset_hex: last_nonzero_offset.map(|offset| format!("0x{offset:04x}")), - nonzero_lanes, - evidence, - }) -} - -fn parse_post_text_field_neighborhood_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - special_conditions_probe: Option<&SmpSpecialConditionsProbe>, -) -> Option { - special_conditions_probe?; - if POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET > bytes.len() { - return None; - } - - let profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()); - let source_kind = match file_extension_hint.unwrap_or("") { - "gmp" => "post-text-grounded-field-neighborhood", - "gms" => "post-text-grounded-field-neighborhood", - "gmx" => "post-text-grounded-field-neighborhood", - _ => "post-text-grounded-field-neighborhood", - } - .to_string(); - - let exact_fields = [ - ( - "Auto-Show Grade During Track Lay", - POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET, - POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET, - 1usize, - ), - ( - "Starting Building Density Level", - POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET, - 0x0f5dusize, - 1usize, - ), - ( - "Building Density Growth", - POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET, - 0x0f61usize, - 1usize, - ), - ( - "leftover simulation time accumulator", - POST_TEXT_FIELD_3_RUNTIME_OBJECT_OFFSET, - 0x0f65usize, - 4usize, - ), - ( - "selected-year lane snapshot", - POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET, - 0x0f6dusize, - 1usize, - ), - ( - "late locomotive policy gate dword", - POST_TEXT_FIELD_5_RUNTIME_OBJECT_OFFSET, - 0x0f71usize, - 4usize, - ), - ]; - - let grounded_field_observations = exact_fields - .iter() - .map( - |(field_name, runtime_object_offset, file_offset, field_width_bytes)| { - let raw = &bytes[*file_offset..*file_offset + *field_width_bytes]; - let raw_hex = hex_encode(raw); - let (value_u8, value_u8_hex, value_u32, value_u32_hex, probable_f32_le) = - if *field_width_bytes == 1 { - let value = raw[0]; - ( - Some(value), - Some(format!("0x{value:02x}")), - None, - None, - None, - ) - } else { - let value = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); - ( - None, - None, - Some(value), - Some(format!("0x{value:08x}")), - probable_normal_f32_string(value), - ) - }; - SmpPostTextGroundedFieldObservation { - field_name: (*field_name).to_string(), - runtime_object_offset: *runtime_object_offset, - runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"), - file_offset: *file_offset, - file_offset_hex: format!("0x{file_offset:04x}"), - field_width_bytes: *field_width_bytes, - field_width_bytes_hex: format!("0x{field_width_bytes:x}"), - raw_hex, - value_u8, - value_u8_hex, - value_u32, - value_u32_hex, - probable_f32_le, - } - }, - ) - .collect::>(); - - let one_byte_early_float_candidates = exact_fields - .iter() - .filter(|(_, _, file_offset, _)| *file_offset > 0) - .filter_map(|(field_name, runtime_object_offset, file_offset, _)| { - let candidate_offset = file_offset - 1; - let value = read_u32_at(bytes, candidate_offset)?; - let probable_f32_le = probable_normal_f32_string(value)?; - Some(SmpPostTextFloatAlignmentCandidate { - grounded_field_name: (*field_name).to_string(), - grounded_field_runtime_object_offset: *runtime_object_offset, - grounded_field_runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"), - grounded_field_file_offset: *file_offset, - grounded_field_file_offset_hex: format!("0x{file_offset:04x}"), - candidate_offset, - candidate_offset_hex: format!("0x{candidate_offset:04x}"), - candidate_value: value, - candidate_value_hex: format!("0x{value:08x}"), - probable_f32_le, - }) - }) - .collect::>(); - - let mut evidence = vec![ - format!( - "post-text grounded-field neighborhood spans file offsets 0x{POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET:04x}..0x{POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET:04x}" - ), - "this neighborhood starts at the first grounded post-text field [world+0x4c74] and extends through the later dword at [world+0x4c8c]".to_string(), - "the exact grounded field offsets here are byte-oriented at 0x0f59, 0x0f5d, 0x0f61, and 0x0f6d, with dword-sized fields only at 0x0f65 and 0x0f71".to_string(), - ]; - if one_byte_early_float_candidates.is_empty() { - evidence.push( - "no one-byte-early little-endian float-looking starts were observed ahead of the grounded fields in this file".to_string(), - ); - } else { - evidence.push(format!( - "observed {} float-looking 4-byte starts exactly one byte before grounded field offsets in this file", - one_byte_early_float_candidates.len() - )); - } - - Some(SmpPostTextFieldNeighborhoodProbe { - profile_family, - source_kind, - window_offset: POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET, - window_end_offset: POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET, - window_len: POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET - POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET, - window_len_hex: format!( - "0x{:x}", - POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET - POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET - ), - grounded_field_observations, - one_byte_early_float_candidates, - evidence, - }) -} - -fn parse_locomotive_policy_neighborhood_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - special_conditions_probe: Option<&SmpSpecialConditionsProbe>, -) -> Option { - special_conditions_probe?; - if LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET > bytes.len() { - return None; - } - - let profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()); - let source_kind = match file_extension_hint.unwrap_or("") { - "gmp" => "locomotive-policy-neighborhood", - "gms" => "locomotive-policy-neighborhood", - "gmx" => "locomotive-policy-neighborhood", - _ => "locomotive-policy-neighborhood", - } - .to_string(); - - let exact_fields = [ - ( - "selected-year bucket companion scalar", - LOCOMOTIVE_POLICY_FIELD_NEG3_RUNTIME_OBJECT_OFFSET, - 0x0f87usize, - 4usize, - ), - ( - "startup-dispatch reset-owned band at +0x4cae", - LOCOMOTIVE_POLICY_FIELD_NEG2_RUNTIME_OBJECT_OFFSET, - 0x0f93usize, - 4usize, - ), - ( - "startup-dispatch reset-owned band at +0x4cb2", - LOCOMOTIVE_POLICY_FIELD_NEG1_RUNTIME_OBJECT_OFFSET, - 0x0f97usize, - 4usize, - ), - ( - "linked-site removal follow-on gate", - LOCOMOTIVE_POLICY_FIELD_0_RUNTIME_OBJECT_OFFSET, - 0x0f78usize, - 1usize, - ), - ( - "All Steam Locos Avail.", - LOCOMOTIVE_POLICY_FIELD_1_RUNTIME_OBJECT_OFFSET, - 0x0f7cusize, - 1usize, - ), - ( - "All Diesel Locos Avail.", - LOCOMOTIVE_POLICY_FIELD_2_RUNTIME_OBJECT_OFFSET, - 0x0f7dusize, - 1usize, - ), - ( - "All Electric Locos Avail.", - LOCOMOTIVE_POLICY_FIELD_3_RUNTIME_OBJECT_OFFSET, - 0x0f7eusize, - 1usize, - ), - ( - "station-list selected station id", - LOCOMOTIVE_POLICY_FIELD_4_RUNTIME_OBJECT_OFFSET, - 0x0f9fusize, - 4usize, - ), - ( - "cached available-locomotive rating", - LOCOMOTIVE_POLICY_FIELD_5_RUNTIME_OBJECT_OFFSET, - 0x0fa3usize, - 4usize, - ), - ]; - - let grounded_field_observations = exact_fields - .iter() - .map( - |(field_name, runtime_object_offset, file_offset, field_width_bytes)| { - let raw = &bytes[*file_offset..*file_offset + *field_width_bytes]; - let raw_hex = hex_encode(raw); - let (value_u8, value_u8_hex, value_u32, value_u32_hex, probable_f32_le) = - if *field_width_bytes == 1 { - let value = raw[0]; - ( - Some(value), - Some(format!("0x{value:02x}")), - None, - None, - None, - ) - } else { - let value = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); - ( - None, - None, - Some(value), - Some(format!("0x{value:08x}")), - probable_normal_f32_string(value), - ) - }; - SmpLocomotivePolicyFieldObservation { - field_name: (*field_name).to_string(), - runtime_object_offset: *runtime_object_offset, - runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"), - file_offset: *file_offset, - file_offset_hex: format!("0x{file_offset:04x}"), - field_width_bytes: *field_width_bytes, - field_width_bytes_hex: format!("0x{field_width_bytes:x}"), - raw_hex, - value_u8, - value_u8_hex, - value_u32, - value_u32_hex, - probable_f32_le, - } - }, - ) - .collect::>(); - - let three_byte_early_float_candidates = exact_fields - .iter() - .filter(|(_, _, _, width)| *width == 4usize) - .filter_map(|(field_name, runtime_object_offset, file_offset, _)| { - let candidate_offset = file_offset.saturating_sub(3); - let value = read_u32_at(bytes, candidate_offset)?; - let probable_f32_le = probable_normal_f32_string(value)?; - Some(SmpLocomotivePolicyFloatAlignmentCandidate { - grounded_field_name: (*field_name).to_string(), - grounded_field_runtime_object_offset: *runtime_object_offset, - grounded_field_runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"), - grounded_field_file_offset: *file_offset, - grounded_field_file_offset_hex: format!("0x{file_offset:04x}"), - candidate_offset, - candidate_offset_hex: format!("0x{candidate_offset:04x}"), - candidate_value: value, - candidate_value_hex: format!("0x{value:08x}"), - probable_f32_le, - }) - }) - .collect::>(); - - let mut evidence = vec![ - format!( - "locomotive-policy neighborhood spans file offsets 0x{LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET:04x}..0x{LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET:04x}" - ), - "this neighborhood covers the selected-year bucket companion scalar, two startup-reset-owned bands, the linked-site removal gate, the three locomotive-availability policy bytes, the station-list selected-station mirror, and the cached available-locomotive rating".to_string(), - "the exact byte policy lanes live at 0x0f78 and 0x0f7c..0x0f7e, while the earlier grounded dword starts map to 0x0f87, 0x0f93, and 0x0f97 and the later grounded dword starts map to 0x0f9f and 0x0fa3".to_string(), - ]; - if three_byte_early_float_candidates.is_empty() { - evidence.push( - "no three-byte-early little-endian float-looking starts were observed ahead of the grounded dword fields in this file".to_string(), - ); - } else { - evidence.push(format!( - "observed {} float-looking 4-byte starts exactly three bytes before grounded dword fields in this file", - three_byte_early_float_candidates.len() - )); - } - - Some(SmpLocomotivePolicyNeighborhoodProbe { - profile_family, - source_kind, - window_offset: LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET, - window_end_offset: LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET, - window_len: LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET - - LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET, - window_len_hex: format!( - "0x{:x}", - LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET - LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET - ), - grounded_field_observations, - three_byte_early_float_candidates, - evidence, - }) -} - -fn parse_pre_recipe_scalar_plateau_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - special_conditions_probe: Option<&SmpSpecialConditionsProbe>, -) -> Option { - special_conditions_probe?; - if PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET > bytes.len() { - return None; - } - - let profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()); - let source_kind = match file_extension_hint.unwrap_or("") { - "gmp" => "pre-recipe-scalar-plateau", - "gms" => "pre-recipe-scalar-plateau", - "gmx" => "pre-recipe-scalar-plateau", - _ => "pre-recipe-scalar-plateau", - } - .to_string(); - - let aligned_dword_count = - (PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET) / 4; - let mut nonzero_lanes = Vec::new(); - for index in 0..aligned_dword_count { - let absolute_offset = PRE_RECIPE_SCALAR_PLATEAU_OFFSET + index * 4; - let value = read_u32_at(bytes, absolute_offset)?; - if value == 0 { - continue; - } - nonzero_lanes.push(SmpPreRecipeScalarPlateauLane { - absolute_offset, - relative_offset: absolute_offset - PRE_RECIPE_SCALAR_PLATEAU_OFFSET, - absolute_offset_hex: format!("0x{absolute_offset:04x}"), - relative_offset_hex: format!( - "0x{:x}", - absolute_offset - PRE_RECIPE_SCALAR_PLATEAU_OFFSET - ), - value, - value_hex: format!("0x{value:08x}"), - probable_f32_le: probable_normal_f32_string(value), - }); - } - - let family_signature = match ( - read_u32_at(bytes, 0x0faf), - read_u32_at(bytes, 0x0fb3), - read_u32_at(bytes, 0x0fcb), - ) { - (Some(0x4000003f), Some(0xe560423f), Some(0x00000000)) => { - "rt3-105-scenario-pre-recipe-plateau-v1" - } - (Some(0x8000003f), Some(0x75c28f3f), Some(0x00300000)) => { - "rt3-105-base-pre-recipe-plateau-v1" - } - (Some(0x8000003f), Some(0x75c28f3f), Some(0xcdcdcd00)) => { - "rt3-105-alt-pre-recipe-plateau-v1" - } - _ => "unknown", - } - .to_string(); - - let mut evidence = vec![ - format!( - "aligned scalar plateau spans file offsets 0x{PRE_RECIPE_SCALAR_PLATEAU_OFFSET:04x}..0x{PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET:04x}" - ), - "this plateau ends immediately before the grounded recipe-book root at [world+0x0fe7]".to_string(), - "current grounding inside this span is still structural rather than semantic, so the probe only records aligned dword lanes and observed family signatures".to_string(), - ]; - if !nonzero_lanes.is_empty() { - evidence.push(format!( - "observed {} nonzero aligned dword lanes in the pre-recipe plateau", - nonzero_lanes.len() - )); - } - if family_signature != "unknown" { - evidence.push(format!( - "matched observed family signature {family_signature}" - )); - } - - Some(SmpPreRecipeScalarPlateauProbe { - profile_family, - source_kind, - window_offset: PRE_RECIPE_SCALAR_PLATEAU_OFFSET, - window_end_offset: PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET, - window_len: PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET, - window_len_hex: format!( - "0x{:x}", - PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET - ), - aligned_dword_count, - family_signature, - nonzero_lanes, - evidence, - }) -} - -fn parse_recipe_book_summary_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - special_conditions_probe: Option<&SmpSpecialConditionsProbe>, -) -> Option { - special_conditions_probe?; - if RECIPE_BOOK_SUMMARY_END_OFFSET > bytes.len() { - return None; - } - - let profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()); - let source_kind = match file_extension_hint.unwrap_or("") { - "gmp" => "recipe-book-summary", - "gms" => "recipe-book-summary", - "gmx" => "recipe-book-summary", - _ => "recipe-book-summary", - } - .to_string(); - - let mut books = Vec::with_capacity(RECIPE_BOOK_COUNT); - let mut mixed_head_count = 0usize; - let mut mixed_line_area_count = 0usize; - let mut cdcd_line_area_count = 0usize; - let mut zero_line_area_count = 0usize; - - for book_index in 0..RECIPE_BOOK_COUNT { - let book_offset = RECIPE_BOOK_ROOT_OFFSET + book_index * RECIPE_BOOK_STRIDE; - let head = &bytes[book_offset..book_offset + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET]; - let line_area_offset = book_offset + RECIPE_BOOK_LINE_AREA_OFFSET; - let line_area = &bytes[line_area_offset..line_area_offset + RECIPE_BOOK_LINE_AREA_LEN]; - let max_annual_production_offset = book_offset + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET; - let max_annual_production_word = read_u32_at(bytes, max_annual_production_offset)?; - let mut lines = Vec::with_capacity(RECIPE_BOOK_LINE_COUNT); - for line_index in 0..RECIPE_BOOK_LINE_COUNT { - let line_offset = line_area_offset + line_index * RECIPE_BOOK_LINE_STRIDE; - let line = &bytes[line_offset..line_offset + RECIPE_BOOK_LINE_STRIDE]; - let supplied_cargo_token_window = &line[0x08..0x20]; - let demanded_cargo_token_window = &line[0x1c..0x30]; - let mode_word = read_u32_at(bytes, line_offset)?; - let annual_amount_word = read_u32_at(bytes, line_offset + 0x04)?; - let supplied_cargo_token_word = read_u32_at(bytes, line_offset + 0x08)?; - let demanded_cargo_token_word = read_u32_at(bytes, line_offset + 0x1c)?; - lines.push(SmpRecipeBookLineSummary { - line_index, - line_offset, - line_offset_hex: format!("0x{line_offset:04x}"), - line_kind: classify_recipe_book_region_kind(line).to_string(), - line_signature_kind: classify_recipe_line_signature( - mode_word, - supplied_cargo_token_word, - demanded_cargo_token_word, - ) - .to_string(), - imports_to_runtime_descriptor: mode_word != 0, - runtime_import_branch_kind: classify_recipe_runtime_import_branch(mode_word) - .to_string(), - line_nonzero_byte_count: line.iter().filter(|byte| **byte != 0).count(), - line_cdcd_byte_count: line.iter().filter(|byte| **byte == 0xcd).count(), - line_first_16_hex: hex_encode(&line[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(line.len())]), - mode_word_offset: line_offset, - mode_word_offset_hex: format!("0x{line_offset:04x}"), - mode_word, - mode_word_hex: format!("0x{mode_word:08x}"), - annual_amount_offset: line_offset + 0x04, - annual_amount_offset_hex: format!("0x{:04x}", line_offset + 0x04), - annual_amount_word, - annual_amount_word_hex: format!("0x{annual_amount_word:08x}"), - annual_amount_probable_f32_le: probable_normal_f32_string(annual_amount_word), - supplied_cargo_token_offset: line_offset + 0x08, - supplied_cargo_token_offset_hex: format!("0x{:04x}", line_offset + 0x08), - supplied_cargo_token_word, - supplied_cargo_token_word_hex: format!("0x{supplied_cargo_token_word:08x}"), - supplied_cargo_token_layout_kind: classify_recipe_token_layout( - supplied_cargo_token_word, - ) - .to_string(), - supplied_cargo_token_window_hex: hex_encode(supplied_cargo_token_window), - supplied_cargo_token_window_ascii: ascii_preview(supplied_cargo_token_window), - supplied_cargo_token_active_in_runtime_import: mode_word != 0 && mode_word != 1, - supplied_cargo_token_probable_high16_ascii_stem: - probable_recipe_token_high16_ascii_stem(supplied_cargo_token_word), - demanded_cargo_token_offset: line_offset + 0x1c, - demanded_cargo_token_offset_hex: format!("0x{:04x}", line_offset + 0x1c), - demanded_cargo_token_word, - demanded_cargo_token_word_hex: format!("0x{demanded_cargo_token_word:08x}"), - demanded_cargo_token_layout_kind: classify_recipe_token_layout( - demanded_cargo_token_word, - ) - .to_string(), - demanded_cargo_token_window_hex: hex_encode(demanded_cargo_token_window), - demanded_cargo_token_window_ascii: ascii_preview(demanded_cargo_token_window), - demanded_cargo_token_active_in_runtime_import: mode_word == 1 || mode_word == 3, - demanded_cargo_token_probable_high16_ascii_stem: - probable_recipe_token_high16_ascii_stem(demanded_cargo_token_word), - }); - } - - let head_kind = classify_recipe_book_region_kind(head).to_string(); - let line_area_kind = classify_recipe_book_region_kind(line_area).to_string(); - if head_kind == "mixed" { - mixed_head_count += 1; - } - match line_area_kind.as_str() { - "zero" => zero_line_area_count += 1, - "cdcd" => cdcd_line_area_count += 1, - _ => mixed_line_area_count += 1, - } - - books.push(SmpRecipeBookSummaryBook { - book_index, - book_offset, - book_offset_hex: format!("0x{book_offset:04x}"), - head_kind, - head_nonzero_byte_count: head.iter().filter(|byte| **byte != 0).count(), - head_cdcd_byte_count: head.iter().filter(|byte| **byte == 0xcd).count(), - head_first_16_hex: hex_encode(&head[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(head.len())]), - max_annual_production_offset, - max_annual_production_offset_hex: format!("0x{max_annual_production_offset:04x}"), - max_annual_production_word, - max_annual_production_word_hex: format!("0x{max_annual_production_word:08x}"), - max_annual_production_probable_f32_le: probable_normal_f32_string( - max_annual_production_word, - ), - line_area_offset, - line_area_offset_hex: format!("0x{line_area_offset:04x}"), - line_area_len: RECIPE_BOOK_LINE_AREA_LEN, - line_area_len_hex: format!("0x{:x}", RECIPE_BOOK_LINE_AREA_LEN), - line_area_kind, - line_area_nonzero_byte_count: line_area.iter().filter(|byte| **byte != 0).count(), - line_area_cdcd_byte_count: line_area.iter().filter(|byte| **byte == 0xcd).count(), - line_area_first_16_hex: hex_encode( - &line_area[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(line_area.len())], - ), - lines, - }); - } - - let mut evidence = vec![ - format!( - "grounded recipe-book root begins at file offset 0x{RECIPE_BOOK_ROOT_OFFSET:04x} and runtime offset [world+0x{RECIPE_BOOK_ROOT_OFFSET:04x}]" - ), - format!( - "parsed {RECIPE_BOOK_COUNT} fixed books with stride 0x{RECIPE_BOOK_STRIDE:x}, shared cap lane at +0x{RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET:x}, and five line slots at +0x{RECIPE_BOOK_LINE_AREA_OFFSET:x} with stride 0x{RECIPE_BOOK_LINE_STRIDE:x}" - ), - "this probe is structural only: it summarizes per-book heads plus five raw line records without decoding the mode or cargo-token semantics beyond the grounded offsets".to_string(), - ]; - evidence.push(format!( - "{mixed_head_count} books have mixed pre-line heads; line areas split into {zero_line_area_count} zero, {cdcd_line_area_count} cdcd, and {mixed_line_area_count} mixed books" - )); - - Some(SmpRecipeBookSummaryProbe { - profile_family, - source_kind, - root_offset: RECIPE_BOOK_ROOT_OFFSET, - root_offset_hex: format!("0x{RECIPE_BOOK_ROOT_OFFSET:04x}"), - runtime_object_root_offset: RECIPE_BOOK_ROOT_OFFSET, - runtime_object_root_offset_hex: format!("0x{RECIPE_BOOK_ROOT_OFFSET:04x}"), - book_count: RECIPE_BOOK_COUNT, - book_stride: RECIPE_BOOK_STRIDE, - book_stride_hex: format!("0x{:x}", RECIPE_BOOK_STRIDE), - max_annual_production_relative_offset: RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET, - max_annual_production_relative_offset_hex: format!( - "0x{:x}", - RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET - ), - line_area_relative_offset: RECIPE_BOOK_LINE_AREA_OFFSET, - line_area_relative_offset_hex: format!("0x{:x}", RECIPE_BOOK_LINE_AREA_OFFSET), - line_count: RECIPE_BOOK_LINE_COUNT, - line_stride: RECIPE_BOOK_LINE_STRIDE, - line_stride_hex: format!("0x{:x}", RECIPE_BOOK_LINE_STRIDE), - books, - evidence, - }) -} - -fn parse_smp_aligned_runtime_rule_band_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, - special_conditions_probe: Option<&SmpSpecialConditionsProbe>, -) -> Option { - special_conditions_probe?; - if SMP_ALIGNED_RUNTIME_RULE_END_OFFSET > bytes.len() { - return None; - } - - let source_kind = match file_extension_hint.unwrap_or("") { - "gmp" => "map-smp-aligned-runtime-rule-band", - "gms" => "save-smp-aligned-runtime-rule-band", - "gmx" => "sandbox-smp-aligned-runtime-rule-band", - _ => "smp-aligned-runtime-rule-band", - } - .to_string(); - let profile_family = container_profile - .map(|profile| profile.profile_family.clone()) - .unwrap_or_else(|| "unknown".to_string()); - - let mut nonzero_lanes = Vec::new(); - for band_index in 0..SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT { - let absolute_offset = SPECIAL_CONDITIONS_OFFSET + band_index * 4; - let value = read_u32_at(bytes, absolute_offset)?; - if value == 0 { - continue; - } - let lane_kind = 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" - } - .to_string(); - let known_label = if band_index < SPECIAL_CONDITION_COUNT { - Some( - KNOWN_SPECIAL_CONDITION_DEFINITIONS[band_index] - .label - .to_string(), - ) - } else { - None - }; - nonzero_lanes.push(SmpAlignedRuntimeRuleBandLane { - band_index, - absolute_offset, - relative_offset: absolute_offset - SPECIAL_CONDITIONS_OFFSET, - absolute_offset_hex: format!("0x{absolute_offset:04x}"), - relative_offset_hex: format!("0x{:x}", absolute_offset - SPECIAL_CONDITIONS_OFFSET), - lane_kind, - known_label, - value, - value_hex: format!("0x{value:08x}"), - probable_f32_le: probable_normal_f32_string(value), - }); - } - - let nonzero_band_indices = nonzero_lanes - .iter() - .map(|lane| lane.band_index) - .collect::>(); - let nonzero_post_window_overlap_band_indices = nonzero_lanes - .iter() - .filter(|lane| { - lane.band_index >= SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX - && lane.band_index - < SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX - + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - }) - .map(|lane| lane.band_index) - .collect::>(); - let nonzero_post_window_overlap_post_relative_offset_hexes = nonzero_lanes - .iter() - .filter(|lane| { - lane.band_index >= SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX - && lane.band_index - < SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX - + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - }) - .map(|lane| { - format!( - "0x{:x}", - (lane.band_index - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX) * 4 - ) - }) - .collect::>(); - let nonzero_relative_offset_hexes = nonzero_lanes - .iter() - .map(|lane| lane.relative_offset_hex.clone()) - .collect::>(); - let mut evidence = vec![ - format!( - "fixed `.smp`-aligned runtime-rule band at 0x{SPECIAL_CONDITIONS_OFFSET:04x}..0x{SMP_ALIGNED_RUNTIME_RULE_END_OFFSET:04x}" - ), - format!( - "band spans {} known editor rule dwords plus one trailing scalar", - SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT - ), - "first 36 dwords overlap the older fixed matrix probe rooted at 0x0d64".to_string(), - format!( - "trailing band indices {}..{} alias the leading post-sentinel window offsets {}..{}", - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX, - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX - + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - - 1, - "0x00", - format!( - "0x{:x}", - (SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - 1) * 4 - ) - ), - "band matches the grounded `.smp` save/load copy into `[world+0x4a7f..+0x4b43]`" - .to_string(), - ]; - if nonzero_lanes.is_empty() { - evidence - .push("all dwords in the aligned runtime-rule band are zero for this file".to_string()); - } else { - evidence.push(format!( - "observed {} nonzero lanes at band indices {:?}", - nonzero_lanes.len(), - nonzero_band_indices - )); - if !nonzero_post_window_overlap_band_indices.is_empty() { - evidence.push(format!( - "nonzero overlap lanes mirror post-window offsets {:?}", - nonzero_post_window_overlap_post_relative_offset_hexes - )); - } - } - - Some(SmpAlignedRuntimeRuleBandProbe { - profile_family, - source_kind, - band_offset: SPECIAL_CONDITIONS_OFFSET, - band_end_offset: SMP_ALIGNED_RUNTIME_RULE_END_OFFSET, - band_len: SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - SPECIAL_CONDITIONS_OFFSET, - band_len_hex: format!( - "0x{:x}", - SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - SPECIAL_CONDITIONS_OFFSET - ), - dword_count: SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT, - known_editor_rule_dword_count: SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT, - trailing_scalar_index: SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1, - trailing_scalar_offset: SPECIAL_CONDITIONS_OFFSET - + (SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1) * 4, - trailing_scalar_offset_hex: format!( - "0x{:04x}", - SPECIAL_CONDITIONS_OFFSET + (SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1) * 4 - ), - post_window_overlap_start_index: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX, - post_window_overlap_dword_count: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT, - post_window_overlap_end_index: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX - + SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - - 1, - post_window_overlap_post_relative_offset_start_hex: "0x0".to_string(), - post_window_overlap_post_relative_offset_end_hex: format!( - "0x{:x}", - (SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - 1) * 4 - ), - nonzero_post_window_overlap_band_indices, - nonzero_post_window_overlap_post_relative_offset_hexes, - nonzero_lane_count: nonzero_lanes.len(), - nonzero_band_indices, - nonzero_relative_offset_hexes, - nonzero_lanes, - evidence, - }) -} - -fn matches_candidate_availability_table_header(bytes: &[u8], header_offset: usize) -> bool { - matches!( - ( - read_u32_at(bytes, header_offset + 0x08), - read_u32_at(bytes, header_offset + 0x0c), - read_u32_at(bytes, header_offset + 0x10), - read_u32_at(bytes, header_offset + 0x14), - read_u32_at(bytes, header_offset + 0x18), - read_u32_at(bytes, header_offset + 0x1c), - read_u32_at(bytes, header_offset + 0x20), - read_u32_at(bytes, header_offset + 0x24), - read_u32_at(bytes, header_offset + 0x28), - ), - ( - Some(0x0000332e), - Some(0x00000001), - Some(0x00000022), - Some(0x00000002), - Some(0x00000002), - Some(_), - Some(_), - Some(0x00000000), - Some(0x00000001), - ) - ) -} - -fn classify_name_table_footer_progress_alignment(value: u32) -> Option<&'static str> { - match value { - 0x32dc => Some( - "Footer progress word 0x000032dc matches the grounded late rehydrate progress id 0x32dc.", - ), - 0x3714 => Some( - "Footer progress word 0x00003714 matches the grounded late rehydrate progress id 0x3714.", - ), - _ => None, - } -} - -fn parse_rt3_105_packed_profile_block( - bytes: &[u8], - packed_profile_offset: usize, - packed_profile_len: usize, -) -> Option { - let block_end = packed_profile_offset.checked_add(packed_profile_len)?; - if block_end > bytes.len() || packed_profile_len != 0x108 { - return None; - } - - let leading_word_0 = read_u32_at(bytes, packed_profile_offset)?; - let trailing_zero_word_count_after_leading_word = (1..4) - .take_while(|index| { - read_u32_at(bytes, packed_profile_offset + (index * 4)).is_some_and(|word| word == 0) - }) - .count(); - let header_flag_word_3 = read_u32_at(bytes, packed_profile_offset + 0x0c)?; - let map_path_offset = 0x10usize; - let display_name_offset = 0x43usize; - let stable_nonzero_word_offsets = [0x00usize, 0x0c, 0x78, 0x7c, 0x80, 0x84]; - let stable_nonzero_words = stable_nonzero_word_offsets - .iter() - .filter_map(|relative_offset| { - let value = read_u32_at(bytes, packed_profile_offset + relative_offset)?; - if value == 0 { - return None; - } - - Some(SmpPackedProfileWordLane { - relative_offset: *relative_offset, - relative_offset_hex: format!("0x{relative_offset:02x}"), - value, - value_hex: format!("0x{value:08x}"), - }) - }) - .collect::>(); - - Some(SmpRt3105PackedProfileBlock { - relative_len: packed_profile_len, - relative_len_hex: format!("0x{packed_profile_len:03x}"), - leading_word_0, - leading_word_0_hex: format!("0x{leading_word_0:08x}"), - trailing_zero_word_count_after_leading_word, - header_flag_word_3, - header_flag_word_3_hex: format!("0x{header_flag_word_3:08x}"), - map_path_offset, - map_path: read_c_string_in_range(bytes, packed_profile_offset + map_path_offset, block_end), - display_name_offset, - display_name: read_c_string_in_range( - bytes, - packed_profile_offset + display_name_offset, - block_end, - ), - profile_byte_0x77: bytes[packed_profile_offset + 0x77], - profile_byte_0x77_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x77]), - profile_byte_0x82: bytes[packed_profile_offset + 0x82], - profile_byte_0x82_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x82]), - profile_byte_0x97: bytes[packed_profile_offset + 0x97], - profile_byte_0x97_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x97]), - profile_byte_0xc5: bytes[packed_profile_offset + 0xc5], - profile_byte_0xc5_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0xc5]), - stable_nonzero_words, - }) -} - -fn collect_runtime_post_span_header_candidates( - bytes: &[u8], - start: usize, - search_len: usize, -) -> Vec { - let end = bytes.len().min(start + search_len); - let mut offset = start & !0x3; - let mut candidates = Vec::new(); - - while offset + 16 <= end && candidates.len() < 8 { - if let Some(candidate) = build_runtime_post_span_header_candidate(bytes, offset) { - let mut cluster_end = offset + 4; - while cluster_end + 16 <= end - && build_runtime_post_span_header_candidate(bytes, cluster_end).is_some() - { - cluster_end += 4; - } - candidates.push(candidate); - offset = cluster_end; - } else { - offset += 4; - } - } - - candidates -} - -fn build_runtime_post_span_header_candidate( - bytes: &[u8], - offset: usize, -) -> Option { - let words = read_u32_window(bytes, offset, 4); - if words.len() < 4 { - return None; - } - - let dense_words = words - .iter() - .copied() - .filter(|word| (word & 0xffff) == 0 && (word >> 16) != 0) - .collect::>(); - if dense_words.len() < 3 { - return None; - } - - let high_u16_words = words - .iter() - .map(|word| (word >> 16) as u16) - .collect::>(); - let mut grounded_alignments = Vec::new(); - for high in &high_u16_words { - if let Some(alignment) = classify_runtime_post_span_high16_grounded_alignment(*high) { - if !grounded_alignments - .iter() - .any(|existing| existing == alignment) - { - grounded_alignments.push(alignment.to_string()); - } - } - } - - Some(SmpRuntimePostSpanHeaderCandidate { - offset, - hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(), - dense_word_count: dense_words.len(), - high_hex_words: high_u16_words - .iter() - .map(|word| format!("0x{word:04x}")) - .collect(), - high_u16_words, - grounded_alignments, - words, - }) -} - -fn classify_runtime_post_span_high16_grounded_alignment(high_u16: u16) -> Option<&'static str> { - match high_u16 { - 0x32dc => Some( - "High-16 value 0x32dc matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.", - ), - 0x3714 => Some( - "High-16 value 0x3714 matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.", - ), - 0x3715 => Some( - "High-16 value 0x3715 matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.", - ), - _ => None, - } -} - -fn find_grounded_progress_high16_hits( - bytes: &[u8], - start: usize, - search_len: usize, -) -> Vec { - let end = bytes.len().min(start + search_len); - let mut hits = Vec::new(); - let mut offset = start & !0x3; - while offset + 4 <= end { - if let Some(word) = read_u32_at(bytes, offset) { - let high = (word >> 16) as u16; - if matches!(high, 0x32dc | 0x3714 | 0x3715) { - hits.push(format!("0x{high:04x}@0x{offset:08x}")); - } - } - offset += 4; - } - hits -} - -fn parse_grounded_progress_hit_offset(hits: &[String], high_u16: u16) -> Option { - let needle = format!("0x{high_u16:04x}@0x"); - let hit = hits.iter().find(|hit| hit.starts_with(&needle))?; - let offset_hex = hit.split("@0x").nth(1)?; - usize::from_str_radix(offset_hex, 16).ok() -} - -fn collect_ascii_previews_in_range( - bytes: &[u8], - start: usize, - end: usize, - min_len: usize, -) -> Vec { - let mut previews = Vec::new(); - let mut run_start = None; - let end = end.min(bytes.len()); - - for index in start..end { - let byte = bytes[index]; - if is_ascii_preview_byte(byte) { - run_start.get_or_insert(index); - continue; - } - - if let Some(current_start) = run_start.take() { - if index - current_start >= min_len { - previews.push(build_ascii_preview(bytes, current_start, index)); - } - } - } - - if let Some(current_start) = run_start { - if end - current_start >= min_len { - previews.push(build_ascii_preview(bytes, current_start, end)); - } - } - - previews -} - -fn find_c_string_with_suffix_in_range( - bytes: &[u8], - start: usize, - end: usize, - suffix: &str, -) -> Option { - let end = end.min(bytes.len()); - let suffix = suffix.as_bytes(); - let mut offset = start.min(end); - - while offset < end { - if !is_ascii_preview_byte(bytes[offset]) { - offset += 1; - continue; - } - - let run_start = offset; - while offset < end && is_ascii_preview_byte(bytes[offset]) { - offset += 1; - } - - let run = &bytes[run_start..offset]; - if run.ends_with(suffix) { - return Some(run_start); - } - } - - None -} - -fn read_c_string_in_range(bytes: &[u8], start: usize, end: usize) -> Option { - if start >= end || start >= bytes.len() { - return None; - } - - let end = end.min(bytes.len()); - let mut cursor = start; - while cursor < end && bytes[cursor] != 0 { - cursor += 1; - } - if cursor == start { - return None; - } - - std::str::from_utf8(&bytes[start..cursor]) - .ok() - .map(ToString::to_string) -} - -fn find_u16_le_offsets(bytes: &[u8], needle: u16) -> Vec { - let pattern = needle.to_le_bytes(); - bytes - .windows(pattern.len()) - .enumerate() - .filter_map(|(offset, window)| (window == pattern).then_some(offset)) - .collect() -} - -fn find_u32_le_offsets(bytes: &[u8], needle: u32) -> Vec { - let pattern = needle.to_le_bytes(); - bytes - .windows(pattern.len()) - .enumerate() - .filter_map(|(offset, window)| (window == pattern).then_some(offset)) - .collect() -} - -fn find_next_nonzero_offset(bytes: &[u8], start: usize) -> Option { - bytes - .iter() - .enumerate() - .skip(start) - .find_map(|(offset, byte)| (*byte != 0).then_some(offset)) -} - -fn find_zero_run(bytes: &[u8], start: usize, min_len: usize) -> Option { - let mut run_start = None; - let mut run_len = 0usize; - - for (offset, byte) in bytes.iter().enumerate().skip(start) { - if *byte == 0 { - run_start.get_or_insert(offset); - run_len += 1; - if run_len >= min_len { - return run_start; - } - } else { - run_start = None; - run_len = 0; - } - } - - None -} - -fn find_first_ascii_run(bytes: &[u8]) -> Option { - let mut start = None; - - for (index, byte) in bytes.iter().copied().enumerate() { - if is_ascii_preview_byte(byte) { - start.get_or_insert(index); - continue; - } - - if let Some(run_start) = start.take() { - if index - run_start >= MIN_ASCII_RUN_LEN { - return Some(build_ascii_preview(bytes, run_start, index)); - } - } - } - - start.and_then(|run_start| { - if bytes.len() - run_start >= MIN_ASCII_RUN_LEN { - Some(build_ascii_preview(bytes, run_start, bytes.len())) - } else { - None - } - }) -} - -fn parse_map_title_hint_probe( - bytes: &[u8], - file_extension_hint: Option<&str>, - container_profile: Option<&SmpContainerProfile>, -) -> Option { - if file_extension_hint != Some("gmp") { - return None; - } - - let grounded_title_hits = POST_LOAD_SCENARIO_FIXUP_TITLE_SET - .iter() - .filter_map(|title| { - let offset = find_first_subsequence_offset(bytes, title.as_bytes())?; - Some(SmpMapTitleHintTitleHit { - title: (*title).to_string(), - earliest_offset: offset, - }) - }) - .collect::>(); - - let embedded_map_references = find_ascii_fragment_occurrences_with_suffix(bytes, ".gmp") - .into_iter() - .map(|(offset, text)| SmpMapTitleHintMapReference { offset, text }) - .collect::>(); - - let adjacent_reference_title_pairs = - build_map_title_hint_adjacent_pairs(&embedded_map_references, &grounded_title_hits); - let strongest_same_stem_pair = adjacent_reference_title_pairs - .iter() - .find(|pair| pair.normalized_stem_match) - .cloned(); - - if grounded_title_hits.is_empty() - && embedded_map_references.is_empty() - && strongest_same_stem_pair.is_none() - { - return None; - } - - Some(SmpMapTitleHintProbe { - source_kind: "grounded-title-string-scan".to_string(), - profile_family: container_profile.map(|profile| profile.profile_family.clone()), - grounded_title_hits, - embedded_map_references, - adjacent_reference_title_pairs, - strongest_same_stem_pair, - }) -} - -fn build_map_title_hint_adjacent_pairs( - map_references: &[SmpMapTitleHintMapReference], - title_hits: &[SmpMapTitleHintTitleHit], -) -> Vec { - let mut pairs = Vec::new(); - - for map_reference in map_references { - let mut best_pair: Option = None; - for title_hit in title_hits { - let byte_distance = map_reference.offset.abs_diff(title_hit.earliest_offset); - if byte_distance > MAP_TITLE_HINT_REFERENCE_PAIR_DISTANCE_LIMIT { - continue; - } - let candidate = SmpMapTitleHintAdjacentPair { - map_reference_offset: map_reference.offset, - map_reference_text: map_reference.text.clone(), - title_offset: title_hit.earliest_offset, - title: title_hit.title.clone(), - byte_distance, - normalized_stem_match: normalize_map_title_hint_stem(&map_reference.text) - == normalize_map_title_hint_stem(&title_hit.title), - }; - let replace = match &best_pair { - Some(current) => { - (candidate.normalized_stem_match && !current.normalized_stem_match) - || (candidate.normalized_stem_match == current.normalized_stem_match - && candidate.byte_distance < current.byte_distance) - } - None => true, - }; - if replace { - best_pair = Some(candidate); - } - } - if let Some(pair) = best_pair { - pairs.push(pair); - } - } - - pairs.sort_by_key(|pair| { - ( - !pair.normalized_stem_match, - pair.byte_distance, - pair.map_reference_offset, - ) - }); - pairs -} - -fn normalize_map_title_hint_stem(text: &str) -> String { - text.trim() - .trim_end_matches(".gmp") - .trim_end_matches(".GMP") - .to_ascii_lowercase() -} - -fn find_first_subsequence_offset(bytes: &[u8], needle: &[u8]) -> Option { - if needle.is_empty() || bytes.len() < needle.len() { - return None; - } - bytes - .windows(needle.len()) - .position(|window| window == needle) -} - -fn find_ascii_fragment_occurrences_with_suffix(bytes: &[u8], suffix: &str) -> Vec<(usize, String)> { - let suffix_bytes = suffix.as_bytes(); - if suffix_bytes.is_empty() || bytes.len() < suffix_bytes.len() { - return Vec::new(); - } - - let mut occurrences = Vec::new(); - let mut seen_offsets = BTreeSet::new(); - for offset in 0..=bytes.len() - suffix_bytes.len() { - if &bytes[offset..offset + suffix_bytes.len()] != suffix_bytes { - continue; - } - if let Some((start, text)) = extract_ascii_fragment_containing(bytes, offset) { - if seen_offsets.insert(start) { - occurrences.push((start, text)); - } - } - } - occurrences -} - -fn extract_ascii_fragment_containing(bytes: &[u8], offset: usize) -> Option<(usize, String)> { - if offset >= bytes.len() || !is_map_title_hint_ascii_fragment_byte(bytes[offset]) { - return None; - } - - let mut start = offset; - while start > 0 && is_map_title_hint_ascii_fragment_byte(bytes[start - 1]) { - start -= 1; - } - - let mut end = offset; - while end < bytes.len() && is_map_title_hint_ascii_fragment_byte(bytes[end]) { - end += 1; - } - - if end <= start { - return None; - } - let len = (end - start).min(MAP_TITLE_HINT_ASCII_FRAGMENT_MAX_LEN); - let text = String::from_utf8_lossy(&bytes[start..start + len]) - .trim() - .to_string(); - if text.is_empty() { - return None; - } - Some((start, text)) -} - -fn is_map_title_hint_ascii_fragment_byte(byte: u8) -> bool { - matches!(byte, b' ' | b'!' | b'-' | b'.' | b'/' | b'\\' | b':' | b'_' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') -} - -fn build_ascii_preview(bytes: &[u8], start: usize, end: usize) -> SmpAsciiPreview { - let byte_len = end - start; - let preview_bytes = &bytes[start..end]; - let preview = String::from_utf8_lossy( - &preview_bytes[..preview_bytes.len().min(ASCII_PREVIEW_CHAR_LIMIT)], - ) - .into_owned(); - - SmpAsciiPreview { - offset: start, - byte_len, - truncated: byte_len > ASCII_PREVIEW_CHAR_LIMIT, - preview, - } -} - -fn is_ascii_preview_byte(byte: u8) -> bool { - matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x21..=0x7e) -} - -fn read_u32_window(bytes: &[u8], offset: usize, count: usize) -> Vec { - let mut words = Vec::new(); - let end = bytes.len().min(offset + count * 4); - for chunk in bytes[offset..end].chunks_exact(4) { - words.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); - } - words -} - -fn read_u16_window(bytes: &[u8], offset: usize, count: usize) -> Vec { - let mut words = Vec::new(); - let end = bytes.len().min(offset + count * 2); - for chunk in bytes[offset..end].chunks_exact(2) { - words.push(u16::from_le_bytes([chunk[0], chunk[1]])); - } - words -} - -fn format_u16_word_signature(words: &[u16]) -> String { - words - .iter() - .map(|word| format!("0x{word:04x}")) - .collect::>() - .join(", ") -} - -fn compact_nondirect_signature_family( - grouped_marker_relative_offset: Option, - head_signature_words: &[u16], - post_group_signature_words: &[u16], -) -> String { - let grouped_marker_bucket = grouped_marker_relative_offset.unwrap_or(0); - let head_word_2 = head_signature_words.get(2).copied().unwrap_or_default(); - let head_word_4 = head_signature_words.get(4).copied().unwrap_or_default(); - let head_word_6 = head_signature_words.get(6).copied().unwrap_or_default(); - let head_word_8 = head_signature_words.get(8).copied().unwrap_or_default(); - let head_word_10 = head_signature_words.get(10).copied().unwrap_or_default(); - let post_word_1 = post_group_signature_words - .get(1) - .copied() - .unwrap_or_default(); - let post_word_3 = post_group_signature_words - .get(3) - .copied() - .unwrap_or_default(); - let post_word_5 = post_group_signature_words - .get(5) - .copied() - .unwrap_or_default(); - let post_word_7 = post_group_signature_words - .get(7) - .copied() - .unwrap_or_default(); - - format!( - "nondirect-ge{:02x}-h{:04x}-{:04x}-{:04x}-{:04x}-{:04x}-p{:04x}-{:04x}-{:04x}-{:04x}", - grouped_marker_bucket, - head_word_2, - head_word_4, - head_word_6, - head_word_8, - head_word_10, - post_word_1, - post_word_3, - post_word_5, - post_word_7, - ) -} - -fn read_u8_at(bytes: &[u8], offset: usize) -> Option { - bytes.get(offset).copied() -} - -fn read_u16_at(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 2)?; - Some(u16::from_le_bytes([chunk[0], chunk[1]])) -} - -fn read_u32_at(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 4)?; - Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) -} - -fn read_i32_at(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 4)?; - Some(i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) -} - -fn read_i64_at(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 8)?; - Some(i64::from_le_bytes([ - chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7], - ])) -} - -fn read_u64_at(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 8)?; - Some(u64::from_le_bytes([ - chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7], - ])) -} - -fn read_f32_at(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 4)?; - Some(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) -} - -fn read_f64_at(bytes: &[u8], offset: usize) -> Option { - let chunk = bytes.get(offset..offset + 8)?; - Some(f64::from_le_bytes([ - chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7], - ])) -} - -fn read_ascii_c_string_at(bytes: &[u8], offset: usize, max_len: usize) -> Option { - let chunk = bytes.get(offset..offset + max_len)?; - let nul_index = chunk.iter().position(|byte| *byte == 0).unwrap_or(max_len); - let text = std::str::from_utf8(&chunk[..nul_index]) - .ok()? - .trim() - .to_string(); - Some(text) -} - -fn parse_nonzero_u32(bytes: &[u8], offset: usize) -> Option> { - read_u32_at(bytes, offset).map(|value| (value != 0).then_some(value)) -} - -fn round_f64_to_i64(value: f64) -> Option { - if !value.is_finite() { - return None; - } - let rounded = value.round(); - if rounded < i64::MIN as f64 || rounded > i64::MAX as f64 { - return None; - } - Some(rounded as i64) -} - -fn probable_normal_f32_string(value: u32) -> Option { - let exponent = (value >> 23) & 0xff; - if exponent == 0 || exponent == 0xff { - return None; - } - let scalar = f32::from_bits(value); - if !scalar.is_finite() { - return None; - } - Some(format!("{scalar:.6}")) -} - -fn probable_recipe_token_high16_ascii_stem(value: u32) -> Option { - if value & 0xffff != 0 { - return None; - } - let high = ((value >> 16) & 0xffff) as u16; - if high == 0 { - return None; - } - let low_byte = (high & 0x00ff) as u8; - let high_byte = (high >> 8) as u8; - if !low_byte.is_ascii_alphabetic() || !high_byte.is_ascii_alphabetic() { - return None; - } - Some(format!("{}{}", low_byte as char, high_byte as char)) -} - -fn classify_recipe_token_layout(value: u32) -> &'static str { - if value == 0 { - return "zero"; - } - if probable_recipe_token_high16_ascii_stem(value).is_some() { - return "high16-ascii-stem"; - } - if value & 0xffff == 0 { - return "high16-numeric"; - } - if value >> 16 == 0 { - return "low16-marker"; - } - "mixed" -} - -fn classify_recipe_line_signature( - mode_word: u32, - supplied_cargo_token_word: u32, - demanded_cargo_token_word: u32, -) -> &'static str { - let supplied_layout = classify_recipe_token_layout(supplied_cargo_token_word); - let demanded_layout = classify_recipe_token_layout(demanded_cargo_token_word); - if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_layout == "high16-numeric" { - return "demand-numeric-entry"; - } - if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_layout == "high16-ascii-stem" { - return "demand-stem-entry"; - } - if mode_word == 0 && demanded_cargo_token_word == 0 && supplied_layout == "high16-numeric" { - return "supply-numeric-entry"; - } - if mode_word != 0 && demanded_cargo_token_word == 0 && supplied_layout == "low16-marker" { - return "supply-marker-entry"; - } - if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_cargo_token_word == 0 { - return "zero"; - } - "mixed" -} - -fn classify_recipe_runtime_import_branch(mode_word: u32) -> &'static str { - if mode_word == 0 { - return "zero-mode-skipped"; - } - if mode_word == 1 { - return "mode1-demand-branch"; - } - if mode_word == 3 { - return "mode3-dual-branch"; - } - "nonzero-supply-branch" -} - -fn classify_recipe_book_region_kind(bytes: &[u8]) -> &'static str { - if bytes.iter().all(|byte| *byte == 0) { - "zero" - } else if bytes.iter().all(|byte| *byte == 0xcd) { - "cdcd" - } else { - "mixed" - } -} - -fn hex_encode(bytes: &[u8]) -> String { - bytes.iter().map(|byte| format!("{byte:02x}")).collect() -} - -fn ascii_preview(bytes: &[u8]) -> String { - bytes - .iter() - .map(|byte| match byte { - 0x20..=0x7e => char::from(*byte), - _ => '.', - }) - .collect() -} - -fn sha256_hex(bytes: &[u8]) -> String { - let digest = Sha256::digest(bytes); - format!("{digest:x}") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reports_grounded_tag_hits_and_offsets() { - let bytes = [ - 0x34, 0x12, 0x00, 0x00, 0xe0, 0x2e, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x80, - 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x71, 0x07, 0x00, 0x00, 0x71, 0x07, 0x00, 0x00, - 0x71, 0x07, 0x00, 0x00, 0xaa, 0xbb, 0x00, 0x00, 0xcc, 0xdd, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, b'H', b'e', b'l', - b'l', b'o', b' ', b'R', b'R', b'T', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, - 0x9a, 0xbc, 0xde, 0xf0, 0x00, 0x00, 0x00, 0x00, 0xee, 0x2c, 0x11, 0x51, 0x2d, 0x22, - 0x71, 0x94, 0x33, 0x72, 0x94, - ]; - let report = inspect_smp_bytes(&bytes); - - assert!(report.contains_grounded_runtime_tags); - assert_eq!(report.known_tag_hits.len(), 4); - assert_eq!(report.preamble.word_count, 16); - assert_eq!(report.preamble.words[0].value_le, 0x00001234); - let shared_header = report - .shared_header - .as_ref() - .expect("shared header should parse"); - assert!(shared_header.matches_grounded_common_signature); - let header_variant = report - .header_variant_probe - .as_ref() - .expect("header variant probe should exist"); - assert_eq!(header_variant.variant_family, "unknown"); - assert!(!header_variant.is_known_family); - assert_eq!(shared_header.primary_family_tag, 0x00002ee0); - assert_eq!( - shared_header.payload_window_words_8_to_9, - vec![0x0000bbaa, 0x0000ddcc] - ); - assert!(shared_header.reserved_words_10_to_14_all_zero); - assert_eq!(shared_header.final_flag_word, 0); - let ascii_run = report - .first_ascii_run - .as_ref() - .expect("ascii run should exist"); - assert_eq!(ascii_run.offset, 67); - assert_eq!(ascii_run.byte_len, 9); - assert_eq!(ascii_run.preview, "Hello RRT"); - let early_probe = report - .early_content_probe - .as_ref() - .expect("early content probe should exist"); - assert_eq!(early_probe.first_post_text_nonzero_offset, 88); - assert_eq!(early_probe.zero_pad_after_text_len, 12); - assert_eq!(early_probe.first_post_text_block_len, 4); - assert_eq!(early_probe.first_post_text_block_hex, "11223344"); - assert_eq!(early_probe.trailing_zero_pad_after_first_block_len, 16); - assert_eq!(early_probe.secondary_nonzero_offset, Some(108)); - assert_eq!(early_probe.secondary_aligned_word_window_offset, Some(108)); - assert_eq!( - &early_probe.secondary_aligned_word_window_words[..2], - &[0x78563412, 0xf0debc9a] - ); - assert!( - early_probe - .secondary_preview_hex - .starts_with("123456789abcdef0") - ); - let secondary_variant = report - .secondary_variant_probe - .as_ref() - .expect("secondary variant probe should exist"); - assert_eq!(secondary_variant.variant_family, "unknown"); - let container_profile = report - .container_profile - .as_ref() - .expect("container profile should exist"); - assert_eq!(container_profile.profile_family, "unknown"); - assert!(!container_profile.is_known_profile); - assert!(report.save_bootstrap_block.is_none()); - assert!(report.save_anchor_run_block.is_none()); - assert!(report.runtime_anchor_cycle_block.is_none()); - assert!(report.runtime_trailer_block.is_none()); - assert!(report.runtime_post_span_probe.is_none()); - assert!(report.classic_rehydrate_profile_probe.is_none()); - assert_eq!(report.known_tag_hits[0].tag_id, 0x2cee); - assert_eq!(report.known_tag_hits[0].hit_count, 1); - assert_eq!(report.known_tag_hits[0].sample_offsets, vec![120]); - assert_eq!(report.known_tag_hits[1].tag_id, 0x2d51); - assert_eq!(report.known_tag_hits[1].sample_offsets, vec![123]); - assert_eq!(report.known_tag_hits[2].tag_id, 0x9471); - assert_eq!(report.known_tag_hits[2].sample_offsets, vec![126]); - assert_eq!(report.known_tag_hits[3].tag_id, 0x9472); - assert_eq!(report.known_tag_hits[3].sample_offsets, vec![129]); - } - - #[test] - fn warns_when_no_grounded_tags_are_present() { - let report = inspect_smp_bytes(&[0xaa, 0xbb, 0xcc]); - - assert!(!report.contains_grounded_runtime_tags); - assert!(report.known_tag_hits.is_empty()); - assert_eq!(report.preamble.word_count, 0); - assert!(report.shared_header.is_none()); - assert!(report.header_variant_probe.is_none()); - assert!(report.first_ascii_run.is_none()); - assert!(report.early_content_probe.is_none()); - assert!(report.secondary_variant_probe.is_none()); - assert!(report.container_profile.is_none()); - assert!(report.save_bootstrap_block.is_none()); - assert!(report.save_anchor_run_block.is_none()); - assert!(report.runtime_anchor_cycle_block.is_none()); - assert!(report.runtime_trailer_block.is_none()); - assert!(report.runtime_post_span_probe.is_none()); - assert!(report.classic_rehydrate_profile_probe.is_none()); - assert!( - report - .warnings - .iter() - .any(|warning| warning.contains("No grounded runtime bundle tags were found")) - ); - } - - #[test] - fn parses_zeroed_post_special_conditions_scalar_window() { - let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET]; - let sentinel_offset = - SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; - bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); - - let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None) - .expect("special-conditions probe should parse"); - let probe = parse_post_special_conditions_scalar_probe( - &bytes, - Some("gmp"), - Some(&SmpContainerProfile { - profile_family: "rt3-map-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&special_conditions_probe), - ) - .expect("post-special-conditions probe should parse"); - - assert_eq!(probe.window_offset, POST_SPECIAL_CONDITIONS_SCALAR_OFFSET); - assert_eq!( - probe.window_end_offset, - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - ); - assert_eq!(probe.dword_count, 79); - assert_eq!( - probe.overlap_end_offset, - POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET - ); - assert_eq!(probe.overlap_dword_count, 14); - assert_eq!(probe.overlap_nonzero_dword_count, 0); - assert!(probe.overlap_nonzero_relative_offset_hexes.is_empty()); - assert_eq!( - probe.tail_offset, - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET - ); - assert_eq!(probe.tail_dword_count, 65); - assert_eq!( - probe.tail_runtime_object_offset, - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET - ); - assert_eq!( - probe.tail_runtime_object_end_offset, - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET - ); - assert!(!probe.tail_runtime_object_validated_byte_mirror); - assert_eq!( - probe.tail_grounded_live_field_offset, - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET - ); - assert_eq!( - probe.tail_grounded_live_field_copy_len, - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN - ); - assert_eq!( - probe.tail_grounded_live_field_copy_end_offset, - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET - ); - assert!(probe.tail_window_cuts_through_grounded_live_field); - assert_eq!( - probe.tail_grounded_live_field_remaining_file_window_offset, - POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - ); - assert_eq!( - probe.tail_grounded_live_field_remaining_file_window_len, - 0x28 - ); - assert_eq!( - probe.tail_grounded_live_field_remaining_file_window_nonzero_byte_count, - 0 - ); - assert_eq!(probe.tail_next_grounded_dword_field_offset_hex, "0x4c80"); - assert_eq!( - probe.tail_next_grounded_dword_field_file_offset_hex, - "0x0f65" - ); - assert_eq!(probe.tail_second_grounded_dword_field_offset_hex, "0x4c8c"); - assert_eq!( - probe.tail_second_grounded_dword_field_file_offset_hex, - "0x0f71" - ); - assert!(!probe.post_text_field_file_alignment_matches_grounded_dword_fields); - assert!( - probe - .tail_grounded_live_field_remaining_file_window_first_nonzero_offset - .is_none() - ); - assert!( - probe - .tail_grounded_live_field_remaining_file_window_last_nonzero_offset - .is_none() - ); - assert_eq!(probe.tail_nonzero_dword_count, 0); - assert!(probe.tail_first_nonzero_offset.is_none()); - assert!(probe.tail_last_nonzero_offset.is_none()); - assert!(probe.tail_nonzero_relative_offset_hexes.is_empty()); - assert_eq!(probe.nonzero_dword_count, 0); - assert!(probe.first_nonzero_offset.is_none()); - assert!(probe.last_nonzero_offset.is_none()); - assert!(probe.nonzero_lanes.is_empty()); - } - - #[test] - fn parses_zeroed_smp_aligned_runtime_rule_band() { - let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET]; - let sentinel_offset = - SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; - bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); - - let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None) - .expect("special-conditions probe should parse"); - let probe = parse_smp_aligned_runtime_rule_band_probe( - &bytes, - Some("gmp"), - Some(&SmpContainerProfile { - profile_family: "rt3-map-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&special_conditions_probe), - ) - .expect("aligned runtime-rule band probe should parse"); - - assert_eq!(probe.band_offset, SPECIAL_CONDITIONS_OFFSET); - assert_eq!(probe.band_end_offset, SMP_ALIGNED_RUNTIME_RULE_END_OFFSET); - assert_eq!(probe.dword_count, SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT); - assert_eq!( - probe.known_editor_rule_dword_count, - SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT - ); - assert_eq!( - probe.post_window_overlap_start_index, - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX - ); - assert_eq!( - probe.post_window_overlap_dword_count, - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - ); - assert_eq!(probe.nonzero_lane_count, 1); - assert_eq!(probe.nonzero_band_indices, vec![35]); - assert!(probe.nonzero_post_window_overlap_band_indices.is_empty()); - assert!( - probe - .nonzero_post_window_overlap_post_relative_offset_hexes - .is_empty() - ); - assert_eq!( - probe.nonzero_lanes[0].lane_kind, - "known-special-condition-dword" - ); - assert_eq!( - probe.nonzero_lanes[0].known_label.as_deref(), - Some("Hidden sentinel") - ); - } - - #[test] - fn parses_nonzero_post_special_conditions_scalar_window() { - let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET]; - let sentinel_offset = - SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; - bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); - bytes[0x0df8..0x0dfc].copy_from_slice(&0x413d298cu32.to_le_bytes()); - bytes[0x0e00..0x0e04].copy_from_slice(&0x40e6b756u32.to_le_bytes()); - bytes[0x0f0c..0x0f10].copy_from_slice(&0x42574909u32.to_le_bytes()); - bytes[0x0f34] = 0xaa; - bytes[0x0f4e] = 0xbb; - - let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) - .expect("special-conditions probe should parse"); - let probe = parse_post_special_conditions_scalar_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&special_conditions_probe), - ) - .expect("post-special-conditions probe should parse"); - - assert_eq!(probe.nonzero_dword_count, 3); - assert_eq!(probe.first_nonzero_offset, Some(0x0df8)); - assert_eq!(probe.last_nonzero_offset, Some(0x0f0c)); - assert_eq!(probe.overlap_nonzero_dword_count, 2); - assert_eq!( - probe.overlap_nonzero_relative_offset_hexes, - vec!["0x4".to_string(), "0xc".to_string()] - ); - assert_eq!(probe.tail_nonzero_dword_count, 1); - assert_eq!(probe.tail_first_nonzero_offset, Some(0x0f0c)); - assert_eq!(probe.tail_last_nonzero_offset, Some(0x0f0c)); - assert_eq!( - probe.tail_nonzero_relative_offset_hexes, - vec!["0x118".to_string()] - ); - assert_eq!(probe.tail_runtime_object_offset_hex, "0x4b47"); - assert_eq!(probe.tail_runtime_object_end_offset_hex, "0x4c4b"); - assert_eq!( - probe.tail_grounded_live_field_copy_end_offset_hex, - "0x4c73".to_string() - ); - assert_eq!( - probe.tail_grounded_live_field_name, - "victory-or-outcome status text buffer" - ); - assert!(probe.tail_window_cuts_through_grounded_live_field); - assert_eq!( - probe.tail_grounded_live_field_remaining_file_window_len_hex, - "0x28" - ); - assert_eq!( - probe.tail_grounded_live_field_remaining_file_window_nonzero_byte_count, - 2 - ); - assert_eq!( - probe.tail_grounded_live_field_remaining_file_window_first_nonzero_offset, - Some(0x0f34) - ); - assert_eq!( - probe.tail_grounded_live_field_remaining_file_window_last_nonzero_offset, - Some(0x0f4e) - ); - assert_eq!(probe.tail_next_grounded_dword_field_file_offset, 0x0f65); - assert_eq!(probe.tail_second_grounded_dword_field_file_offset, 0x0f71); - assert!(!probe.post_text_field_file_alignment_matches_grounded_dword_fields); - assert_eq!(probe.nonzero_lanes[0].relative_offset, 0x04); - assert_eq!(probe.nonzero_lanes[1].relative_offset, 0x0c); - assert_eq!(probe.nonzero_lanes[2].relative_offset, 0x118); - assert!( - probe - .nonzero_lanes - .iter() - .all(|lane| lane.probable_f32_le.is_some()) - ); - } - - #[test] - fn parses_post_text_field_neighborhood_probe() { - let mut bytes = vec![0u8; POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET]; - let sentinel_offset = - SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; - bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); - bytes[0x0f59] = 0x01; - bytes[0x0f5d] = 0x02; - bytes[0x0f61] = 0x03; - bytes[0x0f6d] = 0x04; - bytes[0x0f5c..0x0f60].copy_from_slice(&0x40f33333u32.to_le_bytes()); - bytes[0x0f6c..0x0f70].copy_from_slice(&0x40c08cfbu32.to_le_bytes()); - - let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) - .expect("special-conditions probe should parse"); - let probe = parse_post_text_field_neighborhood_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-scenario-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&special_conditions_probe), - ) - .expect("post-text field neighborhood probe should parse"); - - assert_eq!(probe.window_offset, POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET); - assert_eq!( - probe.window_end_offset, - POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET - ); - assert_eq!(probe.grounded_field_observations.len(), 6); - assert_eq!( - probe.grounded_field_observations[0].field_name, - "Auto-Show Grade During Track Lay" - ); - assert_eq!(probe.grounded_field_observations[0].value_u8, Some(0x01)); - assert_eq!( - probe.grounded_field_observations[3].field_name, - "leftover simulation time accumulator" - ); - assert_eq!(probe.one_byte_early_float_candidates.len(), 2); - assert_eq!( - probe.one_byte_early_float_candidates[0].grounded_field_name, - "Starting Building Density Level" - ); - assert_eq!( - probe.one_byte_early_float_candidates[0].candidate_offset_hex, - "0x0f5c" - ); - assert_eq!( - probe.one_byte_early_float_candidates[1].grounded_field_name, - "selected-year lane snapshot" - ); - assert_eq!( - probe.one_byte_early_float_candidates[1].candidate_offset_hex, - "0x0f6c" - ); - } - - #[test] - fn parses_locomotive_policy_neighborhood_probe() { - let mut bytes = vec![0u8; LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET]; - let sentinel_offset = - SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; - bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); - bytes[0x0f78] = 0x01; - bytes[0x0f7c] = 0x02; - bytes[0x0f7d] = 0x03; - bytes[0x0f7e] = 0x04; - bytes[0x0f9c..0x0fa0].copy_from_slice(&0x42c1c036u32.to_le_bytes()); - bytes[0x0fa0..0x0fa4].copy_from_slice(&0x433a7abeu32.to_le_bytes()); - - let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) - .expect("special-conditions probe should parse"); - let probe = parse_locomotive_policy_neighborhood_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-scenario-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&special_conditions_probe), - ) - .expect("locomotive policy neighborhood probe should parse"); - - assert_eq!(probe.window_offset, LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET); - assert_eq!( - probe.window_end_offset, - LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET - ); - assert_eq!(probe.grounded_field_observations.len(), 9); - assert_eq!( - probe.grounded_field_observations[0].field_name, - "selected-year bucket companion scalar" - ); - assert_eq!( - probe.grounded_field_observations[4].field_name, - "All Steam Locos Avail." - ); - assert_eq!(probe.grounded_field_observations[4].value_u8, Some(0x02)); - assert_eq!( - probe.grounded_field_observations[8].field_name, - "cached available-locomotive rating" - ); - assert_eq!(probe.three_byte_early_float_candidates.len(), 2); - assert_eq!( - probe.three_byte_early_float_candidates[0].grounded_field_name, - "station-list selected station id" - ); - assert_eq!( - probe.three_byte_early_float_candidates[0].candidate_offset_hex, - "0x0f9c" - ); - assert_eq!( - probe.three_byte_early_float_candidates[1].grounded_field_name, - "cached available-locomotive rating" - ); - assert_eq!( - probe.three_byte_early_float_candidates[1].candidate_offset_hex, - "0x0fa0" - ); - } - - #[test] - fn parses_pre_recipe_scalar_plateau_probe() { - let mut bytes = vec![0u8; PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET]; - let sentinel_offset = - SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; - bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); - bytes[0x0fa7..0x0fab].copy_from_slice(&0x82839300u32.to_le_bytes()); - bytes[0x0fab..0x0faf].copy_from_slice(&0x948c9949u32.to_le_bytes()); - bytes[0x0faf..0x0fb3].copy_from_slice(&0x8000003fu32.to_le_bytes()); - bytes[0x0fb3..0x0fb7].copy_from_slice(&0x75c28f3fu32.to_le_bytes()); - bytes[0x0fcb..0x0fcf].copy_from_slice(&0x00300000u32.to_le_bytes()); - bytes[0x0fdb..0x0fdf].copy_from_slice(&0x00ffea22u32.to_le_bytes()); - - let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) - .expect("special-conditions probe should parse"); - let probe = parse_pre_recipe_scalar_plateau_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&special_conditions_probe), - ) - .expect("pre-recipe scalar plateau probe should parse"); - - assert_eq!(probe.window_offset, PRE_RECIPE_SCALAR_PLATEAU_OFFSET); - assert_eq!( - probe.window_end_offset, - PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - ); - assert_eq!(probe.family_signature, "rt3-105-base-pre-recipe-plateau-v1"); - assert_eq!(probe.nonzero_lanes[0].absolute_offset_hex, "0x0fa7"); - assert_eq!(probe.nonzero_lanes[2].absolute_offset_hex, "0x0faf"); - assert_eq!(probe.nonzero_lanes[2].value_hex, "0x8000003f"); - } - - #[test] - fn parses_recipe_book_summary_probe() { - let mut bytes = vec![0u8; RECIPE_BOOK_SUMMARY_END_OFFSET]; - let sentinel_offset = - SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; - bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); - - let book0 = RECIPE_BOOK_ROOT_OFFSET; - bytes[book0..book0 + 16].copy_from_slice(&[ - 0x11, 0x22, 0x33, 0x44, 0xaa, 0xbb, 0xcc, 0xdd, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, - 0x70, 0x80, - ]); - bytes[book0 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET - ..book0 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + 4] - .copy_from_slice(&0x41200000u32.to_le_bytes()); - bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET - ..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + RECIPE_BOOK_LINE_AREA_LEN] - .fill(0xcd); - bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 4] - .copy_from_slice(&0x00000003u32.to_le_bytes()); - bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 4..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 8] - .copy_from_slice(&0x41a00000u32.to_le_bytes()); - bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 8..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 12] - .copy_from_slice(&0x00000017u32.to_le_bytes()); - bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 0x1c - ..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 0x20] - .copy_from_slice(&0x0000002au32.to_le_bytes()); - - let book1 = RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_STRIDE; - bytes[book1 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET - ..book1 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + 4] - .copy_from_slice(&0x00000000u32.to_le_bytes()); - - let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None) - .expect("special-conditions probe should parse"); - let probe = parse_recipe_book_summary_probe( - &bytes, - Some("gmp"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-map-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&special_conditions_probe), - ) - .expect("recipe-book summary probe should parse"); - - assert_eq!(probe.root_offset, RECIPE_BOOK_ROOT_OFFSET); - assert_eq!(probe.book_count, RECIPE_BOOK_COUNT); - assert_eq!(probe.book_stride, RECIPE_BOOK_STRIDE); - assert_eq!( - probe.max_annual_production_relative_offset, - RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET - ); - assert_eq!(probe.books[0].head_kind, "mixed"); - assert_eq!(probe.books[0].line_area_kind, "mixed"); - assert_eq!(probe.books[0].max_annual_production_word_hex, "0x41200000"); - assert_eq!( - probe.books[0] - .max_annual_production_probable_f32_le - .as_deref(), - Some("10.000000") - ); - assert_eq!(probe.books[0].lines.len(), RECIPE_BOOK_LINE_COUNT); - assert_eq!(probe.books[0].lines[0].line_kind, "mixed"); - assert_eq!(probe.books[0].lines[0].mode_word_hex, "0x00000003"); - assert_eq!(probe.books[0].lines[0].annual_amount_word_hex, "0x41a00000"); - assert_eq!( - probe.books[0].lines[0] - .annual_amount_probable_f32_le - .as_deref(), - Some("20.000000") - ); - assert_eq!( - probe.books[0].lines[0].supplied_cargo_token_word_hex, - "0x00000017" - ); - assert_eq!( - probe.books[0].lines[0].supplied_cargo_token_window_hex, - "17000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd2a000000" - ); - assert_eq!( - probe.books[0].lines[0].supplied_cargo_token_window_ascii, - "....................*..." - ); - assert!(probe.books[0].lines[0].supplied_cargo_token_active_in_runtime_import); - assert_eq!( - probe.books[0].lines[0].demanded_cargo_token_word_hex, - "0x0000002a" - ); - assert_eq!( - probe.books[0].lines[0].demanded_cargo_token_window_hex, - "2a000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" - ); - assert_eq!( - probe.books[0].lines[0].demanded_cargo_token_window_ascii, - "*..................." - ); - assert!(probe.books[0].lines[0].demanded_cargo_token_active_in_runtime_import); - assert_eq!(probe.books[1].head_kind, "zero"); - assert_eq!(probe.books[1].line_area_kind, "zero"); - assert_eq!(probe.books[1].lines[0].line_kind, "zero"); - } - - #[test] - fn decodes_probable_recipe_token_high16_ascii_stem() { - assert_eq!( - probable_recipe_token_high16_ascii_stem(0x72470000).as_deref(), - Some("Gr") - ); - assert_eq!( - probable_recipe_token_high16_ascii_stem(0x68430000).as_deref(), - Some("Ch") - ); - assert_eq!(probable_recipe_token_high16_ascii_stem(0x000040a0), None); - assert_eq!(probable_recipe_token_high16_ascii_stem(0x00170000), None); - } - - #[test] - fn classifies_recipe_token_layouts() { - assert_eq!(classify_recipe_token_layout(0x00000000), "zero"); - assert_eq!( - classify_recipe_token_layout(0x72470000), - "high16-ascii-stem" - ); - assert_eq!(classify_recipe_token_layout(0x00170000), "high16-numeric"); - assert_eq!(classify_recipe_token_layout(0x000040a0), "low16-marker"); - } - - #[test] - fn classifies_recipe_line_signatures() { - assert_eq!( - classify_recipe_line_signature(0x00000000, 0x00000000, 0x00010000), - "demand-numeric-entry" - ); - assert_eq!( - classify_recipe_line_signature(0x00000000, 0x00000000, 0x72470000), - "demand-stem-entry" - ); - assert_eq!( - classify_recipe_line_signature(0x00000000, 0x00170000, 0x00000000), - "supply-numeric-entry" - ); - assert_eq!( - classify_recipe_line_signature(0x00110000, 0x000040a0, 0x00000000), - "supply-marker-entry" - ); - } - - #[test] - fn classifies_recipe_runtime_import_branches() { - assert_eq!( - classify_recipe_runtime_import_branch(0), - "zero-mode-skipped" - ); - assert_eq!( - classify_recipe_runtime_import_branch(1), - "mode1-demand-branch" - ); - assert_eq!( - classify_recipe_runtime_import_branch(3), - "mode3-dual-branch" - ); - assert_eq!( - classify_recipe_runtime_import_branch(0x00110000), - "nonzero-supply-branch" - ); - } - - #[test] - fn parses_nonzero_smp_aligned_runtime_rule_band() { - let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET]; - let sentinel_offset = - SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4; - bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes()); - bytes[0x0df8..0x0dfc].copy_from_slice(&0x413d298cu32.to_le_bytes()); - bytes[0x0e00..0x0e04].copy_from_slice(&0x40e6b756u32.to_le_bytes()); - bytes[0x0e18..0x0e1c].copy_from_slice(&0x41d4ccceu32.to_le_bytes()); - bytes[0x0e24..0x0e28].copy_from_slice(&0x3fd2b549u32.to_le_bytes()); - - let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None) - .expect("special-conditions probe should parse"); - let probe = parse_smp_aligned_runtime_rule_band_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-scenario-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&special_conditions_probe), - ) - .expect("aligned runtime-rule band probe should parse"); - - assert_eq!(probe.nonzero_band_indices, vec![35, 37, 39, 45, 48]); - assert_eq!( - probe.nonzero_post_window_overlap_band_indices, - vec![37, 39, 45, 48] - ); - assert_eq!( - probe.nonzero_post_window_overlap_post_relative_offset_hexes, - vec![ - "0x4".to_string(), - "0xc".to_string(), - "0x24".to_string(), - "0x30".to_string() - ] - ); - assert_eq!(probe.nonzero_lanes[1].relative_offset, 0x94); - assert_eq!( - probe.nonzero_lanes[1].lane_kind, - "unlabeled-editor-rule-dword" - ); - assert!(probe.nonzero_lanes[1].probable_f32_le.is_some()); - assert_eq!(probe.nonzero_lanes.last().unwrap().band_index, 48); - } - - #[test] - fn parses_save_anchor_cycle_and_trailer() { - let cycle_words: [u32; 9] = [ - 0x00000000, 0x0186a000, 0x00000000, 0x86a00000, 0x00000001, 0xa0000000, 0x00000186, - 0x00000000, 0x000186a0, - ]; - let trailer_words: [u32; 3] = [0x00020000, 0x00030000, 0x2ee10000]; - let mut bytes = vec![0u8; 0x1c + (cycle_words.len() * 2 + 2 + trailer_words.len()) * 4]; - - let mut cursor = 0x1c; - for _ in 0..2 { - for word in cycle_words { - bytes[cursor..cursor + 4].copy_from_slice(&word.to_le_bytes()); - cursor += 4; - } - } - for word in &cycle_words[..2] { - bytes[cursor..cursor + 4].copy_from_slice(&(*word).to_le_bytes()); - cursor += 4; - } - for word in trailer_words { - bytes[cursor..cursor + 4].copy_from_slice(&word.to_le_bytes()); - cursor += 4; - } - - let container_profile = SmpContainerProfile { - profile_family: "rt3-classic-save-container-v1".to_string(), - profile_evidence: vec!["test".to_string()], - is_known_profile: true, - }; - let bootstrap = SmpSaveBootstrapBlock { - profile_family: "rt3-classic-save-container-v1".to_string(), - aligned_window_offset: 0, - leading_word: 0, - leading_word_hex: "0x00000000".to_string(), - anchor_word: 0, - anchor_word_hex: "0x00000000".to_string(), - descriptor_word_2: 0, - descriptor_word_2_hex: "0x00000000".to_string(), - descriptor_word_3: 0, - descriptor_word_3_hex: "0x00000000".to_string(), - descriptor_word_4: 0, - descriptor_word_4_hex: "0x00000000".to_string(), - descriptor_word_5: 0, - descriptor_word_5_hex: "0x00000000".to_string(), - descriptor_word_6: 0, - descriptor_word_6_hex: "0x00000000".to_string(), - descriptor_word_7: 0, - descriptor_word_7_hex: "0x00000000".to_string(), - }; - - let parsed = - parse_save_anchor_run_block(&bytes, Some(&container_profile), Some(&bootstrap)) - .expect("cycle block should parse"); - - assert_eq!(parsed.cycle_start_offset, 0x1c); - assert_eq!(parsed.cycle_words, cycle_words); - assert_eq!(parsed.full_cycle_count, 2); - assert_eq!(parsed.partial_cycle_word_count, 2); - assert_eq!( - parsed.trailer_offset, - 0x1c + (cycle_words.len() * 2 + 2) * 4 - ); - assert_eq!(parsed.trailer_words, trailer_words); - } - - #[test] - fn classifies_runtime_trailer_family() { - let runtime_anchor_cycle_block = SmpRuntimeAnchorCycleBlock { - profile_family: "rt3-classic-sandbox-container-v1".to_string(), - cycle_start_offset: 0x33c, - cycle_words: vec![0; 9], - cycle_hex_words: vec!["0x00000000".to_string(); 9], - full_cycle_count: 3, - partial_cycle_word_count: 2, - trailer_offset: 0x3b0, - trailer_words: vec![ - 0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, 0x2ee10000, - 0x32c80000, 0x0dcd0000, 0x01010107, 0x26010000, 0x01010107, 0x00010000, 0x0334c68c, - 0x03000000, 0x01000000, - ], - trailer_hex_words: Vec::new(), - }; - let container_profile = SmpContainerProfile { - profile_family: "rt3-classic-sandbox-container-v1".to_string(), - profile_evidence: vec!["test".to_string()], - is_known_profile: true, - }; - - let trailer = parse_runtime_trailer_block( - Some(&container_profile), - Some(&runtime_anchor_cycle_block), - ) - .expect("runtime trailer should parse"); - - assert_eq!(trailer.trailer_family, "rt3-classic-sandbox-trailer-v1"); - assert_eq!(trailer.prefix_words_0_to_5[0], 0x00010000); - assert_eq!(trailer.tag_word_6, 0x2ee10000); - assert_eq!(trailer.tag_chunk_id_u16, 0x2ee1); - assert_eq!(trailer.selector_word_8, 0x0dcd0000); - assert_eq!(trailer.selector_high_u16, 0x0dcd); - assert_eq!(trailer.mode_word_15, 0x01000000); - } - - #[test] - fn probes_runtime_post_span_region() { - let mut bytes = vec![0u8; 0x200]; - bytes[0x90..0x94].copy_from_slice(&0x32dc0000u32.to_le_bytes()); - bytes[0x94..0x98].copy_from_slice(&0x37140000u32.to_le_bytes()); - bytes[0x98..0x9c].copy_from_slice(&0x03000000u32.to_le_bytes()); - bytes[0xa0..0xa4].copy_from_slice(&0x37150000u32.to_le_bytes()); - bytes[0xa4..0xa8].copy_from_slice(&0x00010000u32.to_le_bytes()); - bytes[0xa8..0xac].copy_from_slice(&0x00410000u32.to_le_bytes()); - - let trailer = SmpRuntimeTrailerBlock { - profile_family: "rt3-classic-save-container-v1".to_string(), - trailer_family: "test".to_string(), - trailer_evidence: Vec::new(), - trailer_offset: 0x40, - prefix_words_0_to_5: Vec::new(), - prefix_hex_words_0_to_5: Vec::new(), - tag_word_6: 0x2ee10000, - tag_word_6_hex: "0x2ee10000".to_string(), - tag_chunk_id_u16: 0x2ee1, - tag_chunk_id_hex: "0x2ee1".to_string(), - tag_chunk_id_grounded_alignment: None, - length_word_7: 0x00200000, - length_word_7_hex: "0x00200000".to_string(), - length_high_u16: 0x0020, - length_high_hex: "0x0020".to_string(), - selector_word_8: 0, - selector_word_8_hex: "0x00000000".to_string(), - selector_high_u16: 0, - selector_high_hex: "0x0000".to_string(), - layout_word_9: 0, - layout_word_9_hex: "0x00000000".to_string(), - descriptor_word_10: 0, - descriptor_word_10_hex: "0x00000000".to_string(), - descriptor_high_u16: 0, - descriptor_high_hex: "0x0000".to_string(), - descriptor_word_11: 0, - descriptor_word_11_hex: "0x00000000".to_string(), - counter_word_12: 0, - counter_word_12_hex: "0x00000000".to_string(), - offset_word_13: 0, - offset_word_13_hex: "0x00000000".to_string(), - span_word_14: 0, - span_word_14_hex: "0x00000000".to_string(), - mode_word_15: 0, - mode_word_15_hex: "0x00000000".to_string(), - words: Vec::new(), - hex_words: Vec::new(), - }; - - let probe = parse_runtime_post_span_probe(&bytes, Some(&trailer)) - .expect("post-span probe should parse"); - - assert_eq!(probe.span_target_offset, 0x60); - assert_eq!(probe.next_nonzero_offset, Some(0x92)); - assert_eq!(probe.next_aligned_candidate_offset, Some(0x8c)); - assert_eq!(probe.header_candidates.len(), 1); - assert_eq!(probe.header_candidates[0].dense_word_count, 3); - assert_eq!(probe.header_candidates[0].grounded_alignments.len(), 2); - assert_eq!(probe.grounded_progress_hits[0], "0x32dc@0x00000090"); - } - - #[test] - fn parses_classic_rehydrate_profile_probe() { - let mut bytes = vec![0u8; 0x220]; - bytes[0x90..0x94].copy_from_slice(&0x32dc0000u32.to_le_bytes()); - bytes[0x94..0x98].copy_from_slice(&0x37140000u32.to_le_bytes()); - bytes[0x1a0..0x1a4].copy_from_slice(&0x37150000u32.to_le_bytes()); - bytes[0xab..0xb7].copy_from_slice(b"test-map.gmp"); - bytes[0xde..0xe6].copy_from_slice(b"Test Map"); - - let post_span = SmpRuntimePostSpanProbe { - profile_family: "rt3-classic-save-container-v1".to_string(), - span_target_offset: 0, - next_nonzero_offset: Some(0x92), - next_aligned_candidate_offset: Some(0x8c), - next_aligned_candidate_words: vec![0, 0x32dc0000, 0x37140000, 0x03000000], - next_aligned_candidate_hex_words: vec![], - header_candidates: vec![], - grounded_progress_hits: vec![ - "0x32dc@0x00000090".to_string(), - "0x3714@0x00000094".to_string(), - "0x3715@0x000001a0".to_string(), - ], - }; - - let probe = parse_classic_rehydrate_profile_probe(&bytes, Some(&post_span)) - .expect("classic rehydrate probe should parse"); - - assert_eq!(probe.packed_profile_offset, 0x98); - assert_eq!(probe.packed_profile_len, 0x108); - assert_eq!(probe.ascii_runs[0].preview, "test-map.gmp"); - assert_eq!(probe.packed_profile_block.leading_word_0, 0x00000000); - assert_eq!( - probe.packed_profile_block.map_path.as_deref(), - Some("test-map.gmp") - ); - assert_eq!( - probe.packed_profile_block.display_name.as_deref(), - Some("Test Map") - ); - assert_eq!(probe.packed_profile_block.profile_byte_0x77, 0x00); - assert_eq!(probe.packed_profile_block.profile_byte_0x82, 0x00); - assert_eq!(probe.packed_profile_block.profile_byte_0x97, 0x00); - assert_eq!(probe.packed_profile_block.profile_byte_0xc5, 0x00); - } - - #[test] - fn parses_rt3_105_packed_profile_probe() { - let mut bytes = vec![0u8; 0x9000]; - let block = 0x73c0usize; - bytes[block..block + 4].copy_from_slice(&0x00000003u32.to_le_bytes()); - bytes[block + 0x0c..block + 0x10].copy_from_slice(&0x01000000u32.to_le_bytes()); - bytes[block + 0x10..block + 0x1d].copy_from_slice(b"test-105.gmp\0"); - bytes[block + 0x43..block + 0x4c].copy_from_slice(b"Test 105\0"); - bytes[block + 0x77] = 0x07; - bytes[block + 0x82] = 0x4d; - bytes[block + 0x84..block + 0x88].copy_from_slice(&0x65010000u32.to_le_bytes()); - - let header_variant_probe = SmpHeaderVariantProbe { - variant_family: "rt3-105-common-header-v1".to_string(), - variant_evidence: vec![], - is_known_family: true, - }; - let probe = parse_rt3_105_packed_profile_probe( - &bytes, - Some("gms"), - Some(&header_variant_probe), - None, - ) - .expect("1.05 packed profile probe should parse"); - - assert_eq!(probe.profile_family, "rt3-105-save-analog-block-inferred"); - assert_eq!(probe.packed_profile_offset, 0x73c0); - assert_eq!(probe.packed_profile_len, 0x108); - assert_eq!( - probe.packed_profile_block.map_path.as_deref(), - Some("test-105.gmp") - ); - assert_eq!( - probe.packed_profile_block.display_name.as_deref(), - Some("Test 105") - ); - assert_eq!(probe.packed_profile_block.profile_byte_0x77, 0x07); - assert_eq!(probe.packed_profile_block.profile_byte_0x82, 0x4d); - assert_eq!(probe.packed_profile_block.profile_byte_0x97, 0x00); - assert_eq!(probe.packed_profile_block.profile_byte_0xc5, 0x00); - } - - #[test] - fn builds_classic_save_load_summary() { - let summary = build_save_load_summary( - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-classic-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - None, - None, - Some(&SmpClassicRehydrateProfileProbe { - 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_len_hex: "0x108".to_string(), - packed_profile_block: SmpClassicPackedProfileBlock { - 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: 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![], - }, - ascii_runs: vec![], - }), - None, - None, - ) - .expect("classic summary"); - - assert_eq!(summary.mechanism_family, "classic-save-rehydrate-v1"); - assert_eq!(summary.mechanism_confidence, "grounded"); - assert_eq!(summary.map_path.as_deref(), Some("British Isles.gmp")); - assert_eq!(summary.packed_profile_len, Some(0x108)); - } - - #[test] - fn builds_rt3_105_save_load_summary_with_candidate_table() { - let summary = build_save_load_summary( - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - None, - Some(&SmpRt3105PostSpanBridgeProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(), - bridge_evidence: vec![], - span_target_offset: 0x3678, - next_candidate_offset: Some(0x4f14), - next_candidate_delta_from_span_target: Some(0x189c), - packed_profile_offset: 0x73c0, - packed_profile_delta_from_span_target: 0x3d48, - next_candidate_delta_from_packed_profile: Some(-0x24ac), - selector_high_u16: 0x7110, - selector_high_hex: "0x7110".to_string(), - descriptor_high_u16: 0x7801, - descriptor_high_hex: "0x7801".to_string(), - next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515], - next_candidate_high_hex_words: vec![], - }), - None, - Some(&SmpRt3105PackedProfileProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - packed_profile_offset: 0x73c0, - packed_profile_len: 0x108, - packed_profile_len_hex: "0x108".to_string(), - 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: 1, - header_flag_word_3_hex: "0x00000001".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: 0, - profile_byte_0x97_hex: "0x00".to_string(), - profile_byte_0xc5: 0, - profile_byte_0xc5_hex: "0x00".to_string(), - stable_nonzero_words: vec![], - }, - ascii_runs: vec![], - }), - Some(&SmpRt3105SaveNameTableProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-bridge-secondary-block".to_string(), - semantic_family: "scenario-named-candidate-availability-table".to_string(), - semantic_alignment: vec![], - header_offset: 0x6a70, - header_word_0: 0, - header_word_0_hex: "0x00000000".to_string(), - header_word_1: 0, - header_word_1_hex: "0x00000000".to_string(), - header_word_2: 0x332e, - header_word_2_hex: "0x0000332e".to_string(), - entry_stride: 0x22, - entry_stride_hex: "0x22".to_string(), - header_prefix_word_count: 11, - observed_entry_capacity: 0x44, - observed_entry_count: 67, - zero_trailer_entry_count: 3, - nonzero_trailer_entry_count: 64, - distinct_trailer_words: vec![0, 1], - distinct_trailer_hex_words: vec![ - "0x00000000".to_string(), - "0x00000001".to_string(), - ], - zero_trailer_entry_names: vec![ - "Nuclear Power Plant".to_string(), - "Recycling Plant".to_string(), - "Uranium Mine".to_string(), - ], - entries_offset: 0x6ad1, - entries_end_offset: 0x73b7, - trailing_footer_hex: "dc3200001437000000".to_string(), - footer_progress_word_0: 0x32dc, - footer_progress_word_0_hex: "0x000032dc".to_string(), - footer_progress_word_1: 0x3714, - footer_progress_word_1_hex: "0x00003714".to_string(), - footer_trailing_byte: 0, - footer_trailing_byte_hex: "0x00".to_string(), - footer_grounded_alignments: vec![], - entries: vec![], - evidence: vec![], - }), - ) - .expect("1.05 summary"); - - assert_eq!(summary.mechanism_family, "rt3-105-save-post-span-bridge-v1"); - assert_eq!(summary.mechanism_confidence, "mixed"); - assert_eq!(summary.map_path.as_deref(), Some("Alternate USA.gmp")); - assert_eq!( - summary - .candidate_table - .as_ref() - .expect("candidate table") - .zero_availability_count, - 3 - ); - } - - #[test] - fn loads_classic_save_slice_from_report() { - let mut report = inspect_smp_bytes(&[]); - let classic_probe = SmpClassicRehydrateProfileProbe { - 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_len_hex: "0x108".to_string(), - packed_profile_block: SmpClassicPackedProfileBlock { - 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: 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![], - }, - ascii_runs: vec![], - }; - report.classic_rehydrate_profile_probe = Some(classic_probe.clone()); - report.save_load_summary = build_save_load_summary( - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-classic-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - None, - None, - Some(&classic_probe), - None, - None, - ); - - let slice = load_save_slice_from_report(&report).expect("classic save slice"); - assert_eq!(slice.mechanism_family, "classic-save-rehydrate-v1"); - assert_eq!( - slice - .profile - .as_ref() - .and_then(|profile| profile.map_path.as_deref()), - Some("British Isles.gmp") - ); - assert!(slice.candidate_availability_table.is_none()); - } - - #[test] - fn parses_event_runtime_collection_summary_from_synthetic_chunks() { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - - let header_words = [1u32, 4, 0, 0, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - - bytes.extend_from_slice(&[0x14, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]); - bytes.extend_from_slice(&[0x55, 0x66, 0x77, 0x88]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!(summary.packed_state_version, 0x3e9); - assert_eq!(summary.live_id_bound, 5); - assert_eq!(summary.live_record_count, 3); - assert_eq!(summary.live_entry_ids, vec![1, 3, 5]); - assert_eq!(summary.records_tag_offset, 96); - assert_eq!(summary.decoded_record_count, 0); - assert_eq!(summary.records_with_trigger_kind, 0); - assert_eq!(summary.records_missing_trigger_kind, 3); - assert_eq!(summary.nondirect_compact_record_count, 0); - assert!( - summary - .add_building_dispatch_strip_record_indexes - .is_empty() - ); - assert!( - summary - .add_building_dispatch_strip_descriptor_labels - .is_empty() - ); - assert_eq!(summary.records.len(), 3); - assert_eq!(summary.records[0].decode_status, "unsupported_framing"); - } - - #[test] - fn parses_event_runtime_collection_summary_from_u32_tag_chunks() { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - - let header_words = [1u32, 4, 0, 0, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - - bytes.extend_from_slice(&[0x14, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]); - bytes.extend_from_slice(&[0x55, 0x66, 0x77, 0x88]); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); - bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse from u32 tags"); - - assert_eq!(summary.packed_state_version, 0x3e9); - assert_eq!(summary.live_id_bound, 5); - assert_eq!(summary.live_record_count, 3); - assert_eq!(summary.live_entry_ids, vec![1, 3, 5]); - assert_eq!(summary.records_tag_offset, 98); - assert_eq!(summary.decoded_record_count, 0); - assert_eq!(summary.records_with_trigger_kind, 0); - assert_eq!(summary.records_missing_trigger_kind, 3); - assert_eq!(summary.nondirect_compact_record_count, 0); - assert!( - summary - .add_building_dispatch_strip_record_indexes - .is_empty() - ); - assert!( - summary - .add_building_dispatch_strip_descriptor_labels - .is_empty() - ); - assert_eq!(summary.records.len(), 3); - assert_eq!(summary.records[0].decode_status, "unsupported_framing"); - } - - #[test] - fn parses_nondirect_event_runtime_collection_summary_from_u32_tag_chunks() { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - - let header_words = [ - 0u32, 6, 10, 20, 30, 3, 0, 1, 0, 0, 0, 0, 0, 1, 1, 23, 0, 0, 0, - ]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - - bytes.extend_from_slice(&[0u8; 18]); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); - bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("non-direct event runtime collection summary should parse"); - - assert_eq!( - summary.source_kind, - "packed-event-runtime-collection-nondirect" - ); - assert_eq!(summary.packed_state_version, 0x3e9); - assert_eq!(summary.live_id_bound, 30); - assert_eq!(summary.live_record_count, 3); - assert_eq!(summary.live_entry_ids, vec![1, 2, 3]); - assert_eq!(summary.records_tag_offset, 102); - assert_eq!(summary.decoded_record_count, 0); - assert_eq!(summary.records_with_trigger_kind, 0); - assert_eq!(summary.records_missing_trigger_kind, 3); - assert_eq!(summary.nondirect_compact_record_count, 0); - assert_eq!(summary.nondirect_compact_records_missing_trigger_kind, 0); - assert_eq!(summary.records.len(), 3); - assert_eq!(summary.records[0].decode_status, "unsupported_framing"); - } - - #[test] - fn parses_nondirect_compact_event_runtime_record_rows() { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - - let header_words = [ - 0u32, 6, 10, 20, 30, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 23, 0, 0, 0, - ]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - - bytes.extend_from_slice(&[0u8; 18]); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); - - bytes.extend_from_slice(&(PACKED_EVENT_REAL_CONDITION_MARKER as u32).to_le_bytes()); - bytes.extend_from_slice(&1u32.to_le_bytes()); - bytes.extend_from_slice(&u32::MAX.to_le_bytes()); - bytes.push(4); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.push(2); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&(PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER as u32).to_le_bytes()); - bytes.extend_from_slice(&1u32.to_le_bytes()); - bytes.extend_from_slice(&43u32.to_le_bytes()); - bytes.extend_from_slice(&1u32.to_le_bytes()); - bytes.push(4); - bytes.extend_from_slice(&u32::MAX.to_le_bytes()); - bytes.extend_from_slice(&u32::MAX.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&[0u8; 12]); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&(PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER as u32).to_le_bytes()); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("non-direct event runtime collection summary should parse"); - assert_eq!(summary.records_with_trigger_kind, 0); - assert_eq!(summary.records_missing_trigger_kind, 1); - assert_eq!(summary.nondirect_compact_record_count, 1); - assert_eq!(summary.nondirect_compact_records_missing_trigger_kind, 1); - assert!( - summary - .add_building_dispatch_strip_record_indexes - .is_empty() - ); - assert!( - summary - .add_building_dispatch_strip_descriptor_labels - .is_empty() - ); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("all compact non-direct rows currently decode row bodies only") - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains( - "records with grouped opcodes already in the 0x00431b20 dispatch strip = [0]", - ) - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("decoded grouped rows already reach the 0x00431b20 dispatch strip") - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("every currently decoded dispatch-strip row") - && line.contains("0x4e99/0x4e9a/0x4e9b") - && line.contains("0x4e21/0x4e22") - })); - assert!( - summary - .control_lane_notes - .iter() - .any(|line| { line.contains("0x0042db20 allocates linked 0x1e/0x28 row nodes") }) - ); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("0x004dba23 sits under the event-editor duplication path") - })); - assert!( - summary.control_lane_notes.iter().any(|line| { - line.contains("0x00430b50 allocates a fresh live runtime-effect row") - }) - ); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("0x00443200..0x004436e3") - && line.contains("0x005a57cf") - && line.contains("New Beginnings") - && line.contains("The American") - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("0x004323a0") - && line.contains("[event+0x81f]") - && line.contains("[event+0x7ef]") - && line.contains("0x00432ca1..0x00432cb0") - && line.contains("0x00438710") - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("0x00442c30") - && line.contains("0x00444b50") - && line.contains("0x0062be18/0x0062bae0") - && line.contains("Open Aus") - && line.contains("Win - Gold") - && line.contains("Win - Silver") - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("SP - GOLD") - && line.contains("0x00443526") - && line.contains("1 to 5") - && line.contains("Labor") - && line.contains("0x00443601") - && line.contains("0 to 2") - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("0x00431b20 dispatch-strip opcodes present in decoded grouped rows = [4]") - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains( - "decoded grouped descriptor labels present in the 0x00431b20 dispatch strip = [\"Company Variable 1\"]", - ) - })); - let record = summary - .records - .first() - .expect("first compact non-direct record"); - - assert_eq!(record.decode_status, "parity_only"); - assert_eq!(record.payload_family, "real_packed_nondirect_compact_v1"); - assert_eq!(record.standalone_condition_row_count, 1); - assert_eq!(record.standalone_condition_rows.len(), 1); - assert_eq!(record.standalone_condition_rows[0].raw_condition_id, -1); - assert_eq!(record.standalone_condition_rows[0].subtype, 4); - assert_eq!(record.grouped_effect_row_counts, vec![1, 0, 0, 0]); - assert_eq!(record.grouped_effect_rows.len(), 1); - assert_eq!(record.grouped_effect_rows[0].descriptor_id, 43); - assert_eq!(record.grouped_effect_rows[0].raw_scalar_value, 1); - assert_eq!(record.grouped_effect_rows[0].opcode, 4); - assert!(record.notes.iter().any(|line| { - line.contains("compact non-direct 0x4e99/0x4e9a/0x4e9b map-bundle row framing") - })); - assert!( - record - .notes - .iter() - .any(|line| { line.contains("does not materialize the compact control lane") }) - ); - assert!(record.notes.iter().any(|line| { - line.contains("0x0042e050 reached from editor duplication at 0x004dba23") - })); - assert!( - record.notes.iter().any(|line| { - line.contains("0x00430b50 allocates a fresh live runtime-effect row") - }) - ); - assert!( - record - .notes - .iter() - .any(|line| { line.contains("compact signature family = nondirect-") }) - ); - } - - #[test] - fn summarizes_add_building_dispatch_strip_rows_from_nondirect_compact_records() { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - - let header_words = [ - 0u32, 6, 10, 20, 30, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 23, 0, 0, 0, - ]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - - bytes.extend_from_slice(&[0u8; 18]); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); - bytes.extend_from_slice(&(PACKED_EVENT_REAL_CONDITION_MARKER as u32).to_le_bytes()); - bytes.extend_from_slice(&1u32.to_le_bytes()); - bytes.extend_from_slice(&u32::MAX.to_le_bytes()); - bytes.push(4); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.push(2); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&(PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER as u32).to_le_bytes()); - bytes.extend_from_slice(&1u32.to_le_bytes()); - bytes.extend_from_slice(&548u32.to_le_bytes()); - bytes.extend_from_slice(&1u32.to_le_bytes()); - bytes.push(8); - bytes.extend_from_slice(&u32::MAX.to_le_bytes()); - bytes.extend_from_slice(&u32::MAX.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&[0u8; 12]); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&(PACKED_EVENT_REAL_RECORD_TERMINATOR_MARKER as u32).to_le_bytes()); - bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("non-direct event runtime collection summary should parse"); - - assert_eq!(summary.add_building_dispatch_strip_record_indexes, vec![0]); - assert_eq!( - summary.add_building_dispatch_strip_row_shape_families, - vec!["[0:8:1]".to_string()] - ); - assert_eq!( - summary.add_building_dispatch_strip_signature_families, - vec!["nondirect-ge1e-h0001-ffff-0004-0000-0200-p0000-0000-0000-ffff".to_string()] - ); - assert_eq!( - summary.add_building_dispatch_strip_condition_tuple_families, - vec!["[-1:4]".to_string()] - ); - assert_eq!( - summary.add_building_dispatch_strip_signature_condition_clusters, - vec![ - "nondirect-ge1e-h0001-ffff-0004-0000-0200-p0000-0000-0000-ffff :: [-1:4]" - .to_string() - ] - ); - assert_eq!( - summary.add_building_dispatch_strip_descriptor_labels, - vec!["Add Building Port01".to_string()] - ); - assert_eq!( - summary.add_building_dispatch_strip_records_with_trigger_kind, - 0 - ); - assert_eq!( - summary.add_building_dispatch_strip_records_missing_trigger_kind, - 1 - ); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains( - "records with Add Building descriptors in the 0x00431b20 dispatch strip = [0]", - ) - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains( - "decoded Add Building descriptor labels present in the 0x00431b20 dispatch strip = [\"Add Building Port01\"]", - ) - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains( - "Add Building row-shape families present in the 0x00431b20 dispatch strip = [\"[0:8:1]\"]", - ) - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains( - "Add Building signature families present in the 0x00431b20 dispatch strip = [\"nondirect-ge1e-h0001-ffff-0004-0000-0200-p0000-0000-0000-ffff\"]", - ) - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains( - "Add Building condition-tuple families present in the 0x00431b20 dispatch strip = [\"[-1:4]\"]", - ) - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains( - "Add Building signature/condition clusters present in the 0x00431b20 dispatch strip = [\"nondirect-ge1e-h0001-ffff-0004-0000-0200-p0000-0000-0000-ffff :: [-1:4]\"]", - ) - })); - assert!(summary.control_lane_notes.iter().any(|line| { - line.contains("every currently decoded Add Building dispatch-strip row still has null trigger kind") - })); - } - - fn encode_len_prefixed_string(text: &str) -> Vec { - let mut bytes = Vec::with_capacity(1 + text.len()); - bytes.push(text.len() as u8); - bytes.extend_from_slice(text.as_bytes()); - bytes - } - - fn encode_template( - record_id: u32, - trigger_kind: u8, - flags: u8, - actions: &[Vec], - ) -> Vec { - let mut bytes = Vec::new(); - bytes.extend_from_slice(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC); - bytes.extend_from_slice(&record_id.to_le_bytes()); - bytes.push(trigger_kind); - bytes.push(flags); - bytes.push(actions.len() as u8); - bytes.push(0); - for action in actions { - bytes.extend_from_slice(action); - } - bytes - } - - fn encode_action_set_world_flag(key: &str, value: bool) -> Vec { - let mut bytes = vec![0x01]; - bytes.extend_from_slice(&encode_len_prefixed_string(key)); - bytes.push(u8::from(value)); - bytes - } - - fn encode_action_set_special_condition(label: &str, value: u32) -> Vec { - let mut bytes = vec![0x05]; - bytes.extend_from_slice(&encode_len_prefixed_string(label)); - bytes.extend_from_slice(&value.to_le_bytes()); - bytes - } - - fn encode_action_adjust_company_cash_ids(ids: &[u32], delta: i64) -> Vec { - let mut bytes = vec![0x02, 0x01, ids.len() as u8]; - for id in ids { - bytes.extend_from_slice(&id.to_le_bytes()); - } - bytes.extend_from_slice(&delta.to_le_bytes()); - bytes - } - - fn encode_action_append_template(template: Vec) -> Vec { - let mut bytes = vec![0x06]; - bytes.extend_from_slice(&(template.len() as u32).to_le_bytes()); - bytes.extend_from_slice(&template); - bytes - } - - fn build_synthetic_event_record( - trigger_kind: u8, - flags: u8, - standalone_count: u8, - grouped_counts: [u8; 4], - text_bands: [&[u8]; 6], - actions: &[Vec], - ) -> Vec { - let mut bytes = Vec::new(); - bytes.extend_from_slice(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC); - bytes.push(trigger_kind); - bytes.push(flags); - bytes.push(standalone_count); - bytes.push(actions.len() as u8); - bytes.extend_from_slice(&grouped_counts); - for band in text_bands { - bytes.extend_from_slice(&(band.len() as u16).to_le_bytes()); - bytes.extend_from_slice(band); - } - for action in actions { - bytes.extend_from_slice(action); - } - bytes - } - - fn encode_real_optional_string(text: &str) -> Vec { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&(text.len() as u16).to_le_bytes()); - bytes.extend_from_slice(text.as_bytes()); - bytes - } - - fn build_real_condition_row( - raw_condition_id: i32, - subtype: u8, - flag_seed: u8, - candidate_name: Option<&str>, - ) -> Vec { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&(raw_condition_id as u32).to_le_bytes()); - bytes.push(subtype); - while bytes.len() < PACKED_EVENT_REAL_CONDITION_ROW_LEN { - bytes.push(flag_seed.wrapping_add(bytes.len() as u8)); - } - match candidate_name { - Some(text) => bytes.extend_from_slice(&encode_real_optional_string(text)), - None => bytes.extend_from_slice(&0u16.to_le_bytes()), - } - bytes - } - - fn build_real_condition_row_with_threshold( - raw_condition_id: i32, - subtype: u8, - threshold: i32, - candidate_name: Option<&str>, - ) -> Vec { - let mut bytes = build_real_condition_row(raw_condition_id, subtype, 0, candidate_name); - bytes[5..9].copy_from_slice(&threshold.to_le_bytes()); - bytes - } - - struct RealGroupedEffectRowSpec<'a> { - descriptor_id: u32, - opcode: u8, - raw_scalar_value: i32, - value_byte_0x09: u8, - value_dword_0x0d: u32, - value_byte_0x11: u8, - value_byte_0x12: u8, - value_word_0x14: u16, - value_word_0x16: u16, - locomotive_name: Option<&'a str>, - } - - fn build_real_grouped_effect_row(spec: RealGroupedEffectRowSpec<'_>) -> Vec { - let mut bytes = vec![0; PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN]; - bytes[0..4].copy_from_slice(&spec.descriptor_id.to_le_bytes()); - bytes[4..8].copy_from_slice(&(spec.raw_scalar_value as u32).to_le_bytes()); - bytes[8] = spec.opcode; - bytes[9] = spec.value_byte_0x09; - bytes[0x0d..0x11].copy_from_slice(&spec.value_dword_0x0d.to_le_bytes()); - bytes[0x11] = spec.value_byte_0x11; - bytes[0x12] = spec.value_byte_0x12; - bytes[0x14..0x16].copy_from_slice(&spec.value_word_0x14.to_le_bytes()); - bytes[0x16..0x18].copy_from_slice(&spec.value_word_0x16.to_le_bytes()); - match spec.locomotive_name { - Some(text) => bytes.extend_from_slice(&encode_real_optional_string(text)), - None => bytes.extend_from_slice(&0u16.to_le_bytes()), - } - bytes - } - - #[derive(Clone, Copy)] - struct RealCompactControlSpec { - mode_byte_0x7ef: u8, - primary_selector_0x7f0: u32, - grouped_mode_0x7f4: u8, - one_shot_header_0x7f5: u32, - modifier_flag_0x7f9: u8, - modifier_flag_0x7fa: u8, - grouped_target_scope_ordinals_0x7fb: [u8; PACKED_EVENT_REAL_GROUP_COUNT], - grouped_scope_checkboxes_0x7ff: [u8; PACKED_EVENT_REAL_GROUP_COUNT], - summary_toggle_0x800: u8, - grouped_territory_selectors_0x80f: [i32; PACKED_EVENT_REAL_GROUP_COUNT], - } - - fn build_real_compact_control(spec: RealCompactControlSpec) -> Vec { - let mut bytes = Vec::with_capacity(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN); - bytes.push(spec.mode_byte_0x7ef); - bytes.extend_from_slice(&spec.primary_selector_0x7f0.to_le_bytes()); - bytes.push(spec.grouped_mode_0x7f4); - bytes.extend_from_slice(&spec.one_shot_header_0x7f5.to_le_bytes()); - bytes.push(spec.modifier_flag_0x7f9); - bytes.push(spec.modifier_flag_0x7fa); - bytes.extend_from_slice(&spec.grouped_target_scope_ordinals_0x7fb); - bytes.extend_from_slice(&spec.grouped_scope_checkboxes_0x7ff); - bytes.push(spec.summary_toggle_0x800); - for selector in spec.grouped_territory_selectors_0x80f { - bytes.extend_from_slice(&selector.to_le_bytes()); - } - bytes - } - - fn build_real_event_record( - text_bands: [&[u8]; 6], - compact_control: Option, - condition_rows: &[Vec], - grouped_rows: [&[Vec]; 4], - ) -> Vec { - let mut bytes = Vec::new(); - for band in text_bands { - bytes.extend_from_slice(&(band.len() as u16).to_le_bytes()); - bytes.extend_from_slice(band); - } - if let Some(spec) = compact_control { - bytes.extend_from_slice(&build_real_compact_control(spec)); - } - bytes.extend_from_slice(&PACKED_EVENT_REAL_CONDITION_MARKER.to_le_bytes()); - bytes.extend_from_slice(&(condition_rows.len() as u16).to_le_bytes()); - for row in condition_rows { - bytes.extend_from_slice(row); - } - bytes.extend_from_slice(&PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER.to_le_bytes()); - for rows in grouped_rows { - bytes.extend_from_slice(&(rows.len() as u16).to_le_bytes()); - } - for rows in grouped_rows { - for row in rows { - bytes.extend_from_slice(row); - } - } - bytes - } - - #[test] - fn parses_synthetic_event_runtime_record_summaries_and_actions() { - let append_template = encode_template( - 99, - 0x0a, - 0x01, - &[encode_action_set_special_condition("Imported Follow-On", 1)], - ); - let record_body = build_synthetic_event_record( - 7, - 0x03, - 1, - [0, 1, 0, 0], - [b"Alpha", b"", b"", b"", b"", b""], - &[ - encode_action_set_world_flag("from_packed_root", true), - encode_action_append_template(append_template), - ], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC); - bytes.extend_from_slice(&(record_body.len() as u32).to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!(summary.decoded_record_count, 1); - assert_eq!(summary.imported_runtime_record_count, 1); - assert_eq!(summary.records_with_trigger_kind, 1); - assert_eq!(summary.records_missing_trigger_kind, 0); - assert_eq!(summary.trigger_kinds_present, vec![7]); - assert_eq!(summary.records.len(), 1); - assert_eq!(summary.records[0].decode_status, "executable"); - assert_eq!(summary.records[0].payload_family, "synthetic_harness"); - assert_eq!(summary.records[0].trigger_kind, Some(7)); - assert_eq!(summary.records[0].text_bands[0].preview, "Alpha"); - assert_eq!(summary.records[0].standalone_condition_row_count, 1); - assert_eq!( - summary.records[0].grouped_effect_row_counts, - vec![0, 1, 0, 0] - ); - assert_eq!(summary.records[0].decoded_actions.len(), 2); - match &summary.records[0].decoded_actions[1] { - RuntimeEffect::AppendEventRecord { record } => { - assert_eq!(record.record_id, 99); - assert_eq!(record.trigger_kind, 0x0a); - } - other => panic!("unexpected decoded action: {other:?}"), - } - } - - #[test] - fn decodes_company_targeted_synthetic_records_as_parity_only() { - let record_body = build_synthetic_event_record( - 8, - 0x01, - 0, - [0, 0, 0, 0], - [b"", b"", b"", b"", b"", b""], - &[encode_action_adjust_company_cash_ids(&[7], 25)], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC); - bytes.extend_from_slice(&(record_body.len() as u32).to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!(summary.decoded_record_count, 1); - assert_eq!(summary.imported_runtime_record_count, 1); - assert_eq!(summary.records_with_trigger_kind, 1); - assert_eq!(summary.records_missing_trigger_kind, 0); - assert_eq!(summary.trigger_kinds_present, vec![8]); - assert_eq!(summary.records[0].decode_status, "executable"); - assert_eq!(summary.records[0].payload_family, "synthetic_harness"); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn parses_real_style_event_runtime_record_with_zero_rows() { - let record_body = build_real_event_record( - [b"Alpha", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 1, - modifier_flag_0x7f9: 1, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 1, 2, 3], - grouped_scope_checkboxes_0x7ff: [1, 0, 1, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, 10, -1, 22], - }), - &[], - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!(summary.decoded_record_count, 1); - assert_eq!(summary.imported_runtime_record_count, 0); - assert_eq!(summary.records[0].decode_status, "parity_only"); - assert_eq!(summary.records[0].payload_family, "real_packed_v1"); - assert_eq!(summary.records[0].trigger_kind, Some(7)); - assert_eq!(summary.records[0].one_shot, Some(true)); - assert_eq!( - summary.records[0] - .compact_control - .as_ref() - .expect("real compact control should parse") - .primary_selector_0x7f0, - 0x63 - ); - assert_eq!(summary.records[0].text_bands[0].preview, "Alpha"); - assert_eq!(summary.records[0].standalone_condition_row_count, 0); - assert_eq!(summary.records[0].standalone_condition_rows.len(), 0); - assert!(summary.records[0].negative_sentinel_scope.is_none()); - assert_eq!( - summary.records[0].grouped_effect_row_counts, - vec![0, 0, 0, 0] - ); - assert_eq!(summary.records[0].grouped_effect_rows.len(), 0); - } - - #[test] - fn parses_real_style_rows_and_side_strings() { - let condition_row = build_real_condition_row(-1, 4, 0x30, Some("AutoPlant")); - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 2, - opcode: 8, - raw_scalar_value: 7, - value_byte_0x09: 1, - value_dword_0x0d: 12, - value_byte_0x11: 2, - value_byte_0x12: 3, - value_word_0x14: 24, - value_word_0x16: 36, - locomotive_name: Some("Mikado"), - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"Gamma", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 0x2a, - grouped_mode_0x7f4: 1, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 2, - modifier_flag_0x7fa: 3, - grouped_target_scope_ordinals_0x7fb: [1, 4, 7, 8], - grouped_scope_checkboxes_0x7ff: [0, 1, 0, 1], - summary_toggle_0x800: 0, - grouped_territory_selectors_0x80f: [11, -1, 33, -1], - }), - &[condition_row], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!(summary.records[0].standalone_condition_rows.len(), 1); - assert_eq!( - summary.records[0] - .compact_control - .as_ref() - .expect("real compact control should parse") - .grouped_target_scope_ordinals_0x7fb, - vec![1, 4, 7, 8] - ); - assert_eq!( - summary.records[0].standalone_condition_rows[0].raw_condition_id, - -1 - ); - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .candidate_name - .as_deref(), - Some("AutoPlant") - ); - let negative_sentinel_scope = summary.records[0] - .negative_sentinel_scope - .as_ref() - .expect("negative-sentinel scope summary should decode"); - assert_eq!( - negative_sentinel_scope.company_test_scope, - RuntimeCompanyConditionTestScope::SelectedCompanyOnly - ); - assert_eq!( - negative_sentinel_scope.player_test_scope, - RuntimePlayerConditionTestScope::AiPlayersOnly - ); - assert!(!negative_sentinel_scope.territory_scope_selector_is_0x63); - assert_eq!(negative_sentinel_scope.source_row_indexes, vec![0]); - assert_eq!(summary.records[0].grouped_effect_rows.len(), 1); - assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("Company Cash") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0].target_mask_bits, - Some(0x01) - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0].row_shape, - "multivalue_scalar" - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .semantic_family - .as_deref(), - Some("multivalue_scalar") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .semantic_preview - .as_deref(), - Some("Set Company Cash to 7 with aux [2, 3, 24, 36]") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .locomotive_name - .as_deref(), - Some("Mikado") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::SelectedCompany, - value: 7, - }] - ); - } - - #[test] - fn decodes_real_special_condition_descriptor_from_checked_in_metadata() { - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 108, - opcode: 3, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("Use Wartime Cargos") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .parameter_family - .as_deref(), - Some("special_condition_scalar") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetSpecialCondition { - label: "Use Wartime Cargos".to_string(), - value: 1, - }] - ); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn decodes_real_candidate_availability_descriptor_from_checked_in_metadata() { - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 109, - opcode: 3, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("Turbo Diesel Availability") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .parameter_family - .as_deref(), - Some("candidate_availability_scalar") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetCandidateAvailability { - name: "Turbo Diesel".to_string(), - value: 1, - }] - ); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn decodes_real_special_condition_threshold_from_checked_in_world_condition_metadata() { - let condition_row = build_real_condition_row_with_threshold(3835, 0, 1, None); - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[condition_row], - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .metric - .as_deref(), - Some("Special Condition: Use Wartime Cargos") - ); - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .semantic_family - .as_deref(), - Some("world_state_threshold") - ); - assert_eq!( - summary.records[0].decoded_conditions, - vec![RuntimeCondition::SpecialConditionThreshold { - label: "Use Wartime Cargos".to_string(), - comparator: RuntimeConditionComparator::Ge, - value: 1, - }] - ); - } - - #[test] - fn decodes_real_candidate_availability_threshold_from_checked_in_world_condition_metadata() { - let condition_row = build_real_condition_row_with_threshold( - REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, - 0, - 2, - Some("Mogul"), - ); - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[condition_row], - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .metric - .as_deref(), - Some("Candidate Availability: Mogul") - ); - assert_eq!( - summary.records[0].decoded_conditions, - vec![RuntimeCondition::CandidateAvailabilityThreshold { - name: "Mogul".to_string(), - comparator: RuntimeConditionComparator::Ge, - value: 2, - }] - ); - } - - #[test] - fn decodes_real_economic_status_threshold_from_checked_in_world_condition_metadata() { - let condition_row = build_real_condition_row_with_threshold(2350, 0, 4, None); - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[condition_row], - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .metric - .as_deref(), - Some("Economic Status") - ); - assert_eq!( - summary.records[0].decoded_conditions, - vec![RuntimeCondition::EconomicStatusCodeThreshold { - comparator: RuntimeConditionComparator::Ge, - value: 4, - }] - ); - } - - #[test] - fn decodes_real_named_locomotive_availability_threshold_from_checked_in_metadata() { - let condition_row = build_real_condition_row_with_threshold( - REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID, - 4, - 42, - Some("Big Boy"), - ); - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[condition_row], - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .metric - .as_deref(), - Some("Named Locomotive Availability: Big Boy") - ); - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .semantic_family - .as_deref(), - Some("world_scalar_threshold") - ); - assert_eq!( - summary.records[0].decoded_conditions, - vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name: "Big Boy".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 42, - }] - ); - } - - #[test] - fn decodes_real_named_locomotive_cost_threshold_from_checked_in_metadata() { - let condition_row = build_real_condition_row_with_threshold( - REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID, - 0, - 250000, - Some("Locomotive 1"), - ); - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[condition_row], - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .metric - .as_deref(), - Some("Named Locomotive Cost: Locomotive 1") - ); - assert_eq!( - summary.records[0].decoded_conditions, - vec![RuntimeCondition::NamedLocomotiveCostThreshold { - name: "Locomotive 1".to_string(), - comparator: RuntimeConditionComparator::Ge, - value: 250000, - }] - ); - } - - #[test] - fn decodes_real_named_cargo_production_threshold_from_checked_in_metadata() { - let condition_row = build_real_condition_row_with_threshold( - REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID, - 4, - 125, - Some("Cargo Production Slot 1"), - ); - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[condition_row], - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .metric - .as_deref(), - Some("Cargo Production: Cargo Production Slot 1") - ); - assert_eq!( - summary.records[0].standalone_condition_rows[0].recovered_cargo_slot, - Some(1) - ); - assert_eq!( - summary.records[0].decoded_conditions, - vec![RuntimeCondition::CargoProductionSlotThreshold { - slot: 1, - label: "Cargo Production Slot 1".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 125, - }] - ); - } - - #[test] - fn decodes_real_world_scalar_thresholds_from_checked_in_metadata() { - let condition_rows = vec![ - build_real_condition_row_with_threshold( - REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID, - 0, - 200, - None, - ), - build_real_condition_row_with_threshold( - REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID, - 4, - 125, - None, - ), - build_real_condition_row_with_threshold( - REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID, - 4, - 75, - None, - ), - build_real_condition_row_with_threshold( - REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID, - 4, - 30, - None, - ), - build_real_condition_row_with_threshold( - REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID, - 4, - 18, - None, - ), - build_real_condition_row_with_threshold( - REAL_TERRITORY_ACCESS_COST_CONDITION_ID, - 4, - 750000, - None, - ), - ]; - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &condition_rows, - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .metric - .as_deref(), - Some("Cargo Production Total") - ); - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .semantic_family - .as_deref(), - Some("world_scalar_threshold") - ); - assert_eq!( - summary.records[0].decoded_conditions, - vec![ - RuntimeCondition::CargoProductionTotalThreshold { - comparator: RuntimeConditionComparator::Ge, - value: 200, - }, - RuntimeCondition::FactoryProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 125, - }, - RuntimeCondition::FarmMineProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 75, - }, - RuntimeCondition::OtherCargoProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 30, - }, - RuntimeCondition::LimitedTrackBuildingAmountThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 18, - }, - RuntimeCondition::TerritoryAccessCostThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 750000, - }, - ] - ); - } - - #[test] - fn decodes_real_world_flag_condition_from_checked_in_world_condition_metadata() { - let condition_row = build_real_condition_row_with_threshold(2535, 4, 1, None); - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[condition_row], - [&[], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .metric - .as_deref(), - Some("World Flag: Disable Stock Buying and Selling") - ); - assert_eq!( - summary.records[0].standalone_condition_rows[0] - .semantic_family - .as_deref(), - Some("world_flag_equals") - ); - assert_eq!( - summary.records[0].decoded_conditions, - vec![RuntimeCondition::WorldFlagEquals { - key: "world.disable_stock_buying_and_selling".to_string(), - value: true, - }] - ); - } - - #[test] - fn looks_up_checked_in_world_scalar_condition_metadata() { - let named_cargo = - real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID) - .expect("named cargo condition metadata should exist"); - assert_eq!(named_cargo.label, "%1 Production"); - - let availability = - real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID) - .expect("availability condition metadata should exist"); - assert_eq!(availability.label, "Unknown Loco Available"); - - let cost = real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID) - .expect("cost condition metadata should exist"); - assert_eq!(cost.label, "Unknown Loco Cost"); - - let cargo = real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID) - .expect("cargo condition metadata should exist"); - assert_eq!(cargo.label, "All Cargo Production"); - - let factory = real_ordinary_condition_metadata(REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID) - .expect("factory production condition metadata should exist"); - assert_eq!(factory.label, "All Factory Production"); - - let farm_mine = - real_ordinary_condition_metadata(REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID) - .expect("farm/mine production condition metadata should exist"); - assert_eq!(farm_mine.label, "All Farm/Mine Production"); - - let build_limit = - real_ordinary_condition_metadata(REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID) - .expect("build-limit condition metadata should exist"); - assert_eq!(build_limit.label, "Limited Track Building Amount"); - - let access_cost = real_ordinary_condition_metadata(REAL_TERRITORY_ACCESS_COST_CONDITION_ID) - .expect("territory-access-cost condition metadata should exist"); - assert_eq!(access_cost.label, "Access Rights Cost:"); - } - - #[test] - fn looks_up_checked_in_chairman_and_governance_condition_metadata() { - let world_variable = real_ordinary_condition_metadata(REAL_WORLD_VARIABLE_1_CONDITION_ID) - .expect("world-variable condition metadata should exist"); - assert_eq!(world_variable.label, "Game Variable 1"); - - let player_variable = real_ordinary_condition_metadata(REAL_PLAYER_VARIABLE_3_CONDITION_ID) - .expect("player-variable condition metadata should exist"); - assert_eq!(player_variable.label, "Player Variable 3"); - - let chairman_cash = real_ordinary_condition_metadata(REAL_CHAIRMAN_CASH_CONDITION_ID) - .expect("chairman cash condition metadata should exist"); - assert_eq!(chairman_cash.label, "Player Cash"); - - let holdings = real_ordinary_condition_metadata(REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID) - .expect("chairman holdings condition metadata should exist"); - assert_eq!(holdings.label, "Player Stock Value"); - - let net_worth = real_ordinary_condition_metadata(REAL_CHAIRMAN_NET_WORTH_CONDITION_ID) - .expect("chairman net worth condition metadata should exist"); - assert_eq!(net_worth.label, "Player Net Worth"); - - let purchasing_power = - real_ordinary_condition_metadata(REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID) - .expect("chairman purchasing-power condition metadata should exist"); - assert_eq!(purchasing_power.label, "Purchasing Power"); - - let investor_confidence = - real_ordinary_condition_metadata(REAL_INVESTOR_CONFIDENCE_CONDITION_ID) - .expect("investor-confidence condition metadata should exist"); - assert_eq!(investor_confidence.label, "Investor Confidence"); - - let credit_rating = real_ordinary_condition_metadata(REAL_CREDIT_RATING_CONDITION_ID) - .expect("credit-rating condition metadata should exist"); - assert_eq!(credit_rating.label, "Credit Rating"); - - let prime_rate = real_ordinary_condition_metadata(REAL_PRIME_RATE_CONDITION_ID) - .expect("prime-rate condition metadata should exist"); - assert_eq!(prime_rate.label, "Prime Rate"); - - let management_attitude = - real_ordinary_condition_metadata(REAL_MANAGEMENT_ATTITUDE_CONDITION_ID) - .expect("management-attitude condition metadata should exist"); - assert_eq!(management_attitude.label, "Management Attitude"); - - let book_value = real_ordinary_condition_metadata(REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID) - .expect("book value condition metadata should exist"); - assert_eq!(book_value.label, "Book Value Per Share"); - } - - #[test] - fn decodes_world_variable_condition() { - let row = SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: REAL_WORLD_VARIABLE_1_CONDITION_ID, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&111_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Game Variable 1".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Game Variable 1 == 111".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }; - - assert_eq!( - decode_real_condition_row(&row, None), - Some(RuntimeCondition::WorldVariableThreshold { - index: 1, - comparator: RuntimeConditionComparator::Eq, - value: 111, - }) - ); - } - - #[test] - fn decodes_player_variable_condition_from_selected_player_scope() { - let row = SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: REAL_PLAYER_VARIABLE_3_CONDITION_ID, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&333_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Player Variable 3".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Player Variable 3 == 333".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }; - let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: RuntimeCompanyConditionTestScope::Disabled, - player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, - territory_scope_selector_is_0x63: false, - source_row_indexes: vec![0], - }; - - assert_eq!( - decode_real_condition_row(&row, Some(&negative_scope)), - Some(RuntimeCondition::PlayerVariableThreshold { - target: RuntimePlayerTarget::SelectedPlayer, - index: 3, - comparator: RuntimeConditionComparator::Eq, - value: 333, - }) - ); - } - - #[test] - fn decodes_territory_variable_condition_with_world_territory_scope() { - let row = SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: REAL_TERRITORY_VARIABLE_4_CONDITION_ID, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&444_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Territory Variable 4".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Territory Variable 4 == 444".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }; - let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: RuntimeCompanyConditionTestScope::Disabled, - player_test_scope: RuntimePlayerConditionTestScope::Disabled, - territory_scope_selector_is_0x63: true, - source_row_indexes: vec![0], - }; - - assert_eq!( - decode_real_condition_row(&row, Some(&negative_scope)), - Some(RuntimeCondition::TerritoryVariableThreshold { - target: RuntimeTerritoryTarget::AllTerritories, - index: 4, - comparator: RuntimeConditionComparator::Eq, - value: 444, - }) - ); - } - - #[test] - fn decodes_chairman_cash_condition_from_selected_player_scope() { - let row = SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&500_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Player Cash".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Player Cash == 500".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }; - let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { - company_test_scope: RuntimeCompanyConditionTestScope::Disabled, - player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, - territory_scope_selector_is_0x63: false, - source_row_indexes: vec![0], - }; - - assert_eq!( - decode_real_condition_row(&row, Some(&negative_scope)), - Some(RuntimeCondition::ChairmanNumericThreshold { - target: RuntimeChairmanTarget::SelectedChairman, - metric: RuntimeChairmanMetric::CurrentCash, - comparator: RuntimeConditionComparator::Eq, - value: 500, - }) - ); - } - - #[test] - fn decodes_book_value_per_share_condition_to_company_metric() { - let row = SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Book Value Per Share".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Book Value Per Share == 2620".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }; - - assert_eq!( - decode_real_condition_row(&row, None), - Some(RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - metric: RuntimeCompanyMetric::BookValuePerShare, - comparator: RuntimeConditionComparator::Eq, - value: 2620, - }) - ); - } - - #[test] - fn decodes_investor_confidence_condition_to_company_metric() { - let row = SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: REAL_INVESTOR_CONFIDENCE_CONDITION_ID, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&37_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Investor Confidence".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Investor Confidence == 37".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }; - - assert_eq!( - decode_real_condition_row(&row, None), - Some(RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - metric: RuntimeCompanyMetric::InvestorConfidence, - comparator: RuntimeConditionComparator::Eq, - value: 37, - }) - ); - } - - #[test] - fn decodes_management_attitude_condition_to_company_metric() { - let row = SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: REAL_MANAGEMENT_ATTITUDE_CONDITION_ID, - subtype: 4, - flag_bytes: { - let mut bytes = vec![0; 25]; - bytes[0..4].copy_from_slice(&58_i32.to_le_bytes()); - bytes - }, - candidate_name: None, - comparator: Some("eq".to_string()), - metric: Some("Management Attitude".to_string()), - semantic_family: Some("numeric_threshold".to_string()), - semantic_preview: Some("Test Management Attitude == 58".to_string()), - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }; - - assert_eq!( - decode_real_condition_row(&row, None), - Some(RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::ConditionTrueCompany, - metric: RuntimeCompanyMetric::ManagementAttitude, - comparator: RuntimeConditionComparator::Eq, - value: 58, - }) - ); - } - - #[test] - fn looks_up_checked_in_world_flag_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(110).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Disable Stock Buying and Selling"); - assert_eq!(metadata.parameter_family, "world_flag_toggle"); - assert_eq!( - metadata.runtime_key, - Some("world.disable_stock_buying_and_selling") - ); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_checked_in_credit_rating_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(56).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Credit Rating"); - assert_eq!(metadata.target_mask_bits, 0x0b); - assert_eq!(metadata.parameter_family, "company_governance_scalar"); - assert_eq!( - real_grouped_effect_runtime_status_name(metadata.runtime_status), - "executable" - ); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn checked_in_event_effect_table_covers_the_full_exported_descriptor_set() { - let rows = checked_in_event_effect_descriptor_rows(); - assert_eq!(rows.len(), 614); - for descriptor_id in 0..614_u32 { - assert!( - real_grouped_effect_descriptor_metadata(descriptor_id).is_some(), - "descriptor {descriptor_id} should be recoverable from the checked-in effect table" - ); - } - } - - #[test] - fn looks_up_checked_in_prime_rate_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(57).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Prime Rate"); - assert_eq!(metadata.target_mask_bits, 0x0b); - assert_eq!(metadata.parameter_family, "company_governance_scalar"); - assert_eq!( - real_grouped_effect_runtime_status_name(metadata.runtime_status), - "executable" - ); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn classifies_shell_owned_finance_descriptors_from_checked_in_effect_table() { - let stock_prices = - real_grouped_effect_descriptor_metadata(55).expect("descriptor metadata should exist"); - assert_eq!(stock_prices.label, "Stock Prices"); - assert_eq!( - stock_prices.parameter_family, - "company_finance_shell_scalar" - ); - assert_eq!( - real_grouped_effect_runtime_status_name(stock_prices.runtime_status), - "shell_owned" - ); - assert!(!stock_prices.executable_in_runtime); - - let merger_premium = - real_grouped_effect_descriptor_metadata(58).expect("descriptor metadata should exist"); - assert_eq!(merger_premium.label, "Merger Premium"); - assert_eq!( - merger_premium.parameter_family, - "company_finance_shell_scalar" - ); - assert_eq!( - real_grouped_effect_runtime_status_name(merger_premium.runtime_status), - "shell_owned" - ); - assert!(!merger_premium.executable_in_runtime); - } - - #[test] - fn decodes_credit_rating_descriptor_into_company_governance_scalar_effect() { - let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 56, - raw_scalar_value: 640, - opcode: 3, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let record_body = build_real_event_record( - [b"Gov", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [1, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&[row_bytes], &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("Credit Rating") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .grouped_target_subject - .as_deref(), - Some("company") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget::SelectedCompany, - metric: RuntimeCompanyMetric::CreditRating, - value: 640, - }] - ); - } - - #[test] - fn looks_up_recovered_world_toggle_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(111).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Disable Margin Buying/Short Selling Stock"); - assert_eq!(metadata.parameter_family, "world_flag_toggle"); - assert_eq!( - runtime_world_flag_key(metadata), - Some("world.disable_margin_buying_short_selling_stock".to_string()) - ); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_limited_track_building_amount_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(122).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Limited Track Building Amount"); - assert_eq!(metadata.parameter_family, "world_track_build_limit_scalar"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_recovered_late_world_toggle_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(143).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Disable Train Crashes AND Breakdowns"); - assert_eq!(metadata.parameter_family, "world_flag_toggle"); - assert_eq!( - runtime_world_flag_key(metadata), - Some("world.disable_train_crashes_and_breakdowns".to_string()) - ); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_recovered_locomotive_availability_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(250).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Big Boy 4-8-8-4 Availability"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "locomotive_availability_scalar"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_upper_band_recovered_locomotive_availability_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(457).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Upper-Band Locomotive Availability Slot 1"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "locomotive_availability_scalar"); - assert_eq!(recovered_locomotive_availability_loco_id(457), None); - } - - #[test] - fn looks_up_extended_lower_band_locomotive_availability_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(301).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Zephyr Availability"); - assert_eq!(metadata.parameter_family, "locomotive_availability_scalar"); - assert_eq!(recovered_locomotive_availability_loco_id(301), Some(61)); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn parses_recovered_locomotive_availability_row_with_structured_locomotive_id() { - let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 250, - raw_scalar_value: 1, - opcode: 3, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - - let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None) - .expect("row should parse"); - - assert_eq!(row.descriptor_id, 250); - assert_eq!( - row.descriptor_label.as_deref(), - Some("Big Boy 4-8-8-4 Availability") - ); - assert_eq!(row.recovered_locomotive_id, Some(10)); - assert_eq!( - row.parameter_family.as_deref(), - Some("locomotive_availability_scalar") - ); - } - - #[test] - fn looks_up_recovered_cargo_production_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(230).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Cargo Production Slot 1"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "cargo_production_scalar"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_recovered_all_cargo_price_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(105).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "All Cargo Prices"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "cargo_price_scalar"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_named_cargo_price_slot_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(106).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Alcohol Price"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "cargo_price_scalar"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_runtime_variable_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(43).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Company Variable 1"); - assert_eq!(metadata.target_mask_bits, 0x01); - assert_eq!(metadata.parameter_family, "runtime_variable_scalar"); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_recovered_aggregate_cargo_production_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(177).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "All Cargo Production"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "cargo_production_scalar"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_grounded_named_cargo_production_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(180).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Alcohol Production"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "cargo_production_scalar"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_recovered_lower_band_locomotive_cost_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(352).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "2-D-2 Cost"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "locomotive_cost_scalar"); - assert_eq!(metadata.runtime_key, None); - assert_eq!(recovered_locomotive_cost_loco_id(352), Some(1)); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_recovered_upper_band_locomotive_cost_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(475).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Upper-Band Locomotive Cost Slot 1"); - assert_eq!(metadata.parameter_family, "locomotive_cost_scalar"); - assert_eq!(recovered_locomotive_cost_loco_id(475), None); - assert!(!metadata.executable_in_runtime); - } - - #[test] - fn looks_up_extended_lower_band_locomotive_cost_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(412).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Zephyr Cost"); - assert_eq!(metadata.parameter_family, "locomotive_cost_scalar"); - assert_eq!(recovered_locomotive_cost_loco_id(412), Some(61)); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn looks_up_recovered_territory_access_cost_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(453).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Territory Access Cost"); - assert_eq!(metadata.target_mask_bits, 0x08); - assert_eq!(metadata.parameter_family, "territory_access_cost_scalar"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn parses_recovered_locomotive_cost_row_with_structured_locomotive_id() { - let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 352, - raw_scalar_value: 250000, - opcode: 3, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - - let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None) - .expect("row should parse"); - - assert_eq!(row.descriptor_id, 352); - assert_eq!(row.descriptor_label.as_deref(), Some("2-D-2 Cost")); - assert_eq!(row.recovered_locomotive_id, Some(1)); - assert_eq!( - row.parameter_family.as_deref(), - Some("locomotive_cost_scalar") - ); - } - - #[test] - fn parses_grounded_named_cargo_production_row_with_label() { - let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 180, - raw_scalar_value: 160, - opcode: 3, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - - let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None) - .expect("row should parse"); - - assert_eq!(row.descriptor_id, 180); - assert_eq!(row.descriptor_label.as_deref(), Some("Alcohol Production")); - assert_eq!(row.recovered_cargo_label.as_deref(), Some("Alcohol")); - assert_eq!( - row.parameter_family.as_deref(), - Some("cargo_production_scalar") - ); - } - - #[test] - fn parses_grounded_named_cargo_price_row_with_label() { - let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 106, - raw_scalar_value: 140, - opcode: 3, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - - let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None) - .expect("row should parse"); - - assert_eq!(row.descriptor_id, 106); - assert_eq!(row.descriptor_label.as_deref(), Some("Alcohol Price")); - assert_eq!(row.recovered_cargo_label.as_deref(), Some("Alcohol")); - assert_eq!(row.parameter_family.as_deref(), Some("cargo_price_scalar")); - } - - #[test] - fn looks_up_recovered_locomotive_policy_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(454).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "All Steam Locos Avail."); - assert_eq!(metadata.parameter_family, "world_flag_toggle"); - assert_eq!( - metadata.runtime_key, - Some("world.all_steam_locos_available") - ); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn decodes_recovered_locomotive_policy_descriptor_from_checked_in_metadata() { - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 454, - opcode: 0, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"Locos", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("All Steam Locos Avail.") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .parameter_family - .as_deref(), - Some("world_flag_toggle") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetWorldFlag { - key: "world.all_steam_locos_available".to_string(), - value: true, - }] - ); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn looks_up_checked_in_deactivate_player_descriptor_metadata() { - let metadata = - real_grouped_effect_descriptor_metadata(14).expect("descriptor metadata should exist"); - - assert_eq!(metadata.label, "Deactivate Player"); - assert_eq!(metadata.parameter_family, "player_lifecycle_toggle"); - assert_eq!(metadata.runtime_key, None); - assert!(metadata.executable_in_runtime); - } - - #[test] - fn decodes_real_deactivate_player_descriptor_from_checked_in_metadata() { - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 14, - opcode: 1, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"Players", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [1, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("Deactivate Player") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .parameter_family - .as_deref(), - Some("player_lifecycle_toggle") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::DeactivatePlayer { - target: RuntimePlayerTarget::SelectedPlayer, - }] - ); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn decodes_real_world_flag_descriptor_with_checked_in_runtime_key() { - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 110, - opcode: 0, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("Disable Stock Buying and Selling") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .parameter_family - .as_deref(), - Some("world_flag_toggle") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetWorldFlag { - key: "world.disable_stock_buying_and_selling".to_string(), - value: true, - }] - ); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn decodes_recovered_world_toggle_descriptor_family() { - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 131, - opcode: 0, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("Disable Starting Any Companies") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .parameter_family - .as_deref(), - Some("world_flag_toggle") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetWorldFlag { - key: "world.disable_starting_any_companies".to_string(), - value: true, - }] - ); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn decodes_limited_track_building_amount_descriptor_family() { - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 122, - opcode: 3, - raw_scalar_value: 18, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("Limited Track Building Amount") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .parameter_family - .as_deref(), - Some("world_track_build_limit_scalar") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }] - ); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn decodes_recovered_late_world_toggle_descriptor_family() { - let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 144, - opcode: 0, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }); - let group0_rows = vec![grouped_row]; - let record_body = build_real_event_record( - [b"World", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&group0_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .descriptor_label - .as_deref(), - Some("AI Ignore Territories At Startup") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .parameter_family - .as_deref(), - Some("world_flag_toggle") - ); - assert_eq!( - summary.records[0].decoded_actions, - vec![RuntimeEffect::SetWorldFlag { - key: "world.ai_ignore_territories_at_startup".to_string(), - value: true, - }] - ); - assert!(summary.records[0].executable_import_ready); - } - - #[test] - fn decodes_negative_sentinel_scope_modifiers_and_territory_marker() { - for (value, expected) in [ - (0, RuntimeCompanyConditionTestScope::Disabled), - (1, RuntimeCompanyConditionTestScope::AllCompanies), - (2, RuntimeCompanyConditionTestScope::SelectedCompanyOnly), - (3, RuntimeCompanyConditionTestScope::AiCompaniesOnly), - (4, RuntimeCompanyConditionTestScope::HumanCompaniesOnly), - ] { - assert_eq!(decode_company_condition_test_scope(value), Some(expected)); - } - for (value, expected) in [ - (0, RuntimePlayerConditionTestScope::Disabled), - (1, RuntimePlayerConditionTestScope::AllPlayers), - (2, RuntimePlayerConditionTestScope::SelectedPlayerOnly), - (3, RuntimePlayerConditionTestScope::AiPlayersOnly), - (4, RuntimePlayerConditionTestScope::HumanPlayersOnly), - ] { - assert_eq!(decode_player_condition_test_scope(value), Some(expected)); - } - - let rows = vec![SmpLoadedPackedEventConditionRowSummary { - row_index: 0, - raw_condition_id: -1, - subtype: 4, - flag_bytes: vec![0x30; 25], - candidate_name: Some("AutoPlant".to_string()), - comparator: None, - metric: None, - semantic_family: None, - semantic_preview: None, - recovered_cargo_slot: None, - recovered_cargo_class: None, - requires_candidate_name_binding: false, - notes: vec![], - }]; - let summary = derive_negative_sentinel_scope_summary( - &rows, - &SmpLoadedPackedEventCompactControlSummary { - mode_byte_0x7ef: 6, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 2, - one_shot_header_0x7f5: 1, - modifier_flag_0x7f9: 4, - modifier_flag_0x7fa: 2, - grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3], - grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], - summary_toggle_0x800: 1, - grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], - }, - ) - .expect("negative sentinel summary should derive"); - assert_eq!( - summary.company_test_scope, - RuntimeCompanyConditionTestScope::HumanCompaniesOnly - ); - assert_eq!( - summary.player_test_scope, - RuntimePlayerConditionTestScope::SelectedPlayerOnly - ); - assert!(summary.territory_scope_selector_is_0x63); - assert_eq!(summary.source_row_indexes, vec![0]); - } - - #[test] - fn classifies_real_grouped_row_semantic_families() { - let grouped_rows = vec![ - build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 2, - opcode: 1, - raw_scalar_value: 1, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }), - build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 2, - opcode: 4, - raw_scalar_value: 25, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 2, - value_word_0x16: 6, - locomotive_name: None, - }), - build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 2, - opcode: 3, - raw_scalar_value: 250, - value_byte_0x09: 0, - value_dword_0x0d: 0, - value_byte_0x11: 0, - value_byte_0x12: 0, - value_word_0x14: 0, - value_word_0x16: 0, - locomotive_name: None, - }), - build_real_grouped_effect_row(RealGroupedEffectRowSpec { - descriptor_id: 2, - opcode: 8, - raw_scalar_value: 7, - value_byte_0x09: 1, - value_dword_0x0d: 12, - value_byte_0x11: 2, - value_byte_0x12: 3, - value_word_0x14: 24, - value_word_0x16: 36, - locomotive_name: Some("Mikado"), - }), - ]; - let record_body = build_real_event_record( - [b"Semantic", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 7, - primary_selector_0x7f0: 0x63, - grouped_mode_0x7f4: 1, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [1, 1, 1, 1], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 0, - grouped_territory_selectors_0x80f: [-1, -1, -1, -1], - }), - &[], - [&grouped_rows, &[], &[], &[]], - ); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - let families = summary.records[0] - .grouped_effect_rows - .iter() - .map(|row| row.semantic_family.as_deref().unwrap_or("")) - .collect::>(); - assert_eq!( - families, - vec![ - "bool_toggle", - "timed_duration", - "scalar_assignment", - "multivalue_scalar", - ] - ); - assert_eq!( - summary.records[0].grouped_effect_rows[0] - .semantic_preview - .as_deref(), - Some("Set Company Cash to TRUE") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[1] - .semantic_preview - .as_deref(), - Some("Set Company Cash to 25 for 2 years 6 months") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[2] - .semantic_preview - .as_deref(), - Some("Set Company Cash to 250") - ); - assert_eq!( - summary.records[0].grouped_effect_rows[3] - .semantic_preview - .as_deref(), - Some("Set Company Cash to 7 with aux [2, 3, 24, 36]") - ); - } - - #[test] - fn rejects_truncated_real_style_event_runtime_record() { - let mut record_body = build_real_event_record( - [b"Oops", b"", b"", b"", b"", b""], - Some(RealCompactControlSpec { - mode_byte_0x7ef: 5, - primary_selector_0x7f0: 0, - grouped_mode_0x7f4: 0, - one_shot_header_0x7f5: 0, - modifier_flag_0x7f9: 0, - modifier_flag_0x7fa: 0, - grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], - grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], - summary_toggle_0x800: 0, - grouped_territory_selectors_0x80f: [0, 0, 0, 0], - }), - &[], - [&[], &[], &[], &[]], - ); - record_body.pop(); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); - let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for word in header_words { - bytes.extend_from_slice(&word.to_le_bytes()); - } - bytes.extend_from_slice(&[0x00, 0x00]); - bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); - bytes.extend_from_slice(&record_body); - bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); - - let report = inspect_smp_bytes(&bytes); - let summary = report - .event_runtime_collection_summary - .as_ref() - .expect("event runtime collection summary should parse"); - - assert_eq!(summary.records[0].decode_status, "unsupported_framing"); - assert_eq!(summary.records[0].payload_family, "unsupported_framing"); - } - - #[test] - fn loads_event_runtime_collection_summary_from_report() { - let mut report = inspect_smp_bytes(&[]); - let classic_probe = SmpClassicRehydrateProfileProbe { - 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_len_hex: "0x108".to_string(), - packed_profile_block: SmpClassicPackedProfileBlock { - 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: 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![], - }, - ascii_runs: vec![], - }; - report.classic_rehydrate_profile_probe = Some(classic_probe.clone()); - report.save_load_summary = build_save_load_summary( - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-classic-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - None, - None, - Some(&classic_probe), - None, - None, - ); - report.event_runtime_collection_summary = Some(SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - metadata_tag_offset: 0x7100, - records_tag_offset: 0x7200, - close_tag_offset: 0x7600, - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 5, - live_record_count: 3, - live_entry_ids: vec![1, 3, 5], - decoded_record_count: 0, - imported_runtime_record_count: 0, - records_with_trigger_kind: 0, - records_missing_trigger_kind: 3, - nondirect_compact_record_count: 0, - nondirect_compact_records_missing_trigger_kind: 0, - trigger_kinds_present: vec![], - add_building_dispatch_strip_record_indexes: vec![], - add_building_dispatch_strip_descriptor_labels: vec![], - add_building_dispatch_strip_records_with_trigger_kind: 0, - add_building_dispatch_strip_records_missing_trigger_kind: 0, - add_building_dispatch_strip_row_shape_families: vec![], - add_building_dispatch_strip_signature_families: vec![], - add_building_dispatch_strip_condition_tuple_families: vec![], - add_building_dispatch_strip_signature_condition_clusters: vec![], - control_lane_notes: vec![], - records: build_unsupported_event_runtime_record_summaries(&[1, 3, 5], "test summary"), - }); - - let slice = load_save_slice_from_report(&report).expect("classic save slice"); - assert_eq!( - slice - .event_runtime_collection - .as_ref() - .map(|summary| summary.live_entry_ids.clone()), - Some(vec![1, 3, 5]) - ); - } - - #[test] - fn loads_placed_structure_collection_from_report() { - let mut report = inspect_smp_bytes(&[]); - let classic_probe = SmpClassicRehydrateProfileProbe { - 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_len_hex: "0x108".to_string(), - packed_profile_block: SmpClassicPackedProfileBlock { - 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: 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![], - }, - ascii_runs: vec![], - }; - report.classic_rehydrate_profile_probe = Some(classic_probe.clone()); - report.save_load_summary = build_save_load_summary( - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-classic-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - None, - None, - Some(&classic_probe), - None, - None, - ); - report.save_region_record_triplet_probe = Some(SmpSaveRegionRecordTripletProbe { - profile_family: "rt3-classic-save-container-v1".to_string(), - source_kind: "save-region-record-triplets".to_string(), - semantic_family: "scenario-save-region-record-triplets".to_string(), - records_tag_offset: 0x3400, - close_tag_offset: 0x3500, - record_count: 2, - entries: vec![ - SmpSaveRegionRecordTripletEntryProbe { - record_index: 0, - name: "Marker09".to_string(), - record_payload_relative_offset: 0, - record_payload_relative_offset_hex: "0x0".to_string(), - name_tag_relative_offset: 0, - policy_tag_relative_offset: 0x10, - profile_tag_relative_offset: 0x2e, - pre_name_prefix_len: 0, - pre_name_prefix_hex_bytes: Vec::new(), - pre_name_prefix_dword_candidates: Vec::new(), - policy_chunk_len: 0x1a, - profile_chunk_len: 0x40, - policy_leading_f32_0: 368.0, - policy_leading_f32_1: 0.0, - policy_leading_f32_2: 92.0, - policy_reserved_dwords: vec![0, 0, 0], - policy_reserved_dword_candidates: Vec::new(), - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_collection: Some(SmpSaveRegionProfileCollectionProbe { - direct_collection_flag: 1, - entry_stride: 0x22, - live_id_bound: 18, - live_record_count: 17, - entry_start_relative_offset: 0x4d, - trailing_padding_len: 2, - entries: vec![ - SmpSaveRegionProfileEntryProbe { - entry_index: 0, - row_relative_offset: 0x4d, - name: "House".to_string(), - trailing_weight_f32: 0.2, - }, - SmpSaveRegionProfileEntryProbe { - entry_index: 1, - row_relative_offset: 0x6f, - name: "Farm Corn".to_string(), - trailing_weight_f32: 0.2, - }, - ], - }), - }, - SmpSaveRegionRecordTripletEntryProbe { - record_index: 1, - name: "Marker10".to_string(), - record_payload_relative_offset: 0x6e, - record_payload_relative_offset_hex: "0x6e".to_string(), - name_tag_relative_offset: 0x76, - policy_tag_relative_offset: 0x86, - profile_tag_relative_offset: 0xa4, - pre_name_prefix_len: 8, - pre_name_prefix_hex_bytes: vec![ - "0xaa".to_string(), - "0xbb".to_string(), - "0xcc".to_string(), - "0xdd".to_string(), - ], - pre_name_prefix_dword_candidates: Vec::new(), - policy_chunk_len: 0x1a, - profile_chunk_len: 0x20, - policy_leading_f32_0: 552.0, - policy_leading_f32_1: 0.0, - policy_leading_f32_2: 276.0, - policy_reserved_dwords: vec![0, 4, 0], - policy_reserved_dword_candidates: Vec::new(), - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_collection: Some(SmpSaveRegionProfileCollectionProbe { - direct_collection_flag: 1, - entry_stride: 0x22, - live_id_bound: 26, - live_record_count: 24, - entry_start_relative_offset: 0x50, - trailing_padding_len: 0, - entries: vec![SmpSaveRegionProfileEntryProbe { - entry_index: 0, - row_relative_offset: 0x50, - name: "Farm Corn".to_string(), - trailing_weight_f32: 0.2, - }], - }), - }, - ], - evidence: vec![], - }); - report.save_region_fixed_row_run_candidate_probe = - Some(SmpSaveRegionFixedRowRunCandidateProbe { - profile_family: "rt3-classic-save-container-v1".to_string(), - source_kind: "save-region-fixed-row-run-candidates".to_string(), - semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), - target_row_count: 2, - target_row_stride: 0xbc, - target_row_stride_hex: "0xbc".to_string(), - scan_start_offset: 0x5200, - scan_start_offset_hex: "0x5200".to_string(), - scan_end_offset: 0x5600, - scan_end_offset_hex: "0x5600".to_string(), - candidates: vec![SmpSaveRegionFixedRowRunCandidate { - count_offset: 0x5300, - count_offset_hex: "0x5300".to_string(), - row_count: 2, - row_stride: 0xbc, - row_stride_hex: "0xbc".to_string(), - rows_offset: 0x5310, - rows_offset_hex: "0x5310".to_string(), - rows_end_offset: 0x5488, - rows_end_offset_hex: "0x5488".to_string(), - distance_to_region_metadata_tag: 0x110, - distance_to_region_metadata_tag_hex: "0x110".to_string(), - dword_lane_summaries: vec![], - shape_signature: "dword0:f32,dword1:zero".to_string(), - shape_family_signature: "family-a".to_string(), - trailing_byte_zero_count: 2, - trailing_byte_nonzero_count: 0, - trailing_byte_distinct_value_count: 1, - trailing_byte_sample_values_hex: vec!["0x00".to_string()], - best_probable_density_lane_relative_offset_hex: Some("0x24".to_string()), - }], - evidence: vec![], - }); - report.save_placed_structure_record_triplet_probe = - Some(SmpSavePlacedStructureRecordTripletProbe { - profile_family: "rt3-classic-save-container-v1".to_string(), - source_kind: "save-placed-structure-record-triplets".to_string(), - semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), - records_tag_offset: 0x3600, - close_tag_offset: 0x3800, - record_count: 2, - entries: vec![ - SmpSavePlacedStructureRecordTripletEntryProbe { - record_index: 0, - primary_name: "FarmCorn".to_string(), - secondary_name: "FarmSet".to_string(), - name_tag_relative_offset: 0, - policy_tag_relative_offset: 0x10, - profile_tag_relative_offset: 0x2e, - policy_chunk_len: 0x1a, - profile_chunk_len: 0x10, - policy_f32_lane_0: 1.0, - policy_f32_lane_1: 2.0, - policy_f32_lane_2: 3.0, - policy_f32_lane_3: 4.0, - policy_f32_lane_4: 5.0, - policy_reserved_dword: 0, - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_open_marker: 0x00005dc1, - profile_open_marker_hex: "0x00005dc1".to_string(), - profile_repeated_primary_name: "FarmCorn".to_string(), - profile_repeated_secondary_name: "FarmSet".to_string(), - profile_footer_relative_offset: 0x08, - profile_footer_relative_offset_hex: "0x8".to_string(), - profile_pre_footer_padding_len: 1, - profile_pre_footer_padding_hex_bytes: vec!["0x00".to_string()], - profile_companion_byte_u8: Some(0), - profile_companion_byte_hex: Some("0x00".to_string()), - profile_payload_dword: 0, - profile_payload_dword_hex: "0x00000000".to_string(), - profile_sentinel_i32: 4, - profile_status_kind: "farm_growth_stage_bucket".to_string(), - farm_growth_stage_index: Some(4), - profile_close_marker: 0x00005dc2, - profile_close_marker_hex: "0x00005dc2".to_string(), - }, - SmpSavePlacedStructureRecordTripletEntryProbe { - record_index: 1, - primary_name: "StationA".to_string(), - secondary_name: "StationSetA".to_string(), - name_tag_relative_offset: 0x40, - policy_tag_relative_offset: 0x50, - profile_tag_relative_offset: 0x6e, - policy_chunk_len: 0x1a, - profile_chunk_len: 0x10, - policy_f32_lane_0: 0.0, - policy_f32_lane_1: 0.0, - policy_f32_lane_2: 0.0, - policy_f32_lane_3: 0.0, - policy_f32_lane_4: 0.0, - policy_reserved_dword: 0, - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_open_marker: 0x00005dc1, - profile_open_marker_hex: "0x00005dc1".to_string(), - profile_repeated_primary_name: "StationA".to_string(), - profile_repeated_secondary_name: "StationSetA".to_string(), - profile_footer_relative_offset: 0x08, - profile_footer_relative_offset_hex: "0x8".to_string(), - profile_pre_footer_padding_len: 1, - profile_pre_footer_padding_hex_bytes: vec!["0x07".to_string()], - profile_companion_byte_u8: Some(7), - profile_companion_byte_hex: Some("0x07".to_string()), - profile_payload_dword: 0x00005dc1, - profile_payload_dword_hex: "0x00005dc1".to_string(), - profile_sentinel_i32: 0, - profile_status_kind: "opaque_nondefault".to_string(), - farm_growth_stage_index: None, - profile_close_marker: 0x00005dc2, - profile_close_marker_hex: "0x00005dc2".to_string(), - }, - ], - evidence: vec![], - }); - report.save_placed_structure_dynamic_side_buffer_probe = - Some(SmpSavePlacedStructureDynamicSideBufferProbe { - profile_family: "rt3-classic-save-container-v1".to_string(), - source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(), - semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records" - .to_string(), - metadata_tag_offset: 0x3800, - records_tag_offset: 0x3900, - close_tag_offset: 0x3d00, - records_span_len: 0x400, - direct_record_stride: 6, - direct_record_stride_hex: "0x6".to_string(), - live_id_bound: 0x80, - live_id_bound_hex: "0x00000080".to_string(), - live_record_count: 118, - live_record_count_hex: "0x00000076".to_string(), - owner_shared_dword: 0xff0000ff, - owner_shared_dword_hex: "0xff0000ff".to_string(), - owner_shared_dword_relative_offset: 0, - owner_shared_dword_matches_first_compact_prefix_leading_dword: true, - first_record_child_count_after_owner_shared: Some(1), - first_record_child_count_after_owner_shared_hex: Some("0x0001".to_string()), - first_record_saved_primary_child_byte_after_owner_shared: Some(0xff), - first_record_saved_primary_child_byte_after_owner_shared_hex: Some( - "0xff".to_string(), - ), - first_record_first_name_tag_relative_offset_after_owner_shared: Some(3), - prefix_leading_dword: 0xff0000ff, - prefix_leading_dword_hex: "0xff0000ff".to_string(), - prefix_trailing_word: 1, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - first_embedded_name_tag_relative_offset: 3, - embedded_name_tag_count: 118, - decoded_embedded_name_row_count: 118, - decoded_embedded_name_row_with_tertiary_name_count: 0, - unique_compact_prefix_pattern_count: 4, - prefix_leading_dword_matching_embedded_profile_tag_count: 0, - unique_embedded_name_pair_count: 9, - first_embedded_primary_name: Some("StationA".to_string()), - first_embedded_secondary_name: Some("StationSetA".to_string()), - first_embedded_tertiary_name: None, - embedded_name_row_samples: vec![], - compact_prefix_pattern_summaries: vec![ - SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary { - prefix_leading_dword: 0xff0000ff, - prefix_leading_dword_hex: "0xff0000ff".to_string(), - prefix_trailing_word: 1, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - count: 62, - first_name_tag_relative_offset: 3, - prefix_leading_dword_matches_embedded_profile_tag: false, - section_like_primary_name_count: 12, - cap_like_primary_name_count: 21, - other_primary_name_count: 29, - first_primary_name: Some("StationA".to_string()), - first_secondary_name: Some("StationSetA".to_string()), - }, - ], - name_pair_summaries: vec![SmpSavePlacedStructureDynamicSideBufferNamePairSummary { - primary_name: "StationA".to_string(), - secondary_name: "StationSetA".to_string(), - count: 14, - first_name_tag_relative_offset: 3, - unique_compact_prefix_pattern_count: 1, - dominant_prefix_leading_dword: 0xff0000ff, - dominant_prefix_leading_dword_hex: "0xff0000ff".to_string(), - dominant_prefix_trailing_word: 1, - dominant_prefix_trailing_word_hex: "0x0001".to_string(), - dominant_prefix_separator_byte: 0xff, - dominant_prefix_separator_byte_hex: "0xff".to_string(), - dominant_prefix_count: 14, - }], - payload_envelope_summary: None, - live_entry_prelude_summary: None, - evidence: vec![], - }); - let slice = load_save_slice_from_report(&report).expect("classic save slice"); - let region_collection = slice - .region_collection - .expect("region collection should project"); - assert_eq!(region_collection.source_kind, "save-region-record-triplets"); - assert_eq!(region_collection.observed_entry_count, 2); - assert_eq!(region_collection.entries[0].name, "Marker09"); - assert_eq!(region_collection.entries[1].pre_name_prefix_len, 8); - assert_eq!( - region_collection.entries[1].policy_reserved_dwords, - vec![0, 4, 0] - ); - assert_eq!( - region_collection.entries[0] - .profile_collection - .as_ref() - .map(|collection| collection.entries.len()), - Some(2) - ); - let region_fixed_row_run_summary = slice - .region_fixed_row_run_summary - .expect("region fixed-row summary should project"); - assert_eq!( - region_fixed_row_run_summary.source_kind, - "save-region-fixed-row-run-candidates" - ); - assert_eq!(region_fixed_row_run_summary.candidates.len(), 1); - assert_eq!( - region_fixed_row_run_summary.candidates[0].rows_offset_hex, - "0x5310" - ); - let collection = slice - .placed_structure_collection - .expect("placed structure collection should project"); - assert_eq!( - collection.source_kind, - "save-placed-structure-record-triplets" - ); - assert_eq!(collection.observed_entry_count, 2); - assert_eq!(collection.entries[0].primary_name, "FarmCorn"); - assert_eq!(collection.entries[0].farm_growth_stage_index, Some(4)); - assert_eq!(collection.entries[1].profile_companion_byte_u8, Some(7)); - let side_buffer_summary = slice - .placed_structure_dynamic_side_buffer_summary - .expect("side-buffer summary should project"); - assert_eq!( - side_buffer_summary.source_kind, - "save-placed-structure-dynamic-side-buffer-records" - ); - assert_eq!(side_buffer_summary.observed_entry_count, 118); - assert_eq!(side_buffer_summary.unique_embedded_name_pair_count, 9); - assert_eq!(side_buffer_summary.triplet_alignment_overlap_count, 1); - assert_eq!( - side_buffer_summary.triplet_alignment_side_buffer_only_name_pair_count, - 0 - ); - assert!(slice.notes.iter().any(|line| { - line.contains("loaded region triplet rows as first-class context") - && line.contains("3 embedded profile rows") - })); - assert!(slice.notes.iter().any(|line| { - line.contains("region fixed-row run summary") && line.contains("Some(\"0x5310\")") - })); - assert!(slice.notes.iter().any(|line| { - line.contains("placed-structure triplet rows as first-class context") - && line.contains("2") - })); - assert!(slice.notes.iter().any(|line| { - line.contains("placed-structure dynamic side-buffer summary") - && line.contains("118 decoded name rows") - })); - } - - #[test] - fn loads_rt3_105_save_slice_from_report() { - let mut report = inspect_smp_bytes(&[]); - let packed_profile = SmpRt3105PackedProfileProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - packed_profile_offset: 0x73c0, - packed_profile_len: 0x108, - packed_profile_len_hex: "0x108".to_string(), - 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: 1, - header_flag_word_3_hex: "0x00000001".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: 0, - profile_byte_0x97_hex: "0x00".to_string(), - profile_byte_0xc5: 0, - profile_byte_0xc5_hex: "0x00".to_string(), - stable_nonzero_words: vec![], - }, - ascii_runs: vec![], - }; - let name_table = SmpRt3105SaveNameTableProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-bridge-secondary-block".to_string(), - semantic_family: "scenario-named-candidate-availability-table".to_string(), - semantic_alignment: vec![], - header_offset: 0x6a70, - header_word_0: 0, - header_word_0_hex: "0x00000000".to_string(), - header_word_1: 0, - header_word_1_hex: "0x00000000".to_string(), - header_word_2: 0x332e, - header_word_2_hex: "0x0000332e".to_string(), - entry_stride: 0x22, - entry_stride_hex: "0x22".to_string(), - header_prefix_word_count: 11, - observed_entry_capacity: 0x44, - observed_entry_count: 2, - zero_trailer_entry_count: 1, - nonzero_trailer_entry_count: 1, - distinct_trailer_words: vec![0, 1], - distinct_trailer_hex_words: vec!["0x00000000".to_string(), "0x00000001".to_string()], - zero_trailer_entry_names: vec!["Uranium Mine".to_string()], - entries_offset: 0x6ad1, - entries_end_offset: 0x6b15, - trailing_footer_hex: "dc3200001437000000".to_string(), - footer_progress_word_0: 0x32dc, - footer_progress_word_0_hex: "0x000032dc".to_string(), - footer_progress_word_1: 0x3714, - footer_progress_word_1_hex: "0x00003714".to_string(), - footer_trailing_byte: 0, - footer_trailing_byte_hex: "0x00".to_string(), - footer_grounded_alignments: vec![], - entries: vec![ - SmpRt3105SaveNameTableEntry { - index: 0, - offset: 0x6ad1, - text: "AutoPlant".to_string(), - availability_dword: 1, - availability_dword_hex: "0x00000001".to_string(), - trailer_word: 1, - trailer_word_hex: "0x00000001".to_string(), - }, - SmpRt3105SaveNameTableEntry { - index: 1, - offset: 0x6af3, - text: "Uranium Mine".to_string(), - availability_dword: 0, - availability_dword_hex: "0x00000000".to_string(), - trailer_word: 0, - trailer_word_hex: "0x00000000".to_string(), - }, - ], - evidence: vec![], - }; - let named_locomotive_table = SmpRt3105SaveNamedLocomotiveAvailabilityProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-direct-locomotive-row-run".to_string(), - semantic_family: "scenario-named-locomotive-availability-table".to_string(), - semantic_alignment: vec![], - entries_offset: 0x7c78, - entry_stride: RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE, - entry_stride_hex: format!("0x{:x}", RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE), - observed_entry_count: 2, - zero_availability_count: 1, - zero_availability_names: vec!["Big Boy".to_string()], - entries_end_offset: 0x7c78 + 2 * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE, - entries: vec![ - SmpRt3105SaveNameTableEntry { - index: 0, - offset: 0x7c78, - text: "Big Boy".to_string(), - availability_dword: 0, - availability_dword_hex: "0x00000000".to_string(), - trailer_word: 0, - trailer_word_hex: "0x00000000".to_string(), - }, - SmpRt3105SaveNameTableEntry { - index: 1, - offset: 0x7cb9, - text: "GP7".to_string(), - availability_dword: 1, - availability_dword_hex: "0x00000001".to_string(), - trailer_word: 1, - trailer_word_hex: "0x00000001".to_string(), - }, - ], - evidence: vec![], - }; - let bridge = SmpRt3105PostSpanBridgeProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(), - bridge_evidence: vec![], - span_target_offset: 0x3678, - next_candidate_offset: Some(0x4f14), - next_candidate_delta_from_span_target: Some(0x189c), - packed_profile_offset: 0x73c0, - packed_profile_delta_from_span_target: 0x3d48, - next_candidate_delta_from_packed_profile: Some(-0x24ac), - selector_high_u16: 0x7110, - selector_high_hex: "0x7110".to_string(), - descriptor_high_u16: 0x7801, - descriptor_high_hex: "0x7801".to_string(), - next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515], - next_candidate_high_hex_words: vec![], - }; - report.rt3_105_packed_profile_probe = Some(packed_profile.clone()); - report.rt3_105_save_name_table_probe = Some(name_table.clone()); - report.rt3_105_save_named_locomotive_availability_probe = - Some(named_locomotive_table.clone()); - report.save_load_summary = build_save_load_summary( - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - None, - Some(&bridge), - None, - Some(&packed_profile), - Some(&name_table), - ); - - let slice = load_save_slice_from_report(&report).expect("1.05 save slice"); - assert_eq!(slice.mechanism_family, "rt3-105-save-post-span-bridge-v1"); - assert_eq!( - slice - .profile - .as_ref() - .and_then(|profile| profile.map_path.as_deref()), - Some("Alternate USA.gmp") - ); - assert_eq!( - slice - .candidate_availability_table - .as_ref() - .expect("candidate table") - .entries[1] - .text, - "Uranium Mine" - ); - assert_eq!( - slice - .named_locomotive_availability_table - .as_ref() - .expect("named locomotive availability table") - .entries[1] - .text, - "GP7" - ); - assert_eq!( - slice - .locomotive_catalog - .as_ref() - .expect("derived locomotive catalog") - .entries[0] - .name, - "Big Boy" - ); - assert_eq!( - slice - .locomotive_catalog - .as_ref() - .expect("derived locomotive catalog") - .entries[1] - .locomotive_id, - 2 - ); - } - - #[test] - fn parses_save_world_selection_context_probe_from_fixed_world_block() { - let mut bytes = vec![0u8; 0x8000]; - let chunk_tag_offset = 0x3ceusize; - let payload_offset = chunk_tag_offset + 4; - bytes[chunk_tag_offset..chunk_tag_offset + 4] - .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes()); - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET - ..payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET + 4] - .copy_from_slice(&7u32.to_le_bytes()); - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET - ..payload_offset - + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET - + 4] - .copy_from_slice(&9u32.to_le_bytes()); - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET - ..payload_offset - + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET - + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT] - .copy_from_slice(&[3, 1, 4, 1, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET] = 1; - for (slot_index, role_gate) in [2u8, 1, 0, 2].into_iter().enumerate() { - bytes[payload_offset - + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET - + slot_index * RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE] = role_gate; - } - let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; - bytes[next_chunk_offset..next_chunk_offset + 4] - .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); - - let probe = parse_save_world_selection_context_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("selection-context probe should parse"); - - assert_eq!(probe.chunk_tag_offset, chunk_tag_offset); - assert_eq!(probe.payload_offset, payload_offset); - assert_eq!(probe.selected_company_id, 7); - assert_eq!(probe.selected_chairman_profile_id, 9); - assert_eq!(probe.chairman_slot_selectors[..6], [3, 1, 4, 1, 5, 9]); - assert_eq!(probe.campaign_override_flag, 1); - assert_eq!(probe.chairman_role_gate_bytes[..4], [2, 1, 0, 2]); - } - - #[test] - fn parses_save_world_economic_tuning_probe_from_fixed_world_block() { - let mut bytes = vec![0u8; 0x8000]; - let chunk_tag_offset = 0x3ceusize; - let payload_offset = chunk_tag_offset + 4; - bytes[chunk_tag_offset..chunk_tag_offset + 4] - .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes()); - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET - ..payload_offset + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET + 4] - .copy_from_slice(&1.5f32.to_bits().to_le_bytes()); - for (lane_index, relative_offset) in - RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS - .iter() - .copied() - .enumerate() - { - let value = (lane_index as f32) + 10.25f32; - bytes[payload_offset + relative_offset..payload_offset + relative_offset + 4] - .copy_from_slice(&value.to_bits().to_le_bytes()); - } - let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; - bytes[next_chunk_offset..next_chunk_offset + 4] - .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); - - let probe = parse_save_world_economic_tuning_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("world economic tuning probe should parse"); - - assert_eq!(probe.chunk_tag_offset, chunk_tag_offset); - assert_eq!(probe.payload_offset, payload_offset); - assert_eq!(probe.mirror_lane.relative_offset_hex, "0xbda"); - assert_eq!(probe.mirror_lane.value_f32, 1.5); - assert_eq!(probe.tuning_lanes.len(), 6); - assert_eq!(probe.tuning_lanes[0].relative_offset_hex, "0xbde"); - assert_eq!(probe.tuning_lanes[0].value_f32, 10.25); - assert_eq!(probe.tuning_lanes[5].relative_offset_hex, "0xbf2"); - assert_eq!(probe.tuning_lanes[5].value_f32, 15.25); - } - - #[test] - fn parses_save_world_issue_37_probe_from_fixed_world_block() { - let mut bytes = vec![0u8; 0x8000]; - let chunk_tag_offset = 0x3ceusize; - let payload_offset = chunk_tag_offset + 4; - bytes[chunk_tag_offset..chunk_tag_offset + 4] - .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes()); - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET - ..payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 4] - .copy_from_slice(&3u32.to_le_bytes()); - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET - ..payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET + 4] - .copy_from_slice(&0.06f32.to_bits().to_le_bytes()); - let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; - bytes[next_chunk_offset..next_chunk_offset + 4] - .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); - - let probe = parse_save_world_issue_37_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("world issue-0x37 probe should parse"); - - assert_eq!(probe.chunk_tag_offset, chunk_tag_offset); - assert_eq!(probe.payload_offset, payload_offset); - assert_eq!(probe.issue_value_lane.relative_offset_hex, "0x29"); - assert_eq!(probe.issue_value_lane.value_i32, 3); - assert_eq!(probe.issue_37_raw_u8, 3); - assert_eq!(probe.issue_37_raw_hex, "0x03"); - assert_eq!(probe.issue_38_raw_u8, 0); - assert_eq!(probe.issue_38_raw_hex, "0x00"); - assert_eq!(probe.issue_39_raw_u8, 0); - assert_eq!(probe.issue_39_raw_hex, "0x00"); - assert_eq!(probe.issue_3a_raw_u8, 0); - assert_eq!(probe.issue_3a_raw_hex, "0x00"); - assert_eq!(probe.multiplier_lane.relative_offset_hex, "0x25"); - assert!((probe.multiplier_lane.value_f32 - 0.06).abs() < f32::EPSILON); - } - - #[test] - fn parses_save_world_finance_neighborhood_probe_from_fixed_world_block() { - let mut bytes = vec![0u8; 0x8000]; - let chunk_tag_offset = 0x3ceusize; - let payload_offset = chunk_tag_offset + 4; - bytes[chunk_tag_offset..chunk_tag_offset + 4] - .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes()); - for index in 0..RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS { - let relative_offset = - RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET + index * 4; - bytes[payload_offset + relative_offset..payload_offset + relative_offset + 4] - .copy_from_slice(&((index as u32) + 1).to_le_bytes()); - } - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET] = 1; - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET] = 2; - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET] = 3; - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET] = 4; - bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET - ..payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET + 4] - .copy_from_slice(&2u32.to_le_bytes()); - let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; - bytes[next_chunk_offset..next_chunk_offset + 4] - .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); - - let probe = parse_save_world_finance_neighborhood_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("world finance neighborhood probe should parse"); - - assert_eq!(probe.chunk_tag_offset, chunk_tag_offset); - assert_eq!(probe.payload_offset, payload_offset); - assert_eq!( - probe.dword_candidates.len(), - RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS - ); - assert_eq!( - probe.current_calendar_tuple_word_lane.relative_offset_hex, - "0xd" - ); - assert_eq!(probe.packed_year_word_raw_u16, 1); - assert_eq!(probe.packed_year_word_raw_hex, "0x0001"); - assert_eq!(probe.partial_year_progress_raw_u8, 0); - assert_eq!(probe.partial_year_progress_raw_hex, "0x00"); - assert_eq!(probe.stock_policy_raw_u8, 1); - assert_eq!(probe.stock_policy_raw_hex, "0x01"); - assert_eq!(probe.bond_policy_raw_u8, 2); - assert_eq!(probe.bond_policy_raw_hex, "0x02"); - assert_eq!(probe.bankruptcy_policy_raw_u8, 3); - assert_eq!(probe.bankruptcy_policy_raw_hex, "0x03"); - assert_eq!(probe.dividend_policy_raw_u8, 4); - assert_eq!(probe.dividend_policy_raw_hex, "0x04"); - assert_eq!(probe.building_density_growth_setting_lane.raw_u32, 2); - assert_eq!( - probe - .building_density_growth_setting_lane - .relative_offset_hex, - "0x4c78" - ); - assert_eq!(probe.current_calendar_tuple_word_lane.value_i32, 1); - assert_eq!( - probe.current_calendar_tuple_word_2_lane.relative_offset_hex, - "0x11" - ); - assert_eq!(probe.current_calendar_tuple_word_2_lane.value_i32, 2); - assert_eq!(probe.absolute_counter_lane.relative_offset_hex, "0x15"); - assert_eq!(probe.absolute_counter_lane.value_i32, 3); - assert_eq!( - probe.absolute_counter_mirror_lane.relative_offset_hex, - "0x19" - ); - assert_eq!(probe.absolute_counter_mirror_lane.value_i32, 4); - assert_eq!( - probe.dword_candidates[0].label, - "current_calendar_tuple_word" - ); - assert_eq!(probe.dword_candidates[0].relative_offset_hex, "0xd"); - assert_eq!(probe.dword_candidates[0].value_i32, 1); - assert_eq!(probe.dword_candidates[6].label, "issue_0x37_multiplier"); - assert_eq!(probe.dword_candidates[6].relative_offset_hex, "0x25"); - assert_eq!( - probe.dword_candidates[10].label, - "issue_neighbor_candidate_2" - ); - assert_eq!(probe.dword_candidates[10].relative_offset_hex, "0x35"); - assert_eq!(probe.dword_candidates[10].value_i32, 11); - assert_eq!( - probe.dword_candidates[11].label, - "finance_neighborhood_word_12" - ); - assert_eq!(probe.dword_candidates[11].relative_offset_hex, "0x39"); - assert_eq!(probe.dword_candidates[11].value_i32, 12); - assert_eq!( - probe.dword_candidates[16].label, - "finance_neighborhood_word_17" - ); - assert_eq!(probe.dword_candidates[16].relative_offset_hex, "0x4d"); - assert_eq!(probe.dword_candidates[16].value_i32, 17); - } - - #[test] - fn loads_selection_only_company_and_chairman_context_from_save_world_probe() { - let mut report = inspect_smp_bytes(&[]); - report.save_load_summary = Some(SmpSaveLoadSummary { - file_extension_hint: Some("gms".to_string()), - container_profile_family: Some("rt3-105-save-container-v1".to_string()), - mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), - mechanism_confidence: "mixed".to_string(), - packed_profile_kind: None, - packed_profile_family: None, - packed_profile_offset: None, - packed_profile_len: None, - map_path: None, - display_name: None, - profile_byte_0x77: None, - profile_byte_0x77_hex: None, - profile_byte_0x82: None, - profile_byte_0x82_hex: None, - profile_byte_0x97: None, - profile_byte_0x97_hex: None, - profile_byte_0xc5: None, - profile_byte_0xc5_hex: None, - trailer_family: Some("rt3-105-save-trailer-v1".to_string()), - bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), - candidate_table: None, - notes: vec![], - }); - report.save_world_selection_context_probe = Some(SmpSaveWorldSelectionContextProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-selected-company-and-chairman-context".to_string(), - chunk_tag_offset: 0x3ce, - payload_offset: 0x3d2, - payload_len: RT3_SAVE_WORLD_BLOCK_LEN, - payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), - selected_company_id_offset: 0x3ef, - selected_company_id: 1, - selected_company_id_hex: "0x00000001".to_string(), - selected_chairman_profile_id_offset: 0x3f3, - selected_chairman_profile_id: 9, - selected_chairman_profile_id_hex: "0x00000009".to_string(), - chairman_slot_selector_offset: 0x455, - chairman_slot_selectors: vec![3, 1, 4, 1, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - campaign_override_flag_offset: 0x493, - campaign_override_flag: 1, - campaign_override_flag_hex: "0x01".to_string(), - chairman_role_gate_offset: 0xf91, - chairman_role_gate_bytes: vec![2, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - evidence: vec![], - }); - report.save_world_issue_37_probe = Some(SmpSaveWorldIssue37Probe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-save-world-issue-0x37".to_string(), - chunk_tag_offset: 0x3ce, - payload_offset: 0x3d2, - payload_len: RT3_SAVE_WORLD_BLOCK_LEN, - payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), - issue_37_raw_u8: 3, - issue_37_raw_hex: "0x03".to_string(), - issue_38_raw_u8: 1, - issue_38_raw_hex: "0x01".to_string(), - issue_39_raw_u8: 2, - issue_39_raw_hex: "0x02".to_string(), - issue_3a_raw_u8: 4, - issue_3a_raw_hex: "0x04".to_string(), - issue_value_lane: SmpSaveDwordCandidate { - label: "issue_0x37_value".to_string(), - relative_offset: 0x29, - relative_offset_hex: "0x29".to_string(), - raw_u32: 3, - raw_u32_hex: "0x00000003".to_string(), - value_i32: 3, - value_f32: f32::from_bits(3), - }, - multiplier_lane: SmpSaveDwordCandidate { - label: "issue_0x37_multiplier".to_string(), - relative_offset: 0x25, - relative_offset_hex: "0x25".to_string(), - raw_u32: 0x3d75c28f, - raw_u32_hex: "0x3d75c28f".to_string(), - value_i32: 1031127695, - value_f32: 0.06, - }, - issue_opinion_base_terms_raw_i32: Vec::new(), - evidence: vec![], - }); - report.save_world_economic_tuning_probe = Some(SmpSaveWorldEconomicTuningProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-save-world-economic-tuning".to_string(), - chunk_tag_offset: 0x3ce, - payload_offset: 0x3d2, - payload_len: RT3_SAVE_WORLD_BLOCK_LEN, - payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN), - mirror_lane: SmpSaveDwordCandidate { - label: "economic_tuning_mirror_lane_0".to_string(), - relative_offset: 0xbda, - relative_offset_hex: "0xbda".to_string(), - raw_u32: 0x3f46d093, - raw_u32_hex: "0x3f46d093".to_string(), - value_i32: 1061605523, - value_f32: 0.7766201, - }, - tuning_lanes: vec![ - SmpSaveDwordCandidate { - label: "economic_tuning_lane_0".to_string(), - relative_offset: 0xbde, - relative_offset_hex: "0xbde".to_string(), - raw_u32: 0x3f400000, - raw_u32_hex: "0x3f400000".to_string(), - value_i32: 1061158912, - value_f32: 0.75, - }, - SmpSaveDwordCandidate { - label: "economic_tuning_lane_1".to_string(), - relative_offset: 0xbe2, - relative_offset_hex: "0xbe2".to_string(), - raw_u32: 0x3be56042, - raw_u32_hex: "0x3be56042".to_string(), - value_i32: 1004888130, - value_f32: 0.007, - }, - ], - evidence: vec![], - }); - report.save_company_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-company-tagged-header-counts".to_string(), - semantic_family: "scenario-save-company-header-counts".to_string(), - metadata_tag_offset: 0x1000, - records_tag_offset: 0x1100, - close_tag_offset: 0x1200, - direct_collection_flag: 1, - direct_collection_flag_hex: "0x00000001".to_string(), - direct_record_stride: 0x7684, - direct_record_stride_hex: "0x00007684".to_string(), - live_id_bound: 5, - live_id_bound_hex: "0x00000005".to_string(), - live_record_count: 1, - live_record_count_hex: "0x00000001".to_string(), - header_words: vec![1, 0x7684, 5, 5, 5, 1], - header_hex_words: vec![ - "0x00000001".to_string(), - "0x00007684".to_string(), - "0x00000005".to_string(), - "0x00000005".to_string(), - "0x00000005".to_string(), - "0x00000001".to_string(), - ], - evidence: vec![], - }); - report.save_chairman_profile_collection_header_probe = - Some(SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-chairman-profile-tagged-header-counts".to_string(), - semantic_family: "scenario-save-chairman-profile-header-counts".to_string(), - metadata_tag_offset: 0x2000, - records_tag_offset: 0x2100, - close_tag_offset: 0x2200, - direct_collection_flag: 1, - direct_collection_flag_hex: "0x00000001".to_string(), - direct_record_stride: 0xcab, - direct_record_stride_hex: "0x00000cab".to_string(), - live_id_bound: 8, - live_id_bound_hex: "0x00000008".to_string(), - live_record_count: 2, - live_record_count_hex: "0x00000002".to_string(), - header_words: vec![1, 0xcab, 8, 6, 8, 2], - header_hex_words: vec![ - "0x00000001".to_string(), - "0x00000cab".to_string(), - "0x00000008".to_string(), - "0x00000006".to_string(), - "0x00000008".to_string(), - "0x00000002".to_string(), - ], - evidence: vec![], - }); - report.save_train_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-train-tagged-header-counts".to_string(), - semantic_family: "scenario-save-train-header-counts".to_string(), - metadata_tag_offset: 0x3000, - records_tag_offset: 0x3100, - close_tag_offset: 0x3200, - direct_collection_flag: 1, - direct_collection_flag_hex: "0x00000001".to_string(), - direct_record_stride: 0x1d5, - direct_record_stride_hex: "0x000001d5".to_string(), - live_id_bound: 0x32, - live_id_bound_hex: "0x00000032".to_string(), - live_record_count: 0x14, - live_record_count_hex: "0x00000014".to_string(), - header_words: vec![1, 0x1d5, 0x32, 0x14, 0x32, 0x14], - header_hex_words: vec![ - "0x00000001".to_string(), - "0x000001d5".to_string(), - "0x00000032".to_string(), - "0x00000014".to_string(), - "0x00000032".to_string(), - "0x00000014".to_string(), - ], - evidence: vec![], - }); - report.save_train_collection_directory_probe = Some(SmpSaveTrainCollectionDirectoryProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-train-live-directory".to_string(), - semantic_family: "scenario-save-train-live-directory".to_string(), - metadata_tag_offset: 0x3000, - records_tag_offset: 0x3100, - close_tag_offset: 0x3200, - directory_root_dword_index: 16, - directory_entry_dword_count: 3, - live_record_count: 0x14, - live_id_bound: 0x32, - chain_head_live_entry_id: Some(1), - chain_tail_live_entry_id: Some(20), - entries: vec![ - SmpSaveTrainCollectionDirectoryEntryProbe { - live_entry_id: 1, - payload_relative_offset: 0x2af8, - payload_relative_offset_hex: "0x00002af8".to_string(), - payload_absolute_offset: 0x5afc, - previous_live_entry_id: 0, - previous_live_entry_id_hex: "0x00000000".to_string(), - next_live_entry_id: 2, - next_live_entry_id_hex: "0x00000002".to_string(), - }, - SmpSaveTrainCollectionDirectoryEntryProbe { - live_entry_id: 2, - payload_relative_offset: 0x2ee0, - payload_relative_offset_hex: "0x00002ee0".to_string(), - payload_absolute_offset: 0x5ee4, - previous_live_entry_id: 1, - previous_live_entry_id_hex: "0x00000001".to_string(), - next_live_entry_id: 0, - next_live_entry_id_hex: "0x00000000".to_string(), - }, - ], - evidence: vec![], - }); - report.save_region_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-region-tagged-header-counts".to_string(), - semantic_family: "scenario-save-region-header-counts".to_string(), - metadata_tag_offset: 0x5000, - records_tag_offset: 0x5100, - close_tag_offset: 0x5200, - direct_collection_flag: 0, - direct_collection_flag_hex: "0x00000000".to_string(), - direct_record_stride: 0x06, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 0x96, - live_id_bound_hex: "0x00000096".to_string(), - live_record_count: 0x91, - live_record_count_hex: "0x00000091".to_string(), - header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x91], - header_hex_words: vec![ - "0x00000000".to_string(), - "0x00000006".to_string(), - "0x0000000a".to_string(), - "0x00000014".to_string(), - "0x00000096".to_string(), - "0x00000091".to_string(), - ], - evidence: vec![], - }); - report.save_region_record_triplet_probe = Some(SmpSaveRegionRecordTripletProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-region-record-triplets".to_string(), - semantic_family: "scenario-save-region-record-triplets".to_string(), - records_tag_offset: 0x5100, - close_tag_offset: 0x5200, - record_count: 2, - entries: vec![ - SmpSaveRegionRecordTripletEntryProbe { - record_index: 0, - name: "Marker09".to_string(), - record_payload_relative_offset: 0, - record_payload_relative_offset_hex: "0x0".to_string(), - name_tag_relative_offset: 0, - policy_tag_relative_offset: 0x10, - profile_tag_relative_offset: 0x2e, - pre_name_prefix_len: 0, - pre_name_prefix_hex_bytes: Vec::new(), - pre_name_prefix_dword_candidates: Vec::new(), - policy_chunk_len: 0x1a, - profile_chunk_len: 0x40, - policy_leading_f32_0: 368.0, - policy_leading_f32_1: 0.0, - policy_leading_f32_2: 92.0, - policy_reserved_dwords: vec![0, 0, 0], - policy_reserved_dword_candidates: Vec::new(), - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_collection: Some(SmpSaveRegionProfileCollectionProbe { - direct_collection_flag: 1, - entry_stride: 0x22, - live_id_bound: 18, - live_record_count: 17, - entry_start_relative_offset: 0x4d, - trailing_padding_len: 2, - entries: vec![ - SmpSaveRegionProfileEntryProbe { - entry_index: 0, - row_relative_offset: 0x4d, - name: "House".to_string(), - trailing_weight_f32: 0.2, - }, - SmpSaveRegionProfileEntryProbe { - entry_index: 1, - row_relative_offset: 0x6f, - name: "Farm Corn".to_string(), - trailing_weight_f32: 0.2, - }, - ], - }), - }, - SmpSaveRegionRecordTripletEntryProbe { - record_index: 1, - name: "Marker10".to_string(), - record_payload_relative_offset: 0x6e, - record_payload_relative_offset_hex: "0x6e".to_string(), - name_tag_relative_offset: 0x6e, - policy_tag_relative_offset: 0x7e, - profile_tag_relative_offset: 0x9c, - pre_name_prefix_len: 0, - pre_name_prefix_hex_bytes: Vec::new(), - pre_name_prefix_dword_candidates: Vec::new(), - policy_chunk_len: 0x1a, - profile_chunk_len: 0x20, - policy_leading_f32_0: 552.0, - policy_leading_f32_1: 0.0, - policy_leading_f32_2: 276.0, - policy_reserved_dwords: vec![0, 0, 0], - policy_reserved_dword_candidates: Vec::new(), - policy_trailing_word: 1, - policy_trailing_word_hex: "0x0001".to_string(), - profile_collection: Some(SmpSaveRegionProfileCollectionProbe { - direct_collection_flag: 1, - entry_stride: 0x22, - live_id_bound: 26, - live_record_count: 24, - entry_start_relative_offset: 0x50, - trailing_padding_len: 0, - entries: vec![SmpSaveRegionProfileEntryProbe { - entry_index: 0, - row_relative_offset: 0x50, - name: "Farm Corn".to_string(), - trailing_weight_f32: 0.2, - }], - }), - }, - ], - evidence: vec![], - }); - report.save_placed_structure_collection_header_probe = - Some(SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-placed-structure-tagged-header-counts".to_string(), - semantic_family: "scenario-save-placed-structure-header-counts".to_string(), - metadata_tag_offset: 0x4000, - records_tag_offset: 0x4100, - close_tag_offset: 0x4200, - direct_collection_flag: 0, - direct_collection_flag_hex: "0x00000000".to_string(), - direct_record_stride: 0x06, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 0x7ee, - live_id_bound_hex: "0x000007ee".to_string(), - live_record_count: 0x7ea, - live_record_count_hex: "0x000007ea".to_string(), - header_words: vec![0, 6, 0x0a, 0x14, 0x7ee, 0x7ea], - header_hex_words: vec![ - "0x00000000".to_string(), - "0x00000006".to_string(), - "0x0000000a".to_string(), - "0x00000014".to_string(), - "0x000007ee".to_string(), - "0x000007ea".to_string(), - ], - evidence: vec![], - }); - - let slice = load_save_slice_from_report(&report).expect("save slice"); - - let company_roster = slice.company_roster.expect("selection-only company roster"); - assert_eq!(company_roster.observed_entry_count, 1); - assert_eq!(company_roster.selected_company_id, Some(1)); - assert!(company_roster.entries.is_empty()); - - let chairman_table = slice - .chairman_profile_table - .expect("selection-only chairman table"); - assert_eq!(chairman_table.observed_entry_count, 2); - assert_eq!(chairman_table.selected_chairman_profile_id, Some(9)); - assert!(chairman_table.entries.is_empty()); - let issue_37_state = slice - .world_issue_37_state - .expect("world issue-0x37 state should load"); - assert_eq!(issue_37_state.issue_value, 3); - assert_eq!(issue_37_state.issue_38_value, 1); - assert_eq!(issue_37_state.issue_39_value, 2); - assert_eq!(issue_37_state.issue_3a_value, 4); - assert_eq!(issue_37_state.multiplier_raw_hex, "0x3d75c28f"); - assert_eq!(issue_37_state.multiplier_value_f32_text, "0.060000"); - let tuning_state = slice - .world_economic_tuning_state - .expect("world economic tuning state should load"); - assert_eq!(tuning_state.mirror_raw_hex, "0x3f46d093"); - assert_eq!(tuning_state.mirror_value_f32_text, "0.776620"); - assert_eq!( - tuning_state.lane_value_f32_text, - vec!["0.750000", "0.007000"] - ); - - assert!( - slice - .notes - .iter() - .any(|note| note.contains("selected_company_id=1")) - ); - assert!( - slice - .notes - .iter() - .any(|note| note.contains("selected_chairman_profile_id=9")) - ); - assert!( - slice - .notes - .iter() - .any(|note| note.contains("grounded issue-0x37 pair: value=3")) - ); - assert!( - slice - .notes - .iter() - .any(|note| note.contains("campaign_override_flag=1")) - ); - assert!( - slice - .notes - .iter() - .any(|note| note.contains("tagged company header reports live_record_count=1")) - ); - assert!(slice.notes.iter().any(|note| { - note.contains("tagged chairman/profile header reports live_record_count=2") - })); - assert!( - slice - .notes - .iter() - .any(|note| note.contains("tagged train header reports live_record_count=20")) - ); - assert!(slice.notes.iter().any(|note| { - note.contains("tagged train metadata also exposes a live-entry directory") - })); - assert!( - slice.notes.iter().any(|note| { - note.contains("tagged region header reports live_record_count=145") - }) - ); - assert!(slice.notes.iter().any(|note| { - note.contains( - "tagged region records also expose 2 repeated 0x55f1/0x55f2/0x55f3 triplets", - ) - })); - assert!(slice.notes.iter().any(|note| { - note.contains("tagged placed-structure header reports live_record_count=2026") - })); - } - - #[test] - fn parses_company_tagged_collection_header_probe_from_exact_u32_tags() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x180usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x000061a9u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x000061aau32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000061abu32.to_le_bytes()); - let header_words = [ - 1u32, 0x7684, 5, 5, 5, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - - let probe = parse_save_company_collection_header_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("company header probe should parse"); - - assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); - assert_eq!(probe.records_tag_offset, records_tag_offset); - assert_eq!(probe.close_tag_offset, close_tag_offset); - assert_eq!(probe.direct_record_stride, 0x7684); - assert_eq!(probe.live_id_bound, 5); - assert_eq!(probe.live_record_count, 1); - } - - #[test] - fn parses_chairman_profile_tagged_collection_header_probe_from_exact_u32_tags() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x180usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x00005209u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x0000520au32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); - let header_words = [ - 1u32, 0xcab, 8, 6, 8, 2, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - - let probe = parse_save_chairman_profile_collection_header_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("chairman profile header probe should parse"); - - assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); - assert_eq!(probe.records_tag_offset, records_tag_offset); - assert_eq!(probe.close_tag_offset, close_tag_offset); - assert_eq!(probe.direct_record_stride, 0xcab); - assert_eq!(probe.live_id_bound, 8); - assert_eq!(probe.live_record_count, 2); - } - - #[test] - fn parses_train_tagged_collection_header_probe_from_exact_u32_tags() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x180usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x00005209u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x0000520au32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); - let header_words = [ - 1u32, 0x1d5, 0x32, 0x14, 0x32, 0x14, 0x14, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - - let probe = parse_save_train_collection_header_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("train header probe should parse"); - - assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); - assert_eq!(probe.records_tag_offset, records_tag_offset); - assert_eq!(probe.close_tag_offset, close_tag_offset); - assert_eq!(probe.direct_collection_flag, 1); - assert_eq!(probe.direct_record_stride, 0x1d5); - assert_eq!(probe.live_id_bound, 0x32); - assert_eq!(probe.live_record_count, 0x14); - } - - #[test] - fn parses_region_tagged_collection_header_probe_from_marker09_family() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x180usize; - let close_tag_offset = 0x1c0usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x00005209u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x0000520au32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); - let header_words = [ - 0u32, 0x06, 0x0a, 0x14, 0x96, 0x91, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - let marker_offset = records_tag_offset + 4 + 0x20; - bytes[marker_offset..marker_offset + 8].copy_from_slice(b"Marker09"); - - let probe = parse_save_region_collection_header_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("region header probe should parse"); - - assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); - assert_eq!(probe.records_tag_offset, records_tag_offset); - assert_eq!(probe.close_tag_offset, close_tag_offset); - assert_eq!(probe.direct_collection_flag, 0); - assert_eq!(probe.direct_record_stride, 0x06); - assert_eq!(probe.live_id_bound, 0x96); - assert_eq!(probe.live_record_count, 0x91); - } - - #[test] - fn parses_train_collection_directory_probe_from_tagged_metadata_triplets() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x180usize; - let close_tag_offset = 0x1c0usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x00005209u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x0000520au32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); - let header_words = [ - 1u32, 0x1d5, 0x32, 0x03, 0x32, 0x03, 0x14, 1, 0, 0, 1, 1, 0x14, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - let triplets = [(0x2af8u32, 0u32, 2u32), (0x2ee0, 1, 3), (0x32c8, 2, 0)]; - for (index, (offset_word, prev, next)) in triplets.into_iter().enumerate() { - let base = metadata_tag_offset - + 4 - + (SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX + index * 3) * 4; - bytes[base..base + 4].copy_from_slice(&offset_word.to_le_bytes()); - bytes[base + 4..base + 8].copy_from_slice(&prev.to_le_bytes()); - bytes[base + 8..base + 12].copy_from_slice(&next.to_le_bytes()); - } - - let header_probe = parse_save_train_collection_header_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("train header probe should parse"); - let directory_probe = - parse_save_train_collection_directory_probe(&bytes, Some(&header_probe)) - .expect("train directory probe should parse"); - - assert_eq!(directory_probe.directory_root_dword_index, 16); - assert_eq!(directory_probe.live_record_count, 3); - assert_eq!(directory_probe.chain_head_live_entry_id, Some(1)); - assert_eq!(directory_probe.chain_tail_live_entry_id, Some(3)); - assert_eq!(directory_probe.entries.len(), 3); - assert_eq!(directory_probe.entries[0].live_entry_id, 1); - assert_eq!(directory_probe.entries[0].payload_relative_offset, 0x2af8); - assert_eq!(directory_probe.entries[0].previous_live_entry_id, 0); - assert_eq!(directory_probe.entries[0].next_live_entry_id, 2); - assert_eq!(directory_probe.entries[2].live_entry_id, 3); - assert_eq!(directory_probe.entries[2].previous_live_entry_id, 2); - assert_eq!(directory_probe.entries[2].next_live_entry_id, 0); - } - - #[test] - fn parses_region_record_triplet_probe_from_marker09_records() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x260usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x00005209u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x0000520au32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); - let mut cursor = records_tag_offset + 4; - for (name, x, density, y) in [ - ("Marker09", 368.0f32, 0.0f32, 92.0f32), - ("Marker10", 552.0f32, 1.5f32, 276.0f32), - ] { - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); - bytes[cursor + 4] = name.len() as u8; - bytes[cursor + 5..cursor + 5 + name.len()].copy_from_slice(name.as_bytes()); - cursor += 0x10; - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); - bytes[cursor + 4..cursor + 8].copy_from_slice(&x.to_bits().to_le_bytes()); - bytes[cursor + 8..cursor + 12].copy_from_slice(&density.to_bits().to_le_bytes()); - bytes[cursor + 12..cursor + 16].copy_from_slice(&y.to_bits().to_le_bytes()); - bytes[cursor + 28..cursor + 30].copy_from_slice(&1u16.to_le_bytes()); - cursor += 0x1e; - bytes[cursor..cursor + 2] - .copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); - cursor += 0x40; - } - - let header_probe = SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-region-tagged-header-counts".to_string(), - semantic_family: "scenario-save-region-header-counts".to_string(), - metadata_tag_offset, - records_tag_offset, - close_tag_offset, - direct_collection_flag: 0, - direct_collection_flag_hex: "0x00000000".to_string(), - direct_record_stride: 0x06, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 0x96, - live_id_bound_hex: "0x00000096".to_string(), - live_record_count: 2, - live_record_count_hex: "0x00000002".to_string(), - header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x02], - header_hex_words: vec![], - evidence: vec![], - }; - let triplet_probe = parse_save_region_record_triplet_probe(&bytes, Some(&header_probe)) - .expect("region triplet probe should parse"); - - assert_eq!(triplet_probe.record_count, 2); - assert_eq!(triplet_probe.entries[0].name, "Marker09"); - assert_eq!(triplet_probe.entries[0].policy_tag_relative_offset, 0x10); - assert_eq!(triplet_probe.entries[0].profile_tag_relative_offset, 0x2e); - assert_eq!(triplet_probe.entries[0].policy_leading_f32_0, 368.0); - assert_eq!(triplet_probe.entries[0].policy_leading_f32_1, 0.0); - assert_eq!(triplet_probe.entries[0].policy_leading_f32_2, 92.0); - assert!( - triplet_probe.entries[0] - .pre_name_prefix_dword_candidates - .is_empty() - ); - assert_eq!( - triplet_probe.entries[0].policy_reserved_dwords, - vec![0, 0, 0] - ); - assert_eq!( - triplet_probe.entries[0] - .policy_reserved_dword_candidates - .len(), - 3 - ); - assert_eq!( - triplet_probe.entries[0].policy_reserved_dword_candidates[0].relative_offset_hex, - "0x20" - ); - assert_eq!(triplet_probe.entries[0].policy_trailing_word, 1); - assert_eq!(triplet_probe.entries[1].name, "Marker10"); - assert_eq!(triplet_probe.entries[1].policy_leading_f32_0, 552.0); - assert_eq!(triplet_probe.entries[1].policy_leading_f32_1, 1.5); - assert_eq!(triplet_probe.entries[1].policy_leading_f32_2, 276.0); - assert!( - triplet_probe.entries[1] - .pre_name_prefix_dword_candidates - .is_empty() - ); - assert_eq!( - triplet_probe.entries[1] - .policy_reserved_dword_candidates - .len(), - 3 - ); - } - - #[test] - fn parses_region_record_triplet_prefix_dword_candidates() { - let mut bytes = vec![0u8; 0x320]; - let metadata_tag_offset = 0x0usize; - let records_tag_offset = 0x100usize; - let close_tag_offset = 0x200usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x00005209u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x0000520au32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); - let mut cursor = records_tag_offset + 4; - let first_record_relative_offset = 0usize; - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); - bytes[cursor + 4] = 8; - bytes[cursor + 5..cursor + 13].copy_from_slice(b"Marker11"); - cursor += 0x10; - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); - bytes[cursor + 4..cursor + 8].copy_from_slice(&100.0f32.to_bits().to_le_bytes()); - bytes[cursor + 8..cursor + 12].copy_from_slice(&2.0f32.to_bits().to_le_bytes()); - bytes[cursor + 12..cursor + 16].copy_from_slice(&50.0f32.to_bits().to_le_bytes()); - bytes[cursor + 28..cursor + 30].copy_from_slice(&1u16.to_le_bytes()); - cursor += 0x1e; - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); - cursor += 0x20; - let second_record_relative_offset = cursor - (records_tag_offset + 4); - bytes[cursor..cursor + 4].copy_from_slice(&0x11223344u32.to_le_bytes()); - bytes[cursor + 4..cursor + 8].copy_from_slice(&0x55667788u32.to_le_bytes()); - cursor += 8; - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); - bytes[cursor + 4] = 8; - bytes[cursor + 5..cursor + 13].copy_from_slice(b"Marker12"); - cursor += 0x10; - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); - bytes[cursor + 4..cursor + 8].copy_from_slice(&120.0f32.to_bits().to_le_bytes()); - bytes[cursor + 8..cursor + 12].copy_from_slice(&3.0f32.to_bits().to_le_bytes()); - bytes[cursor + 12..cursor + 16].copy_from_slice(&60.0f32.to_bits().to_le_bytes()); - bytes[cursor + 16..cursor + 20].copy_from_slice(&0x01020304u32.to_le_bytes()); - bytes[cursor + 20..cursor + 24].copy_from_slice(&0u32.to_le_bytes()); - bytes[cursor + 24..cursor + 28].copy_from_slice(&0x05060708u32.to_le_bytes()); - bytes[cursor + 28..cursor + 30].copy_from_slice(&1u16.to_le_bytes()); - cursor += 0x1e; - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); - let directory_root_byte_offset = SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX * 4; - let first_payload_relative_offset = records_tag_offset - metadata_tag_offset; - let second_payload_relative_offset = - first_payload_relative_offset + second_record_relative_offset; - bytes[metadata_tag_offset + 4 + directory_root_byte_offset - ..metadata_tag_offset + 8 + directory_root_byte_offset] - .copy_from_slice(&(first_payload_relative_offset as u32).to_le_bytes()); - bytes[metadata_tag_offset + 16 + directory_root_byte_offset - ..metadata_tag_offset + 20 + directory_root_byte_offset] - .copy_from_slice(&(second_payload_relative_offset as u32).to_le_bytes()); - - let header_probe = SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-region-tagged-header-counts".to_string(), - semantic_family: "scenario-save-region-header-counts".to_string(), - metadata_tag_offset, - records_tag_offset, - close_tag_offset, - direct_collection_flag: 0, - direct_collection_flag_hex: "0x00000000".to_string(), - direct_record_stride: 0x06, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 0x96, - live_id_bound_hex: "0x00000096".to_string(), - live_record_count: 2, - live_record_count_hex: "0x00000002".to_string(), - header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x02], - header_hex_words: vec![], - evidence: vec![], - }; - let triplet_probe = parse_save_region_record_triplet_probe(&bytes, Some(&header_probe)) - .expect("region triplet probe should parse"); - - assert_eq!(triplet_probe.entries.len(), 2); - assert_eq!( - triplet_probe.entries[0].record_payload_relative_offset, - first_record_relative_offset - ); - assert_eq!(triplet_probe.entries[0].pre_name_prefix_len, 0); - assert_eq!( - triplet_probe.entries[1].record_payload_relative_offset, - second_record_relative_offset - ); - assert_eq!(triplet_probe.entries[1].pre_name_prefix_len, 8); - assert_eq!( - triplet_probe.entries[1] - .pre_name_prefix_dword_candidates - .len(), - 2 - ); - assert_eq!( - triplet_probe.entries[1].pre_name_prefix_dword_candidates[0].raw_u32_hex, - "0x11223344" - ); - assert_eq!( - triplet_probe.entries[1].pre_name_prefix_dword_candidates[1].relative_offset_hex, - "0x52" - ); - assert_eq!( - triplet_probe.entries[1].policy_reserved_dword_candidates[2].relative_offset_hex, - "0xcc" - ); - assert_eq!( - triplet_probe.entries[1].policy_reserved_dword_candidates[0].raw_u32_hex, - "0x01020304" - ); - assert_eq!( - triplet_probe.entries[1].policy_reserved_dword_candidates[2].raw_u32_hex, - "0x05060708" - ); - assert!(triplet_probe.evidence.iter().any(|line| line.contains( - "fixed 0x55f2 policy reserved dwords are nonzero on 1 of 2 decoded region records" - ))); - } - - #[test] - fn parses_region_profile_collection_probe_from_fixed_name_rows() { - let mut payload = vec![0u8; 0x80]; - let header_words = [1u32, 0x22, 2, 2, 3, 2, 0, 1]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = index * 4; - payload[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - let first_row_offset = 0x20usize; - let first_name = b"House"; - payload[first_row_offset..first_row_offset + first_name.len()].copy_from_slice(first_name); - payload[first_row_offset + 0x1e..first_row_offset + 0x22] - .copy_from_slice(&0.2f32.to_bits().to_le_bytes()); - let second_row_offset = first_row_offset + 0x22; - let second_name = b"Farm Corn"; - payload[second_row_offset..second_row_offset + second_name.len()] - .copy_from_slice(second_name); - payload[second_row_offset + 0x1e..second_row_offset + 0x22] - .copy_from_slice(&0.45f32.to_bits().to_le_bytes()); - - let profile_probe = parse_save_region_profile_collection_probe(&payload) - .expect("profile collection probe should parse"); - - assert_eq!(profile_probe.direct_collection_flag, 1); - assert_eq!(profile_probe.entry_stride, 0x22); - assert_eq!(profile_probe.live_id_bound, 3); - assert_eq!(profile_probe.live_record_count, 2); - assert_eq!(profile_probe.entry_start_relative_offset, 0x20); - assert_eq!(profile_probe.entries.len(), 2); - assert_eq!(profile_probe.entries[0].name, "House"); - assert_eq!(profile_probe.entries[0].trailing_weight_f32, 0.2); - assert_eq!(profile_probe.entries[1].name, "Farm Corn"); - assert_eq!(profile_probe.entries[1].trailing_weight_f32, 0.45); - } - - #[test] - fn parses_region_queued_notice_record_probe_from_seeded_node() { - let mut bytes = vec![0u8; 0x200]; - let node_base_offset = 0x80usize; - bytes[node_base_offset + 4..node_base_offset + 8] - .copy_from_slice(&SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED.to_le_bytes()); - bytes[node_base_offset + 8..node_base_offset + 12] - .copy_from_slice(&SAVE_REGION_QUEUED_NOTICE_NODE_KIND.to_le_bytes()); - bytes[node_base_offset + 12..node_base_offset + 16].copy_from_slice(&0u32.to_le_bytes()); - bytes[node_base_offset + 16..node_base_offset + 20].copy_from_slice(&5u32.to_le_bytes()); - bytes[node_base_offset + 20..node_base_offset + 24].copy_from_slice(&1200u32.to_le_bytes()); - bytes[node_base_offset + 24..node_base_offset + 28].copy_from_slice(&(-1i32).to_le_bytes()); - bytes[node_base_offset + 28..node_base_offset + 32].copy_from_slice(&(-1i32).to_le_bytes()); - - let probe = parse_save_region_queued_notice_record_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-region-tagged-header-counts".to_string(), - semantic_family: "scenario-save-region-header-counts".to_string(), - metadata_tag_offset: 0, - records_tag_offset: 0, - close_tag_offset: 0, - direct_collection_flag: 0, - direct_collection_flag_hex: "0x00000000".to_string(), - direct_record_stride: 0x06, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 0x96, - live_id_bound_hex: "0x00000096".to_string(), - live_record_count: 0x91, - live_record_count_hex: "0x00000091".to_string(), - header_words: vec![], - header_hex_words: vec![], - evidence: vec![], - }), - ) - .expect("region queued notice record probe should parse"); - - assert_eq!(probe.entries.len(), 1); - assert_eq!(probe.entries[0].node_base_offset, node_base_offset); - assert_eq!(probe.entries[0].payload_seed_dword_hex, "0x005c87a8"); - assert_eq!(probe.entries[0].kind, SAVE_REGION_QUEUED_NOTICE_NODE_KIND); - assert_eq!(probe.entries[0].region_id, 5); - assert_eq!(probe.entries[0].amount, 1200); - assert_eq!(probe.entries[0].trailing_sentinel_i32_0, -1); - assert_eq!(probe.entries[0].trailing_sentinel_i32_1, -1); - } - - #[test] - fn parses_region_fixed_row_run_candidate_probe_from_seeded_rows() { - let mut bytes = vec![0u8; 0x200]; - let count_offset = 0x20usize; - let rows_offset = count_offset + 4; - let metadata_tag_offset = 0x120usize; - bytes[count_offset..count_offset + 4].copy_from_slice(&2u32.to_le_bytes()); - - let first_row = rows_offset; - bytes[first_row..first_row + 4].copy_from_slice(&11u32.to_le_bytes()); - bytes[first_row + 4..first_row + 8].copy_from_slice(&0.25f32.to_bits().to_le_bytes()); - bytes[first_row + 8..first_row + 12].copy_from_slice(&0x11223344u32.to_le_bytes()); - bytes[first_row + 0x28] = 0x07; - - let second_row = rows_offset + SAVE_REGION_FIXED_ROW_STRIDE; - bytes[second_row..second_row + 4].copy_from_slice(&12u32.to_le_bytes()); - bytes[second_row + 4..second_row + 8].copy_from_slice(&0.5f32.to_bits().to_le_bytes()); - bytes[second_row + 8..second_row + 12].copy_from_slice(&0x55667788u32.to_le_bytes()); - bytes[second_row + 0x28] = 0x08; - - let probe = parse_save_region_fixed_row_run_candidate_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-region-tagged-header-counts".to_string(), - semantic_family: "scenario-save-region-header-counts".to_string(), - metadata_tag_offset, - records_tag_offset: 0, - close_tag_offset: 0, - direct_collection_flag: 0, - direct_collection_flag_hex: "0x00000000".to_string(), - direct_record_stride: 0x06, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 0x96, - live_id_bound_hex: "0x00000096".to_string(), - live_record_count: 2, - live_record_count_hex: "0x00000002".to_string(), - header_words: vec![], - header_hex_words: vec![], - evidence: vec![], - }), - ) - .expect("region fixed-row run candidate probe should parse"); - - assert_eq!(probe.target_row_count, 2); - assert_eq!(probe.target_row_stride, SAVE_REGION_FIXED_ROW_STRIDE); - assert_eq!(probe.candidates.len(), 1); - assert_eq!(probe.candidates[0].count_offset, count_offset); - assert_eq!(probe.candidates[0].rows_offset, rows_offset); - assert_eq!( - probe.candidates[0].best_probable_density_lane_relative_offset_hex, - Some("0x4".to_string()) - ); - assert_eq!( - probe.candidates[0].dword_lane_summaries[0].small_unsigned_count, - 2 - ); - assert_eq!( - probe.candidates[0].dword_lane_summaries[1].probable_normal_f32_count, - 2 - ); - assert_eq!(probe.candidates[0].trailing_byte_nonzero_count, 2); - assert_eq!( - probe.candidates[0].trailing_byte_sample_values_hex, - vec!["0x07".to_string(), "0x08".to_string()] - ); - assert_eq!( - probe.candidates[0].shape_signature, - "pf32=[0x4:2,0x8:2]|small=[0x0:2]|zero=[]|trail=0/2" - ); - assert_eq!( - probe.candidates[0].shape_family_signature, - "dense_pf32=[0x4,0x8]|small_nonzero=[0x0,0x4,0x8]|partial_zero=[]|trail_bucket=0/0" - ); - } - - #[test] - fn parses_placed_structure_record_triplet_probe_from_dual_name_rows() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x260usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x000036b1u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x000036b2u32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000036b3u32.to_le_bytes()); - let mut cursor = records_tag_offset + 4; - for (primary, secondary, lane0, lane1, lane2, lane3, lane4) in [ - ( - "StationA", - "StationSetA", - 43111.92f32, - 1385.5f32, - 34581.95f32, - 0.0f32, - 5.9760494f32, - ), - ( - "StationB", - "StationSetB", - 44000.0f32, - 1200.0f32, - 33000.0f32, - 0.0f32, - 4.5f32, - ), - ] { - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); - bytes[cursor + 4] = primary.len() as u8; - bytes[cursor + 5..cursor + 5 + primary.len()].copy_from_slice(primary.as_bytes()); - let second_len_offset = cursor + 5 + primary.len(); - bytes[second_len_offset] = secondary.len() as u8; - bytes[second_len_offset + 1..second_len_offset + 1 + secondary.len()] - .copy_from_slice(secondary.as_bytes()); - cursor += 0x19; - bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); - bytes[cursor + 4..cursor + 8].copy_from_slice(&lane0.to_bits().to_le_bytes()); - bytes[cursor + 8..cursor + 12].copy_from_slice(&lane1.to_bits().to_le_bytes()); - bytes[cursor + 12..cursor + 16].copy_from_slice(&lane2.to_bits().to_le_bytes()); - bytes[cursor + 16..cursor + 20].copy_from_slice(&lane3.to_bits().to_le_bytes()); - bytes[cursor + 20..cursor + 24].copy_from_slice(&lane4.to_bits().to_le_bytes()); - bytes[cursor + 28..cursor + 30].copy_from_slice(&0x0101u16.to_le_bytes()); - cursor += 0x1e; - bytes[cursor..cursor + 2] - .copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); - bytes[cursor + 4..cursor + 8].copy_from_slice(&0x5dc1u32.to_le_bytes()); - let mut payload_cursor = cursor + 8; - bytes[payload_cursor] = primary.len() as u8; - bytes[payload_cursor + 1..payload_cursor + 1 + primary.len()] - .copy_from_slice(primary.as_bytes()); - payload_cursor += 1 + primary.len(); - bytes[payload_cursor] = 0; - payload_cursor += 1; - bytes[payload_cursor] = secondary.len() as u8; - bytes[payload_cursor + 1..payload_cursor + 1 + secondary.len()] - .copy_from_slice(secondary.as_bytes()); - payload_cursor += 1 + secondary.len(); - bytes[payload_cursor] = 0; - payload_cursor += 1; - bytes[payload_cursor..payload_cursor + 4].copy_from_slice(&0x0e373500u32.to_le_bytes()); - bytes[payload_cursor + 4..payload_cursor + 8].copy_from_slice(&(-1i32).to_le_bytes()); - bytes[payload_cursor + 8..payload_cursor + 12] - .copy_from_slice(&0x5dc2u32.to_le_bytes()); - cursor += 0x18 + primary.len() + secondary.len(); - } - - let header_probe = SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-placed-structure-tagged-header-counts".to_string(), - semantic_family: "scenario-save-placed-structure-header-counts".to_string(), - metadata_tag_offset, - records_tag_offset, - close_tag_offset, - direct_collection_flag: 0, - direct_collection_flag_hex: "0x00000000".to_string(), - direct_record_stride: 0x06, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 3, - live_id_bound_hex: "0x00000003".to_string(), - live_record_count: 2, - live_record_count_hex: "0x00000002".to_string(), - header_words: vec![0, 6, 0x0a, 0x14, 3, 2], - header_hex_words: vec![], - evidence: vec![], - }; - let triplet_probe = - parse_save_placed_structure_record_triplet_probe(&bytes, Some(&header_probe)) - .expect("placed-structure triplet probe should parse"); - - assert_eq!(triplet_probe.record_count, 2); - assert_eq!(triplet_probe.entries[0].primary_name, "StationA"); - assert_eq!(triplet_probe.entries[0].secondary_name, "StationSetA"); - assert_eq!(triplet_probe.entries[0].policy_chunk_len, 0x1a); - assert_eq!(triplet_probe.entries[0].policy_f32_lane_4, 5.9760494); - assert_eq!(triplet_probe.entries[0].policy_trailing_word, 0x0101); - assert_eq!( - triplet_probe.entries[0].profile_open_marker_hex, - "0x00005dc1" - ); - assert_eq!( - triplet_probe.entries[0].profile_repeated_primary_name, - "StationA" - ); - assert_eq!( - triplet_probe.entries[0].profile_repeated_secondary_name, - "StationSetA" - ); - assert_eq!( - triplet_probe.entries[0].profile_footer_relative_offset_hex, - "0x1b" - ); - assert_eq!(triplet_probe.entries[0].profile_pre_footer_padding_len, 1); - assert_eq!( - triplet_probe.entries[0].profile_pre_footer_padding_hex_bytes, - vec!["0x00".to_string()] - ); - assert_eq!(triplet_probe.entries[0].profile_companion_byte_u8, Some(0)); - assert_eq!( - triplet_probe.entries[0] - .profile_companion_byte_hex - .as_deref(), - Some("0x00") - ); - assert_eq!( - triplet_probe.entries[0].profile_payload_dword_hex, - "0x0e373500" - ); - assert_eq!(triplet_probe.entries[0].profile_sentinel_i32, -1); - assert_eq!(triplet_probe.entries[0].profile_status_kind, "unset"); - assert_eq!(triplet_probe.entries[0].farm_growth_stage_index, None); - assert_eq!( - triplet_probe.entries[0].profile_close_marker_hex, - "0x00005dc2" - ); - assert_eq!(triplet_probe.entries[1].primary_name, "StationB"); - assert_eq!(triplet_probe.entries[1].secondary_name, "StationSetB"); - assert_eq!(triplet_probe.entries[1].policy_f32_lane_1, 1200.0); - } - - #[test] - fn derives_placed_structure_farm_growth_stage_from_nonnegative_status() { - assert_eq!( - derive_save_placed_structure_profile_status("FarmCorn", "FarmSet", 4), - ("farm_growth_stage_bucket", Some(4)) - ); - assert_eq!( - derive_save_placed_structure_profile_status("StationA", "StationSetA", -1), - ("unset", None) - ); - assert_eq!( - derive_save_placed_structure_profile_status("StationA", "StationSetA", 4), - ("opaque_nondefault", None) - ); - } - - #[test] - fn parses_placed_structure_dynamic_side_buffer_probe_from_embedded_name_row() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x220usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x000038a5u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x000038a6u32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000038a7u32.to_le_bytes()); - let header_words = [ - 0u32, 0x06, 1000, 500, 1000, 388, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - let payload_offset = records_tag_offset + 4; - bytes[payload_offset..payload_offset + 4].copy_from_slice(&0x0005d368u32.to_le_bytes()); - bytes[payload_offset + 4..payload_offset + 6].copy_from_slice(&0x0001u16.to_le_bytes()); - bytes[payload_offset + 6] = 0xff; - let name_tag_offset = payload_offset + 7; - bytes[name_tag_offset..name_tag_offset + 2] - .copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); - let first_name = "TrackCapST_Cap.3dp"; - let second_name = "Infrastructure"; - bytes[name_tag_offset + 4] = first_name.len() as u8; - bytes[name_tag_offset + 5..name_tag_offset + 5 + first_name.len()] - .copy_from_slice(first_name.as_bytes()); - let second_len_offset = name_tag_offset + 5 + first_name.len(); - bytes[second_len_offset] = second_name.len() as u8; - bytes[second_len_offset + 1..second_len_offset + 1 + second_name.len()] - .copy_from_slice(second_name.as_bytes()); - - let probe = parse_save_placed_structure_dynamic_side_buffer_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("placed-structure dynamic side-buffer probe should parse"); - - assert_eq!(probe.direct_record_stride, 0x06); - assert_eq!(probe.live_id_bound, 1000); - assert_eq!(probe.live_record_count, 388); - assert_eq!(probe.owner_shared_dword_hex, "0x0005d368"); - assert_eq!(probe.owner_shared_dword_relative_offset, 0); - assert!(probe.owner_shared_dword_matches_first_compact_prefix_leading_dword); - assert_eq!(probe.first_record_child_count_after_owner_shared, Some(1)); - assert_eq!( - probe - .first_record_saved_primary_child_byte_after_owner_shared_hex - .as_deref(), - Some("0xff") - ); - assert_eq!( - probe.first_record_first_name_tag_relative_offset_after_owner_shared, - Some(3) - ); - assert_eq!(probe.prefix_leading_dword_hex, "0x0005d368"); - assert_eq!(probe.prefix_trailing_word_hex, "0x0001"); - assert_eq!(probe.prefix_separator_byte_hex, "0xff"); - assert_eq!(probe.first_embedded_name_tag_relative_offset, 7); - assert_eq!(probe.embedded_name_tag_count, 1); - assert_eq!(probe.decoded_embedded_name_row_count, 1); - assert_eq!(probe.decoded_embedded_name_row_with_tertiary_name_count, 0); - assert_eq!(probe.unique_compact_prefix_pattern_count, 1); - assert_eq!( - probe.prefix_leading_dword_matching_embedded_profile_tag_count, - 0 - ); - assert_eq!(probe.unique_embedded_name_pair_count, 1); - assert_eq!( - probe.first_embedded_primary_name.as_deref(), - Some("TrackCapST_Cap.3dp") - ); - assert_eq!( - probe.first_embedded_secondary_name.as_deref(), - Some("Infrastructure") - ); - assert_eq!(probe.first_embedded_tertiary_name.as_deref(), None); - assert_eq!(probe.compact_prefix_pattern_summaries.len(), 1); - assert_eq!( - probe.compact_prefix_pattern_summaries[0].prefix_leading_dword_hex, - "0x0005d368" - ); - assert_eq!(probe.compact_prefix_pattern_summaries[0].count, 1); - assert_eq!( - probe.compact_prefix_pattern_summaries[0].cap_like_primary_name_count, - 1 - ); - assert_eq!( - probe.compact_prefix_pattern_summaries[0].section_like_primary_name_count, - 0 - ); - assert_eq!(probe.name_pair_summaries.len(), 1); - assert_eq!(probe.name_pair_summaries[0].count, 1); - assert_eq!( - probe.name_pair_summaries[0].dominant_prefix_leading_dword_hex, - "0x0005d368" - ); - let payload_envelope_summary = probe - .payload_envelope_summary - .as_ref() - .expect("payload envelope summary should be present"); - assert_eq!( - payload_envelope_summary.row_count_with_policy_tag_before_next_name, - 0 - ); - assert_eq!( - payload_envelope_summary.row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope, - 0 - ); - assert_eq!( - payload_envelope_summary.row_count_missing_policy_tag_before_next_name, - 1 - ); - } - - #[test] - fn summarizes_placed_structure_dynamic_side_buffer_compact_prefix_patterns() { - let mut bytes = vec![0u8; 0x600]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x320usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x000038a5u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x000038a6u32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000038a7u32.to_le_bytes()); - let header_words = [ - 0u32, 0x06, 1000, 500, 1000, 388, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - let mut cursor = records_tag_offset + 4; - for (leading_dword, primary_name) in [ - (0x000055f3u32, "TunnelSTBrick_Section.3dp"), - (0x000055f3u32, "TunnelSTBrick_Cap.3dp"), - (0xff0000ffu32, "TunnelSTBrick_Cap.3dp"), - ] { - bytes[cursor..cursor + 4].copy_from_slice(&leading_dword.to_le_bytes()); - bytes[cursor + 4..cursor + 6].copy_from_slice(&0x0001u16.to_le_bytes()); - bytes[cursor + 6] = 0xff; - let name_tag_offset = cursor + 7; - bytes[name_tag_offset..name_tag_offset + 2] - .copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); - let secondary_name = "Infrastructure"; - bytes[name_tag_offset + 4] = primary_name.len() as u8; - bytes[name_tag_offset + 5..name_tag_offset + 5 + primary_name.len()] - .copy_from_slice(primary_name.as_bytes()); - let second_len_offset = name_tag_offset + 5 + primary_name.len(); - bytes[second_len_offset] = secondary_name.len() as u8; - bytes[second_len_offset + 1..second_len_offset + 1 + secondary_name.len()] - .copy_from_slice(secondary_name.as_bytes()); - cursor = second_len_offset + 1 + secondary_name.len(); - } - - let probe = parse_save_placed_structure_dynamic_side_buffer_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("placed-structure dynamic side-buffer probe should parse"); - - assert_eq!(probe.embedded_name_tag_count, 3); - assert_eq!(probe.decoded_embedded_name_row_count, 3); - assert_eq!(probe.decoded_embedded_name_row_with_tertiary_name_count, 0); - assert_eq!(probe.unique_compact_prefix_pattern_count, 2); - assert_eq!( - probe.prefix_leading_dword_matching_embedded_profile_tag_count, - 2 - ); - assert_eq!(probe.unique_embedded_name_pair_count, 2); - assert_eq!(probe.compact_prefix_pattern_summaries.len(), 2); - assert_eq!( - probe.compact_prefix_pattern_summaries[0].prefix_leading_dword_hex, - "0x000055f3" - ); - assert_eq!(probe.compact_prefix_pattern_summaries[0].count, 2); - assert_eq!( - probe.compact_prefix_pattern_summaries[0].section_like_primary_name_count, - 1 - ); - assert_eq!( - probe.compact_prefix_pattern_summaries[0].cap_like_primary_name_count, - 1 - ); - assert!( - probe.compact_prefix_pattern_summaries[0] - .prefix_leading_dword_matches_embedded_profile_tag - ); - assert_eq!( - probe.compact_prefix_pattern_summaries[1].prefix_leading_dword_hex, - "0xff0000ff" - ); - assert_eq!(probe.compact_prefix_pattern_summaries[1].count, 1); - assert_eq!(probe.name_pair_summaries.len(), 2); - assert_eq!(probe.name_pair_summaries[0].count, 2); - assert_eq!( - probe.name_pair_summaries[0].dominant_prefix_leading_dword_hex, - "0x000055f3" - ); - assert_eq!(probe.name_pair_summaries[1].count, 1); - let payload_envelope_summary = probe - .payload_envelope_summary - .as_ref() - .expect("payload envelope summary should be present"); - assert_eq!( - payload_envelope_summary.row_count_with_policy_tag_before_next_name, - 0 - ); - assert_eq!( - payload_envelope_summary.row_count_missing_policy_tag_before_next_name, - 3 - ); - } - - #[test] - fn parses_save_len_prefixed_ascii_name_triplet_with_optional_third_name() { - let bytes = [ - 5u8, b'F', b'i', b'r', b's', b't', 0, 6, b'S', b'e', b'c', b'o', b'n', b'd', 0, 5, - b'T', b'h', b'i', b'r', b'd', - ]; - let parsed = parse_save_len_prefixed_ascii_name_triplet(&bytes) - .expect("triplet parser should decode three len-prefixed ascii names"); - assert_eq!(parsed.0, "First"); - assert_eq!(parsed.1, "Second"); - assert_eq!(parsed.2.as_deref(), Some("Third")); - } - - #[test] - fn parses_save_len_prefixed_ascii_name_triplet_with_extended_length_prefix() { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&[0x80, 0x03, b'A', b'B', b'C']); - bytes.extend_from_slice(&[0, 1, b'X']); - let parsed = parse_save_len_prefixed_ascii_name_triplet(&bytes) - .expect("triplet parser should decode extended-length prefix"); - assert_eq!(parsed.0, "ABC"); - assert_eq!(parsed.1, "X"); - assert_eq!(parsed.2, None); - } - - #[test] - fn parses_save_len_prefixed_ascii_name_triplet_with_consumed_len() { - let bytes = [ - 5u8, b'F', b'i', b'r', b's', b't', 0, 6, b'S', b'e', b'c', b'o', b'n', b'd', 0, 5, - b'T', b'h', b'i', b'r', b'd', 0xff, - ]; - let (parsed, consumed_len) = - parse_save_len_prefixed_ascii_name_triplet_and_consumed_len(&bytes) - .expect("triplet parser should decode consumed len"); - assert_eq!(parsed.0, "First"); - assert_eq!(parsed.1, "Second"); - assert_eq!(parsed.2.as_deref(), Some("Third")); - assert_eq!(consumed_len, 21); - } - - #[test] - fn aligns_placed_structure_dynamic_side_buffer_name_pairs_with_triplets() { - let side_buffer = SmpSavePlacedStructureDynamicSideBufferProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(), - semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records" - .to_string(), - metadata_tag_offset: 0, - records_tag_offset: 0, - close_tag_offset: 0, - records_span_len: 0, - direct_record_stride: 6, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 1000, - live_id_bound_hex: "0x000003e8".to_string(), - live_record_count: 10, - live_record_count_hex: "0x0000000a".to_string(), - owner_shared_dword: 0, - owner_shared_dword_hex: "0x00000000".to_string(), - owner_shared_dword_relative_offset: 0, - owner_shared_dword_matches_first_compact_prefix_leading_dword: true, - first_record_child_count_after_owner_shared: None, - first_record_child_count_after_owner_shared_hex: None, - first_record_saved_primary_child_byte_after_owner_shared: None, - first_record_saved_primary_child_byte_after_owner_shared_hex: None, - first_record_first_name_tag_relative_offset_after_owner_shared: None, - prefix_leading_dword: 0, - prefix_leading_dword_hex: "0x00000000".to_string(), - prefix_trailing_word: 1, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - first_embedded_name_tag_relative_offset: 7, - embedded_name_tag_count: 3, - decoded_embedded_name_row_count: 3, - decoded_embedded_name_row_with_tertiary_name_count: 0, - unique_compact_prefix_pattern_count: 2, - prefix_leading_dword_matching_embedded_profile_tag_count: 2, - unique_embedded_name_pair_count: 2, - first_embedded_primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - first_embedded_secondary_name: Some("Infrastructure".to_string()), - first_embedded_tertiary_name: None, - embedded_name_row_samples: vec![], - compact_prefix_pattern_summaries: vec![], - name_pair_summaries: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePairSummary { - primary_name: "TunnelSTBrick_Section.3dp".to_string(), - secondary_name: "Infrastructure".to_string(), - count: 2, - first_name_tag_relative_offset: 7, - unique_compact_prefix_pattern_count: 1, - dominant_prefix_leading_dword: 0x55f3, - dominant_prefix_leading_dword_hex: "0x000055f3".to_string(), - dominant_prefix_trailing_word: 1, - dominant_prefix_trailing_word_hex: "0x0001".to_string(), - dominant_prefix_separator_byte: 0xff, - dominant_prefix_separator_byte_hex: "0xff".to_string(), - dominant_prefix_count: 2, - }, - SmpSavePlacedStructureDynamicSideBufferNamePairSummary { - primary_name: "BridgeSTWood_Section.3dp".to_string(), - secondary_name: "Infrastructure".to_string(), - count: 1, - first_name_tag_relative_offset: 27, - unique_compact_prefix_pattern_count: 1, - dominant_prefix_leading_dword: 0xff000000, - dominant_prefix_leading_dword_hex: "0xff000000".to_string(), - dominant_prefix_trailing_word: 1, - dominant_prefix_trailing_word_hex: "0x0001".to_string(), - dominant_prefix_separator_byte: 0xff, - dominant_prefix_separator_byte_hex: "0xff".to_string(), - dominant_prefix_count: 1, - }, - ], - payload_envelope_summary: None, - live_entry_prelude_summary: None, - evidence: vec![], - }; - let triplets = SmpSavePlacedStructureRecordTripletProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-placed-structure-record-triplets".to_string(), - semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), - records_tag_offset: 0, - close_tag_offset: 0, - record_count: 2, - entries: vec![ - SmpSavePlacedStructureRecordTripletEntryProbe { - record_index: 0, - primary_name: "TunnelSTBrick_Section.3dp".to_string(), - secondary_name: "Infrastructure".to_string(), - name_tag_relative_offset: 0, - policy_tag_relative_offset: 0, - profile_tag_relative_offset: 0, - policy_chunk_len: 0, - profile_chunk_len: 0, - policy_f32_lane_0: 0.0, - policy_f32_lane_1: 0.0, - policy_f32_lane_2: 0.0, - policy_f32_lane_3: 0.0, - policy_f32_lane_4: 0.0, - policy_reserved_dword: 0, - policy_trailing_word: 0, - policy_trailing_word_hex: "0x0000".to_string(), - profile_open_marker: 0, - profile_open_marker_hex: "0x00000000".to_string(), - profile_repeated_primary_name: "TunnelSTBrick_Section.3dp".to_string(), - profile_repeated_secondary_name: "Infrastructure".to_string(), - profile_footer_relative_offset: 0, - profile_footer_relative_offset_hex: "0x0".to_string(), - profile_pre_footer_padding_len: 0, - profile_pre_footer_padding_hex_bytes: Vec::new(), - profile_companion_byte_u8: None, - profile_companion_byte_hex: None, - profile_payload_dword: 0, - profile_payload_dword_hex: "0x00000000".to_string(), - profile_sentinel_i32: -1, - profile_status_kind: "unset".to_string(), - farm_growth_stage_index: None, - profile_close_marker: 0, - profile_close_marker_hex: "0x00000000".to_string(), - }, - SmpSavePlacedStructureRecordTripletEntryProbe { - record_index: 1, - primary_name: "TrackCapST_Cap.3dp".to_string(), - secondary_name: "Infrastructure".to_string(), - name_tag_relative_offset: 0, - policy_tag_relative_offset: 0, - profile_tag_relative_offset: 0, - policy_chunk_len: 0, - profile_chunk_len: 0, - policy_f32_lane_0: 0.0, - policy_f32_lane_1: 0.0, - policy_f32_lane_2: 0.0, - policy_f32_lane_3: 0.0, - policy_f32_lane_4: 0.0, - policy_reserved_dword: 0, - policy_trailing_word: 0, - policy_trailing_word_hex: "0x0000".to_string(), - profile_open_marker: 0, - profile_open_marker_hex: "0x00000000".to_string(), - profile_repeated_primary_name: "TrackCapST_Cap.3dp".to_string(), - profile_repeated_secondary_name: "Infrastructure".to_string(), - profile_footer_relative_offset: 0, - profile_footer_relative_offset_hex: "0x0".to_string(), - profile_pre_footer_padding_len: 0, - profile_pre_footer_padding_hex_bytes: Vec::new(), - profile_companion_byte_u8: None, - profile_companion_byte_hex: None, - profile_payload_dword: 0, - profile_payload_dword_hex: "0x00000000".to_string(), - profile_sentinel_i32: -1, - profile_status_kind: "unset".to_string(), - farm_growth_stage_index: None, - profile_close_marker: 0, - profile_close_marker_hex: "0x00000000".to_string(), - }, - ], - evidence: vec![], - }; - - let alignment = - summarize_placed_structure_dynamic_side_buffer_alignment(&side_buffer, &triplets); - - assert_eq!(alignment.unique_side_buffer_name_pair_count, 2); - assert_eq!(alignment.unique_triplet_name_pair_count, 2); - assert_eq!(alignment.overlapping_name_pair_count, 1); - assert_eq!( - alignment.side_buffer_rows_with_matching_triplet_name_pair_count, - 2 - ); - assert_eq!( - alignment.side_buffer_rows_without_matching_triplet_name_pair_count, - 1 - ); - assert_eq!( - alignment.triplet_name_pairs_without_side_buffer_match_count, - 1 - ); - assert_eq!(alignment.matched_name_pair_samples.len(), 1); - assert_eq!(alignment.unmatched_side_buffer_name_pair_samples.len(), 1); - } - - #[test] - fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x180usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x000036b1u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x000036b2u32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000036b3u32.to_le_bytes()); - let header_words = [ - 0u32, 0x06, 0x0a, 0x14, 0x7ee, 0x7ea, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - - let probe = parse_save_placed_structure_collection_header_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("placed structure header probe should parse"); - - assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); - assert_eq!(probe.records_tag_offset, records_tag_offset); - assert_eq!(probe.close_tag_offset, close_tag_offset); - assert_eq!(probe.direct_collection_flag, 0); - assert_eq!(probe.direct_record_stride, 0x06); - assert_eq!(probe.live_id_bound, 0x7ee); - assert_eq!(probe.live_record_count, 0x7ea); - } - - #[test] - fn scans_unclassified_tagged_collection_header_probe_from_adjacent_low_tags() { - let mut bytes = vec![0u8; 0x400]; - let metadata_tag_offset = 0x40usize; - let records_tag_offset = 0x140usize; - let close_tag_offset = 0x1c0usize; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x00007001u32.to_le_bytes()); - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x00007002u32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x00007003u32.to_le_bytes()); - let header_words = [ - 0u32, 0x12, 0x0a, 0x14, 0x90, 0x78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - - let probes = scan_save_unclassified_tagged_collection_header_probes( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ); - - let probe = probes - .iter() - .find(|probe| probe.metadata_tag == 0x7001) - .expect("should include synthetic unclassified tag family"); - assert_eq!(probe.records_tag, 0x7002); - assert_eq!(probe.close_tag, 0x7003); - assert_eq!(probe.direct_record_stride, 0x12); - assert_eq!(probe.live_id_bound, 0x90); - assert_eq!(probe.live_record_count, 0x78); - assert_eq!( - probe.records_span_len, - close_tag_offset - (records_tag_offset + 4) - ); - } - - #[test] - fn parses_save_company_roster_probe_from_direct_records() { - let metadata_tag_offset = 0x40usize; - let stride = 0x7684usize; - let count = 2usize; - let start_offset = 0xc6usize; - let total_len = metadata_tag_offset + 4 + start_offset + stride * count + 0x400; - let mut bytes = vec![0u8; total_len]; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x000061a9u32.to_le_bytes()); - let header_words = [ - 1u32, - 0x7684, - 5, - 5, - 5, - count as u32, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - let records_tag_offset = metadata_tag_offset + 4 + 0x200; - let close_tag_offset = records_tag_offset + 4; - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x000061aau32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000061abu32.to_le_bytes()); - - for ( - index, - ( - name, - linked, - merger, - takeover, - bond_count, - _debt, - track_capacity, - mutable_support_scalar_raw_u32, - young_company_support_scalar_raw_u32, - support_progress_word, - recent_per_share_subscore_raw_u32, - cached_share_price_raw_u32, - chairman_salary_baseline, - chairman_salary_current, - chairman_bonus_year, - chairman_bonus_amount, - founding_year, - last_bankruptcy_year, - last_dividend_year, - current_issue_calendar_word, - current_issue_calendar_word_2, - prior_issue_calendar_word, - prior_issue_calendar_word_2, - preferred_locomotive_engine_type_raw_u8, - city_connection_latch, - linked_transit_latch, - linked_transit_route_anchor_entry_id, - linked_transit_route_anchor_fallback_counts, - ), - ) in [ - ( - "Company One", - 1u32, - 1862u32, - 1865u32, - 2u8, - 1_000_000u32, - Some(603i32), - 0x3f800000u32, - 0x42340000u32, - 17u32, - 0x41f00000u32, - 0x426c0000u32, - 24u32, - 31u32, - 1849u32, - 1250i32, - 1842u32, - 1851u32, - 1848u32, - 7u32, - 8u32, - 6u32, - 7u32, - 2u8, - true, - false, - Some(77u32), - [3u32, 5u32, 8u32], - ), - ( - "Company Two", - 2u32, - 0u32, - 1871u32, - 1u8, - 500_000u32, - None, - 0x40000000u32, - 0x42700000u32, - 33u32, - 0x42000000u32, - 0x42780000u32, - 28u32, - 36u32, - 0u32, - 0i32, - 1845u32, - 0u32, - 1850u32, - 3u32, - 4u32, - 2u32, - 3u32, - 1u8, - false, - true, - Some(41u32), - [13u32, 21u32, 34u32], - ), - ] - .into_iter() - .enumerate() - { - let record_offset = metadata_tag_offset + 4 + start_offset + index * stride; - bytes[record_offset..record_offset + 4] - .copy_from_slice(&((index + 1) as u32).to_le_bytes()); - bytes[record_offset + 4..record_offset + 4 + name.len()] - .copy_from_slice(name.as_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET + 4] - .copy_from_slice(&linked.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET] = 1; - bytes[record_offset + 0x47..record_offset + 0x4b] - .copy_from_slice(&20000u32.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET + 4] - .copy_from_slice(&mutable_support_scalar_raw_u32.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET + 4] - .copy_from_slice(&young_company_support_scalar_raw_u32.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET] = bond_count; - for slot_index in 0..bond_count as usize { - let slot_offset = record_offset - + SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET - + slot_index * SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE; - let (principal, coupon_rate) = if index == 0 && slot_index == 0 { - (900_000i32, 0.08f32) - } else if index == 0 && slot_index == 1 { - (650_000i32, 0.12f32) - } else { - (500_000i32, 0.10f32) - }; - bytes[slot_offset..slot_offset + 4].copy_from_slice(&principal.to_le_bytes()); - bytes[slot_offset + 4..slot_offset + 8] - .copy_from_slice(&(1894u32 + slot_index as u32).to_le_bytes()); - bytes[slot_offset + 8..slot_offset + 12] - .copy_from_slice(&coupon_rate.to_le_bytes()); - } - bytes[record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET + 4] - .copy_from_slice(&merger.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET + 4] - .copy_from_slice(&takeover.to_le_bytes()); - let raw_capacity = track_capacity.unwrap_or(-1); - bytes[record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET + 4] - .copy_from_slice(&raw_capacity.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET + 4] - .copy_from_slice(&support_progress_word.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET + 4] - .copy_from_slice(&recent_per_share_subscore_raw_u32.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET + 4] - .copy_from_slice(&cached_share_price_raw_u32.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET + 4] - .copy_from_slice(&chairman_salary_baseline.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET + 4] - .copy_from_slice(&chairman_salary_current.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET + 4] - .copy_from_slice(&chairman_bonus_year.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET + 4] - .copy_from_slice(&chairman_bonus_amount.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET + 4] - .copy_from_slice(&founding_year.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET + 4] - .copy_from_slice(&last_bankruptcy_year.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET + 4] - .copy_from_slice(&last_dividend_year.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET + 4] - .copy_from_slice(¤t_issue_calendar_word.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2 - ..record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2 + 4] - .copy_from_slice(¤t_issue_calendar_word_2.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET - ..record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET + 4] - .copy_from_slice(&prior_issue_calendar_word.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2 - ..record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2 + 4] - .copy_from_slice(&prior_issue_calendar_word_2.to_le_bytes()); - bytes[record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET] = - preferred_locomotive_engine_type_raw_u8; - bytes[record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET] = - u8::from(city_connection_latch); - bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET] = - u8::from(linked_transit_latch); - if let Some(anchor_entry_id) = linked_transit_route_anchor_entry_id { - bytes[record_offset - + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET - ..record_offset - + SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_ENTRY_ID_OFFSET - + 4] - .copy_from_slice(&anchor_entry_id.to_le_bytes()); - } - for (fallback_index, relative_offset) in - SAVE_COMPANY_RECORD_LINKED_TRANSIT_ROUTE_ANCHOR_FALLBACK_COUNT_OFFSETS - .into_iter() - .enumerate() - { - bytes[record_offset + relative_offset..record_offset + relative_offset + 4] - .copy_from_slice( - &linked_transit_route_anchor_fallback_counts[fallback_index].to_le_bytes(), - ); - } - let current_cash: f64 = if index == 0 { 125_000.0 } else { -25_000.0 }; - let current_cash_slot_offset = record_offset - + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET - + (crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN as usize - * 8); - bytes[current_cash_slot_offset..current_cash_slot_offset + 8] - .copy_from_slice(¤t_cash.to_bits().to_le_bytes()); - } - - let header_probe = parse_save_company_collection_header_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("company header probe should parse"); - let roster = parse_save_company_roster_probe( - &bytes, - Some(&header_probe), - Some(&SmpSaveWorldSelectionContextProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-selected-company-and-chairman-context".to_string(), - chunk_tag_offset: 0, - payload_offset: 0, - payload_len: 0, - payload_len_hex: "0x0".to_string(), - selected_company_id_offset: 0, - selected_company_id: 2, - selected_company_id_hex: "0x00000002".to_string(), - selected_chairman_profile_id_offset: 0, - selected_chairman_profile_id: 1, - selected_chairman_profile_id_hex: "0x00000001".to_string(), - chairman_slot_selector_offset: 0, - chairman_slot_selectors: vec![], - campaign_override_flag_offset: 0, - campaign_override_flag: 0, - campaign_override_flag_hex: "0x00".to_string(), - chairman_role_gate_offset: 0, - chairman_role_gate_bytes: vec![], - evidence: vec![], - }), - ) - .expect("company roster should parse"); - - assert_eq!(roster.observed_entry_count, 2); - assert_eq!(roster.selected_company_id, Some(2)); - assert_eq!(roster.entries.len(), 2); - assert_eq!(roster.entries[0].company_id, 1); - assert_eq!(roster.entries[0].current_cash, 125_000); - assert_eq!(roster.entries[0].linked_chairman_profile_id, Some(1)); - assert_eq!(roster.entries[0].debt, 1_550_000); - assert_eq!(roster.entries[0].available_track_laying_capacity, Some(603)); - assert_eq!(roster.entries[0].merger_cooldown_year, Some(1862)); - let market_state = roster.entries[0] - .market_state - .as_ref() - .expect("company market state should load"); - assert_eq!(market_state.outstanding_shares, 20_000); - assert_eq!(market_state.live_bond_slots.len(), 2); - assert_eq!(market_state.live_bond_slots[0].principal, 900_000); - assert_eq!(market_state.live_bond_slots[0].maturity_year, 1894); - assert_eq!( - market_state.live_bond_slots[1].coupon_rate_raw_u32, - 0.12f32.to_bits() - ); - assert_eq!(market_state.largest_live_bond_principal, Some(900_000)); - assert_eq!( - market_state.highest_coupon_live_bond_principal, - Some(650_000) - ); - assert_eq!(market_state.mutable_support_scalar_raw_u32, 0x3f800000); - assert_eq!( - market_state.young_company_support_scalar_raw_u32, - 0x42340000 - ); - assert_eq!(market_state.support_progress_word, 17); - assert_eq!(market_state.recent_per_share_subscore_raw_u32, 0x41f00000); - assert_eq!(market_state.cached_share_price_raw_u32, 0x426c0000); - assert_eq!(market_state.chairman_salary_baseline, 24); - assert_eq!(market_state.chairman_salary_current, 31); - assert_eq!(market_state.chairman_bonus_year, 1849); - assert_eq!(market_state.chairman_bonus_amount, 1250); - assert_eq!(market_state.founding_year, 1842); - assert_eq!(market_state.last_bankruptcy_year, 1851); - assert_eq!(market_state.last_dividend_year, 1848); - assert_eq!(market_state.current_issue_calendar_word, 7); - assert_eq!(market_state.current_issue_calendar_word_2, 8); - assert_eq!(market_state.prior_issue_calendar_word, 6); - assert_eq!(market_state.prior_issue_calendar_word_2, 7); - assert_eq!(market_state.linked_transit_route_anchor_entry_id, Some(77)); - assert_eq!( - market_state.linked_transit_route_anchor_fallback_counts, - vec![3, 5, 8] - ); - assert_eq!( - roster.entries[0].preferred_locomotive_engine_type_raw_u8, - Some(2) - ); - assert!(market_state.city_connection_latch); - assert!(!market_state.linked_transit_latch); - assert_eq!( - market_state.stat_band_root_0cfb_candidates.len(), - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS - ); - assert_eq!( - market_state.stat_band_root_0d7f_candidates.len(), - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS - ); - assert_eq!( - market_state.stat_band_root_1c47_candidates.len(), - SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS - ); - assert_eq!( - market_state.stat_band_root_0cfb_candidates[31].label, - "stat_band_0cfb_word_32" - ); - assert_eq!( - market_state.stat_band_root_0cfb_candidates[31].relative_offset_hex, - "0xd77" - ); - assert_eq!(roster.entries[1].company_id, 2); - assert_eq!(roster.entries[1].current_cash, -25_000); - assert_eq!(roster.entries[1].linked_chairman_profile_id, Some(2)); - assert_eq!(roster.entries[1].debt, 500_000); - assert_eq!(roster.entries[1].available_track_laying_capacity, None); - assert_eq!(roster.entries[1].takeover_cooldown_year, Some(1871)); - let second_market_state = roster.entries[1] - .market_state - .as_ref() - .expect("second company market state should load"); - assert_eq!( - second_market_state.largest_live_bond_principal, - Some(500_000) - ); - assert_eq!( - second_market_state.highest_coupon_live_bond_principal, - Some(500_000) - ); - assert_eq!(second_market_state.chairman_bonus_year, 0); - assert_eq!(second_market_state.chairman_bonus_amount, 0); - assert_eq!(second_market_state.last_dividend_year, 1850); - assert_eq!(second_market_state.current_issue_calendar_word, 3); - assert_eq!(second_market_state.current_issue_calendar_word_2, 4); - assert_eq!(second_market_state.prior_issue_calendar_word, 2); - assert_eq!(second_market_state.prior_issue_calendar_word_2, 3); - assert_eq!( - second_market_state.linked_transit_route_anchor_entry_id, - Some(41) - ); - assert_eq!( - second_market_state.linked_transit_route_anchor_fallback_counts, - vec![13, 21, 34] - ); - assert_eq!( - roster.entries[1].preferred_locomotive_engine_type_raw_u8, - Some(1) - ); - assert!(!second_market_state.city_connection_latch); - assert!(second_market_state.linked_transit_latch); - } - - #[test] - fn parses_save_chairman_profile_table_probe_from_direct_records() { - let metadata_tag_offset = 0x40usize; - let stride = 0xcabusize; - let count = 2usize; - let start_offset = 0x4eusize; - let total_len = metadata_tag_offset + 4 + start_offset + stride * count + 0x200; - let mut bytes = vec![0u8; total_len]; - bytes[metadata_tag_offset..metadata_tag_offset + 4] - .copy_from_slice(&0x00005209u32.to_le_bytes()); - let header_words = [ - 1u32, - 0xcab, - 8, - 6, - 8, - count as u32, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - ]; - for (index, word) in header_words.into_iter().enumerate() { - let offset = metadata_tag_offset + 4 + index * 4; - bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); - } - let records_tag_offset = metadata_tag_offset + 4 + 0x100; - let close_tag_offset = records_tag_offset + 4; - bytes[records_tag_offset..records_tag_offset + 4] - .copy_from_slice(&0x0000520au32.to_le_bytes()); - bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); - - for (index, (name, linked, cash, cache0, cache1, cache4, holdings)) in [ - ( - "Collis Huntington", - 1u32, - -107644.0f64, - 252508.0f64, - 0.0f64, - 0.0f64, - vec![(1u32, 6000u32)], - ), - ( - "Thomas Durant", - 2u32, - -382718.0f64, - -283562.0f64, - 822000.0f64, - 1_392_000.0f64, - vec![(2u32, 9000u32)], - ), - ] - .into_iter() - .enumerate() - { - let record_offset = metadata_tag_offset + 4 + start_offset + index * stride; - bytes[record_offset..record_offset + 4] - .copy_from_slice(&((index + 1) as u32).to_le_bytes()); - bytes[record_offset + 4..record_offset + 8].copy_from_slice(&1u32.to_le_bytes()); - bytes[record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET - ..record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET + name.len()] - .copy_from_slice(name.as_bytes()); - bytes[record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET - ..record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET + 8] - .copy_from_slice(&cash.to_le_bytes()); - bytes[record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET - ..record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET + 4] - .copy_from_slice(&linked.to_le_bytes()); - bytes[record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET] = - (index as u8) + 10; - bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET - ..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET + 8] - .copy_from_slice(&cache0.to_le_bytes()); - bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET - ..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET + 8] - .copy_from_slice(&cache1.to_le_bytes()); - bytes[record_offset + 0x211..record_offset + 0x211 + 8] - .copy_from_slice(&cache4.to_le_bytes()); - for (company_id, units) in holdings { - let slot_offset = record_offset - + SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET - + company_id as usize * 4; - bytes[slot_offset..slot_offset + 4].copy_from_slice(&units.to_le_bytes()); - } - } - - let header_probe = parse_save_chairman_profile_collection_header_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("chairman header probe should parse"); - let table = parse_save_chairman_profile_table_probe( - &bytes, - Some(&header_probe), - Some(&SmpSaveWorldSelectionContextProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-selected-company-and-chairman-context".to_string(), - chunk_tag_offset: 0, - payload_offset: 0, - payload_len: 0, - payload_len_hex: "0x0".to_string(), - selected_company_id_offset: 0, - selected_company_id: 2, - selected_company_id_hex: "0x00000002".to_string(), - selected_chairman_profile_id_offset: 0, - selected_chairman_profile_id: 2, - selected_chairman_profile_id_hex: "0x00000002".to_string(), - chairman_slot_selector_offset: 0, - chairman_slot_selectors: vec![], - campaign_override_flag_offset: 0, - campaign_override_flag: 0, - campaign_override_flag_hex: "0x00".to_string(), - chairman_role_gate_offset: 0, - chairman_role_gate_bytes: vec![], - evidence: vec![], - }), - Some(&SmpSaveTaggedCollectionHeaderProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-company-tagged-header-counts".to_string(), - semantic_family: "scenario-save-company-header-counts".to_string(), - metadata_tag_offset: 0, - records_tag_offset: 0, - close_tag_offset: 0, - direct_collection_flag: 1, - direct_collection_flag_hex: "0x00000001".to_string(), - direct_record_stride: 0x7684, - direct_record_stride_hex: "0x00007684".to_string(), - live_id_bound: 5, - live_id_bound_hex: "0x00000005".to_string(), - live_record_count: 2, - live_record_count_hex: "0x00000002".to_string(), - header_words: vec![], - header_hex_words: vec![], - evidence: vec![], - }), - ) - .expect("chairman profile table should parse"); - - assert_eq!(table.observed_entry_count, 2); - assert_eq!(table.selected_chairman_profile_id, Some(2)); - assert_eq!(table.entries.len(), 2); - assert_eq!(table.entries[0].profile_id, 1); - assert_eq!(table.entries[0].name, "Collis Huntington"); - assert_eq!(table.entries[0].linked_company_id, Some(1)); - assert_eq!(table.entries[0].company_holdings.get(&1), Some(&6000)); - assert_eq!(table.entries[0].current_cash, -107644); - assert_eq!(table.entries[0].holdings_value_total, 252508); - assert_eq!(table.entries[0].purchasing_power_total, 144864); - assert_eq!(table.entries[0].personality_byte_0x291, Some(10)); - assert_eq!(table.entries[1].profile_id, 2); - assert_eq!(table.entries[1].company_holdings.get(&2), Some(&9000)); - assert_eq!(table.entries[1].holdings_value_total, 822000); - assert_eq!(table.entries[1].purchasing_power_total, 1_009_282); - assert_eq!(table.entries[1].personality_byte_0x291, Some(11)); - } - - #[test] - fn builds_save_world_selection_role_analysis_from_probe() { - let probe = SmpSaveWorldSelectionContextProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-direct-world-block".to_string(), - semantic_family: "scenario-selected-company-and-chairman-context".to_string(), - chunk_tag_offset: 0, - payload_offset: 0, - payload_len: 0x4f2c, - payload_len_hex: "0x4f2c".to_string(), - selected_company_id_offset: 0x21, - selected_company_id: 3, - selected_company_id_hex: "0x00000003".to_string(), - selected_chairman_profile_id_offset: 0x25, - selected_chairman_profile_id: 7, - selected_chairman_profile_id_hex: "0x00000007".to_string(), - chairman_slot_selector_offset: 0x87, - chairman_slot_selectors: vec![1, 0, 2, 0], - campaign_override_flag_offset: 0xc5, - campaign_override_flag: 1, - campaign_override_flag_hex: "0x01".to_string(), - chairman_role_gate_offset: 0x0bc3, - chairman_role_gate_bytes: vec![2, 0, 1, 0], - evidence: vec![], - }; - - let analysis = build_save_world_selection_role_analysis(&probe); - - assert_eq!(analysis.selected_company_id, 3); - assert_eq!(analysis.selected_chairman_profile_id, 7); - assert_eq!(analysis.campaign_override_flag_hex, "0x01"); - assert_eq!(analysis.chairman_slots.len(), 4); - assert_eq!(analysis.chairman_slots[0].selector_byte_hex, "0x01"); - assert_eq!(analysis.chairman_slots[2].role_gate_byte_hex, "0x01"); - } - - #[test] - fn builds_save_candidate_views_with_raw_bits() { - let mut bytes = vec![0u8; 0x40]; - bytes[0x08..0x0c].copy_from_slice(&0x3f800000u32.to_le_bytes()); - bytes[0x10..0x18].copy_from_slice(&(-2458.0f64).to_le_bytes()); - - let dword = build_save_dword_candidate(&bytes, 0, "unit_float", 0x08) - .expect("dword candidate should build"); - let qword = - build_save_qword_candidate(&bytes, 0, 0x10).expect("qword candidate should build"); - - assert_eq!(dword.raw_u32_hex, "0x3f800000"); - assert_eq!(dword.value_i32, 1_065_353_216); - assert_eq!(dword.value_f32, 1.0); - assert_eq!(qword.raw_u64, (-2458.0f64).to_bits()); - assert_eq!(qword.value_i64, (-2458.0f64).to_bits() as i64); - assert_eq!(qword.value_f64, -2458.0); - } - - #[test] - fn derives_chairman_holdings_share_price_total_from_grounded_company_prices() { - let holdings_by_company = - BTreeMap::from([(2u32, 19_000u32), (4u32, 1_000u32), (6u32, 2_000u32)]); - let company_share_prices = BTreeMap::from([(2u32, 66i64), (4u32, 69i64), (6u32, 27i64)]); - - let total = - derive_chairman_holdings_share_price_total(&holdings_by_company, &company_share_prices) - .expect("derived holdings total should compute"); - - assert_eq!(total, 1_377_000); - } - - #[test] - fn derives_chairman_cached_purchasing_power_from_strongest_nonnegative_cache() { - let cached_scalar_candidates = vec![ - SmpSaveScalarCandidate { - relative_offset: 0x1e9, - relative_offset_hex: "0x1e9".to_string(), - raw_u64: (-343_508.0f64).to_bits(), - raw_u64_hex: format!("0x{:016x}", (-343_508.0f64).to_bits()), - value_i64: round_f64_to_i64(-343_508.0).expect("i64"), - value_f64: -343_508.0, - }, - SmpSaveScalarCandidate { - relative_offset: 0x201, - relative_offset_hex: "0x201".to_string(), - raw_u64: 1_386_000.0f64.to_bits(), - raw_u64_hex: format!("0x{:016x}", 1_386_000.0f64.to_bits()), - value_i64: round_f64_to_i64(1_386_000.0).expect("i64"), - value_f64: 1_386_000.0, - }, - SmpSaveScalarCandidate { - relative_offset: 0x211, - relative_offset_hex: "0x211".to_string(), - raw_u64: 1_392_000.0f64.to_bits(), - raw_u64_hex: format!("0x{:016x}", 1_392_000.0f64.to_bits()), - value_i64: round_f64_to_i64(1_392_000.0).expect("i64"), - value_f64: 1_392_000.0, - }, - ]; - - let total = - derive_chairman_cached_purchasing_power_total(-463_436, &cached_scalar_candidates) - .expect("derived purchasing power should compute"); - - assert_eq!(total, 928_564); - } - - #[test] - fn classifies_rt3_105_post_span_bridge_variants() { - let base_trailer = SmpRuntimeTrailerBlock { - profile_family: "rt3-105-save-container-v1".to_string(), - trailer_family: "rt3-105-save-trailer-v1".to_string(), - trailer_evidence: vec![], - trailer_offset: 944, - prefix_words_0_to_5: vec![], - prefix_hex_words_0_to_5: vec![], - tag_word_6: 0, - tag_word_6_hex: String::new(), - tag_chunk_id_u16: 0x2ee1, - tag_chunk_id_hex: "0x2ee1".to_string(), - tag_chunk_id_grounded_alignment: None, - length_word_7: 0x32c8_0000, - length_word_7_hex: "0x32c80000".to_string(), - length_high_u16: 0x32c8, - length_high_hex: "0x32c8".to_string(), - selector_word_8: 0x7110_0000, - selector_word_8_hex: "0x71100000".to_string(), - selector_high_u16: 0x7110, - selector_high_hex: "0x7110".to_string(), - layout_word_9: 0, - layout_word_9_hex: String::new(), - descriptor_word_10: 0x7801_0000, - descriptor_word_10_hex: "0x78010000".to_string(), - descriptor_high_u16: 0x7801, - descriptor_high_hex: "0x7801".to_string(), - descriptor_word_11: 0, - descriptor_word_11_hex: String::new(), - counter_word_12: 0, - counter_word_12_hex: String::new(), - offset_word_13: 0, - offset_word_13_hex: String::new(), - span_word_14: 0, - span_word_14_hex: String::new(), - mode_word_15: 0, - mode_word_15_hex: String::new(), - words: vec![], - hex_words: vec![], - }; - let base_post_span = SmpRuntimePostSpanProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - span_target_offset: 13944, - next_nonzero_offset: Some(14795), - next_aligned_candidate_offset: Some(20244), - next_aligned_candidate_words: vec![], - next_aligned_candidate_hex_words: vec![], - header_candidates: vec![SmpRuntimePostSpanHeaderCandidate { - offset: 20244, - words: vec![], - hex_words: vec![], - dense_word_count: 3, - high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515], - high_hex_words: vec![], - grounded_alignments: vec![], - }], - grounded_progress_hits: vec![], - }; - let base_profile = SmpRt3105PackedProfileProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - packed_profile_offset: 29632, - packed_profile_len: 0x108, - packed_profile_len_hex: "0x108".to_string(), - 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: 0x0100_0000, - 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: 0, - profile_byte_0x97_hex: "0x00".to_string(), - profile_byte_0xc5: 0, - profile_byte_0xc5_hex: "0x00".to_string(), - stable_nonzero_words: vec![], - }, - ascii_runs: vec![], - }; - let base_bridge = parse_rt3_105_post_span_bridge_probe( - Some(&base_trailer), - Some(&base_post_span), - Some(&base_profile), - ) - .expect("base bridge should parse"); - assert_eq!( - base_bridge.bridge_family, - "rt3-105-save-post-span-bridge-v1" - ); - assert_eq!(base_bridge.packed_profile_delta_from_span_target, 15688); - assert_eq!( - base_bridge.next_candidate_delta_from_packed_profile, - Some(-9388) - ); - let base_variant_trailer = SmpRuntimeTrailerBlock { - descriptor_word_10: 0x7401_0000, - descriptor_word_10_hex: "0x74010000".to_string(), - descriptor_high_u16: 0x7401, - descriptor_high_hex: "0x7401".to_string(), - ..base_trailer.clone() - }; - let base_variant_bridge = parse_rt3_105_post_span_bridge_probe( - Some(&base_variant_trailer), - Some(&base_post_span), - Some(&base_profile), - ) - .expect("base bridge variant should parse"); - assert_eq!( - base_variant_bridge.bridge_family, - "rt3-105-save-post-span-bridge-v1" - ); - - let alt_trailer = SmpRuntimeTrailerBlock { - profile_family: "rt3-105-alt-save-container-v1".to_string(), - selector_word_8: 0x54cd_0000, - selector_word_8_hex: "0x54cd0000".to_string(), - selector_high_u16: 0x54cd, - selector_high_hex: "0x54cd".to_string(), - descriptor_word_10: 0x5901_0000, - descriptor_word_10_hex: "0x59010000".to_string(), - descriptor_high_u16: 0x5901, - descriptor_high_hex: "0x5901".to_string(), - ..base_trailer.clone() - }; - let alt_post_span = SmpRuntimePostSpanProbe { - profile_family: "rt3-105-alt-save-container-v1".to_string(), - next_aligned_candidate_offset: Some(29892), - header_candidates: vec![SmpRuntimePostSpanHeaderCandidate { - offset: 29892, - words: vec![], - hex_words: vec![], - dense_word_count: 3, - high_u16_words: vec![0x1500, 0x0100, 0x4100, 0x0200], - high_hex_words: vec![], - grounded_alignments: vec![], - }], - ..base_post_span.clone() - }; - let alt_profile = SmpRt3105PackedProfileProbe { - profile_family: "rt3-105-alt-save-container-v1".to_string(), - packed_profile_block: SmpRt3105PackedProfileBlock { - map_path: Some("Spanish Mainline.gmp".to_string()), - display_name: Some("Spanish Mainline".to_string()), - profile_byte_0x82: 0xa3, - profile_byte_0x82_hex: "0xa3".to_string(), - ..base_profile.packed_profile_block.clone() - }, - ..base_profile.clone() - }; - let alt_bridge = parse_rt3_105_post_span_bridge_probe( - Some(&alt_trailer), - Some(&alt_post_span), - Some(&alt_profile), - ) - .expect("alt bridge should parse"); - assert_eq!( - alt_bridge.bridge_family, - "rt3-105-alt-save-post-span-bridge-v1" - ); - assert_eq!( - alt_bridge.next_candidate_delta_from_packed_profile, - Some(260) - ); - - let scenario_trailer = SmpRuntimeTrailerBlock { - profile_family: "rt3-105-scenario-save-container-v1".to_string(), - trailer_family: "unknown".to_string(), - trailer_offset: 864, - length_word_7: 0, - length_word_7_hex: "0x00000000".to_string(), - length_high_u16: 0, - length_high_hex: "0x0000".to_string(), - selector_word_8: 0x0001_86a0, - selector_word_8_hex: "0x000186a0".to_string(), - selector_high_u16: 0x0001, - selector_high_hex: "0x0001".to_string(), - descriptor_word_10: 0x0186_a000, - descriptor_word_10_hex: "0x0186a000".to_string(), - descriptor_high_u16: 0x0186, - descriptor_high_hex: "0x0186".to_string(), - ..base_trailer.clone() - }; - let scenario_post_span = SmpRuntimePostSpanProbe { - profile_family: "rt3-105-scenario-save-container-v1".to_string(), - span_target_offset: 864, - next_aligned_candidate_offset: Some(940), - header_candidates: vec![SmpRuntimePostSpanHeaderCandidate { - offset: 940, - words: vec![], - hex_words: vec![], - dense_word_count: 3, - high_u16_words: vec![0x0186, 0x0006, 0x0006, 0x0001], - high_hex_words: vec![], - grounded_alignments: vec![], - }], - ..base_post_span.clone() - }; - let scenario_profile = SmpRt3105PackedProfileProbe { - profile_family: "rt3-105-scenario-save-container-v1".to_string(), - packed_profile_block: SmpRt3105PackedProfileBlock { - map_path: Some("Southern Pacific.gmp".to_string()), - display_name: Some("Southern Pacific".to_string()), - profile_byte_0x82: 0x90, - profile_byte_0x82_hex: "0x90".to_string(), - ..base_profile.packed_profile_block.clone() - }, - ..base_profile.clone() - }; - let scenario_bridge = parse_rt3_105_post_span_bridge_probe( - Some(&scenario_trailer), - Some(&scenario_post_span), - Some(&scenario_profile), - ) - .expect("scenario bridge should parse"); - assert_eq!( - scenario_bridge.bridge_family, - "rt3-105-scenario-post-span-bridge-v1" - ); - assert_eq!( - scenario_bridge.next_candidate_delta_from_packed_profile, - Some(-28692) - ); - } - - #[test] - fn parses_rt3_105_save_bridge_payload_probe() { - let mut bytes = vec![0u8; 0x7000]; - let primary = 0x4f14usize; - let secondary = 0x671cusize; - let primary_words: [u32; 8] = [ - 0x62000000, 0x00000000, 0xfff70000, 0x55150000, 0x55550000, 0x00000000, 0xfff70000, - 0x54550000, - ]; - for (index, word) in primary_words.iter().enumerate() { - bytes[primary + index * 4..primary + (index + 1) * 4] - .copy_from_slice(&(*word).to_le_bytes()); - } - - let secondary_words: [u32; 8] = [ - 0x00050000, 0x00050005, 0xfff70000, 0x54540000, 0x545400f9, 0x00f900f9, 0x00f94008, - 0x00001555, - ]; - for (index, word) in secondary_words.iter().enumerate() { - bytes[secondary + index * 4..secondary + (index + 1) * 4] - .copy_from_slice(&(*word).to_le_bytes()); - } - - let bridge_probe = SmpRt3105PostSpanBridgeProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(), - bridge_evidence: vec![], - span_target_offset: 0x3678, - next_candidate_offset: Some(primary), - next_candidate_delta_from_span_target: Some(primary - 0x3678), - packed_profile_offset: 0x73c0, - packed_profile_delta_from_span_target: 0x3d48, - next_candidate_delta_from_packed_profile: Some(primary as i64 - 0x73c0), - selector_high_u16: 0x7110, - selector_high_hex: "0x7110".to_string(), - descriptor_high_u16: 0x7801, - descriptor_high_hex: "0x7801".to_string(), - next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515], - next_candidate_high_hex_words: vec![], - }; - - let probe = parse_rt3_105_save_bridge_payload_probe(&bytes, Some(&bridge_probe)) - .expect("save bridge payload probe should parse"); - - assert_eq!(probe.primary_block_offset, primary); - assert_eq!(probe.primary_block_len, 0x20); - assert_eq!(probe.secondary_block_offset, secondary); - assert_eq!(probe.secondary_block_delta_from_primary, 0x1808); - assert_eq!(probe.secondary_block_end_offset, 0x73c0); - assert_eq!(probe.secondary_block_len, 0xca4); - assert_eq!(probe.primary_words[..4], primary_words[..4]); - assert_eq!(probe.secondary_words[..8], secondary_words[..8]); - } - - #[test] - fn parses_rt3_105_save_name_table_probe() { - let mut bytes = vec![0u8; 0x7400]; - let secondary = 0x671cusize; - let header = secondary + 0x354; - let entries = secondary + 0x3b5; - let stride = 0x22usize; - let names = ["AluminumMill", "Nuclear Power Plant", "Bakery"]; - - bytes[header..header + 4].copy_from_slice(&0x10000000u32.to_le_bytes()); - bytes[header + 4..header + 8].copy_from_slice(&0x00009000u32.to_le_bytes()); - bytes[header + 8..header + 12].copy_from_slice(&0x0000332eu32.to_le_bytes()); - bytes[header + 0x1c..header + 0x20].copy_from_slice(&4u32.to_le_bytes()); - bytes[header + 0x20..header + 0x24].copy_from_slice(&(names.len() as u32).to_le_bytes()); - bytes[header + 12..header + 16].copy_from_slice(&1u32.to_le_bytes()); - bytes[header + 16..header + 20].copy_from_slice(&0x22u32.to_le_bytes()); - bytes[header + 20..header + 24].copy_from_slice(&2u32.to_le_bytes()); - bytes[header + 24..header + 28].copy_from_slice(&2u32.to_le_bytes()); - bytes[header + 0x28..header + 0x2c].copy_from_slice(&1u32.to_le_bytes()); - - for (index, name) in names.iter().enumerate() { - let off = entries + index * stride; - let raw = &mut bytes[off..off + stride]; - raw[..name.len()].copy_from_slice(name.as_bytes()); - let trailer = if *name == "Nuclear Power Plant" { - 0u32 - } else { - 1u32 - }; - raw[stride - 4..stride].copy_from_slice(&trailer.to_le_bytes()); - } - let footer = entries + names.len() * stride; - bytes[footer..footer + 4].copy_from_slice(&0x32dcu32.to_le_bytes()); - bytes[footer + 4..footer + 8].copy_from_slice(&0x3714u32.to_le_bytes()); - bytes[footer + 8] = 0x00; - - let payload = SmpRt3105SaveBridgePayloadProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(), - primary_block_offset: 0x4f14, - primary_block_len: 0x20, - primary_block_len_hex: "0x20".to_string(), - primary_words: vec![], - primary_hex_words: vec![], - secondary_block_offset: secondary, - secondary_block_delta_from_primary: 0x1808, - secondary_block_delta_from_primary_hex: "0x1808".to_string(), - secondary_block_end_offset: footer + 9, - secondary_block_len: footer + 9 - secondary, - secondary_block_len_hex: format!("0x{:x}", footer + 9 - secondary), - secondary_preview_word_count: 32, - secondary_words: vec![], - secondary_hex_words: vec![], - evidence: vec![], - }; - - let probe = parse_rt3_105_save_name_table_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&payload), - ) - .expect("save name table probe should parse"); - - assert_eq!(probe.source_kind, "save-bridge-secondary-block"); - assert_eq!( - probe.semantic_family, - "scenario-named-candidate-availability-table" - ); - assert_eq!(probe.header_offset, header); - assert_eq!(probe.entry_stride, stride); - assert_eq!(probe.observed_entry_capacity, 4); - assert_eq!(probe.observed_entry_count, names.len()); - assert_eq!(probe.entries[0].text, "AluminumMill"); - assert_eq!(probe.entries[0].availability_dword, 1); - assert_eq!(probe.entries[2].text, "Bakery"); - assert_eq!(probe.zero_trailer_entry_count, 1); - assert_eq!( - probe.zero_trailer_entry_names, - vec!["Nuclear Power Plant".to_string()] - ); - assert_eq!(probe.trailing_footer_hex, "dc3200001437000000"); - assert_eq!(probe.footer_progress_word_0, 0x32dc); - assert_eq!(probe.footer_progress_word_1, 0x3714); - assert_eq!(probe.footer_trailing_byte, 0x00); - } - - #[test] - fn parses_rt3_105_map_name_table_probe_from_fixed_offsets() { - let mut bytes = vec![0u8; 0x7400]; - let header = 0x6a70usize; - let entries = 0x6ad1usize; - let stride = 0x22usize; - let observed_entry_count = 67usize; - - bytes[header..header + 4].copy_from_slice(&0x00000000u32.to_le_bytes()); - bytes[header + 4..header + 8].copy_from_slice(&0x00000000u32.to_le_bytes()); - bytes[header + 8..header + 12].copy_from_slice(&0x0000332eu32.to_le_bytes()); - bytes[header + 12..header + 16].copy_from_slice(&1u32.to_le_bytes()); - bytes[header + 16..header + 20].copy_from_slice(&0x22u32.to_le_bytes()); - bytes[header + 20..header + 24].copy_from_slice(&2u32.to_le_bytes()); - bytes[header + 24..header + 28].copy_from_slice(&2u32.to_le_bytes()); - bytes[header + 0x1c..header + 0x20].copy_from_slice(&0x44u32.to_le_bytes()); - bytes[header + 0x20..header + 0x24] - .copy_from_slice(&(observed_entry_count as u32).to_le_bytes()); - bytes[header + 0x28..header + 0x2c].copy_from_slice(&1u32.to_le_bytes()); - - for index in 0..observed_entry_count { - let name = match index { - 0 => "AutoPlant".to_string(), - 1 => "Nuclear Power Plant".to_string(), - 66 => "Warehouse11".to_string(), - _ => format!("Entry{index:02}"), - }; - let off = entries + index * stride; - let raw = &mut bytes[off..off + stride]; - raw[..name.len()].copy_from_slice(name.as_bytes()); - let trailer = if name == "Nuclear Power Plant" { - 0u32 - } else { - 1u32 - }; - raw[stride - 4..stride].copy_from_slice(&trailer.to_le_bytes()); - } - - let footer = entries + observed_entry_count * stride; - bytes[footer..footer + 4].copy_from_slice(&0x32dcu32.to_le_bytes()); - bytes[footer + 4..footer + 8].copy_from_slice(&0x3714u32.to_le_bytes()); - bytes[footer + 8] = 0x00; - - let probe = parse_rt3_105_save_name_table_probe( - &bytes, - Some("gmp"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-map-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - None, - ) - .expect("map name table probe should parse"); - - assert_eq!(probe.profile_family, "rt3-105-map-container-v1"); - assert_eq!(probe.source_kind, "map-fixed-catalog-range"); - assert_eq!(probe.header_offset, header); - assert_eq!(probe.entries_offset, entries); - assert_eq!(probe.observed_entry_count, observed_entry_count); - assert_eq!(probe.entries[0].text, "AutoPlant"); - assert_eq!(probe.entries[66].text, "Warehouse11"); - assert_eq!( - probe.zero_trailer_entry_names, - vec!["Nuclear Power Plant".to_string()] - ); - assert_eq!(probe.footer_progress_word_0, 0x32dc); - assert_eq!(probe.footer_progress_word_1, 0x3714); - } - - #[test] - fn parses_map_title_hint_probe_from_grounded_titles_and_embedded_map_reference() { - let mut bytes = vec![0u8; 0x9000]; - let embedded_reference = b"Dutchlantis.gmp"; - let title = b"Dutchlantis"; - let later_title = b"Germany"; - - bytes[0x73d0..0x73d0 + embedded_reference.len()].copy_from_slice(embedded_reference); - bytes[0x73e0..0x73e0 + title.len()].copy_from_slice(title); - bytes[0x8400..0x8400 + later_title.len()].copy_from_slice(later_title); - - let probe = parse_map_title_hint_probe( - &bytes, - Some("gmp"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-map-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - ) - .expect("map title hint probe should parse"); - - assert_eq!(probe.source_kind, "grounded-title-string-scan"); - assert_eq!( - probe.profile_family, - Some("rt3-105-map-container-v1".to_string()) - ); - assert_eq!(probe.grounded_title_hits.len(), 2); - assert_eq!(probe.grounded_title_hits[0].title, "Germany"); - assert_eq!(probe.grounded_title_hits[0].earliest_offset, 0x8400); - assert_eq!(probe.grounded_title_hits[1].title, "Dutchlantis"); - assert_eq!(probe.grounded_title_hits[1].earliest_offset, 0x73d0); - assert_eq!(probe.embedded_map_references.len(), 1); - assert_eq!(probe.embedded_map_references[0].offset, 0x73d0); - assert_eq!(probe.embedded_map_references[0].text, "Dutchlantis.gmp"); - assert_eq!(probe.adjacent_reference_title_pairs.len(), 1); - assert_eq!( - probe - .strongest_same_stem_pair - .as_ref() - .map(|pair| pair.title.as_str()), - Some("Dutchlantis") - ); - let pair = probe.strongest_same_stem_pair.expect("same-stem pair"); - assert_eq!(pair.map_reference_offset, 0x73d0); - assert_eq!(pair.title_offset, 0x73d0); - assert!(pair.normalized_stem_match); - assert_eq!(pair.byte_distance, 0); - } - - #[test] - fn parses_rt3_105_save_named_locomotive_availability_probe() { - let mut bytes = vec![0u8; 0x9000]; - let packed_profile_offset = 0x73c0usize; - let packed_profile_len = 0x108usize; - let entries_offset = 0x7c78usize; - let names = [ - ("Eight Wheeler 4-4-0", 1u32), - ("EP-2 Bipolar", 1u32), - ("ET22", 1u32), - ("F3", 0u32), - ("Fairlie 0-6-6-0", 1u32), - ("Firefly 2-2-2", 0u32), - ("FP45", 0u32), - ("Ge 6/6 Crocodile", 1u32), - ("GG1", 0u32), - ("GP7", 1u32), - ]; - - for (index, (name, value)) in names.iter().enumerate() { - let offset = entries_offset + index * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE; - bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); - bytes[offset + 4..offset + 4 + name.len()].copy_from_slice(name.as_bytes()); - } - - let probe = parse_rt3_105_save_named_locomotive_availability_probe( - &bytes, - Some("gms"), - Some(&SmpContainerProfile { - profile_family: "rt3-105-save-container-v1".to_string(), - profile_evidence: vec![], - is_known_profile: true, - }), - Some(&SmpRt3105PackedProfileProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - packed_profile_offset, - packed_profile_len, - packed_profile_len_hex: "0x108".to_string(), - packed_profile_block: SmpRt3105PackedProfileBlock { - relative_len: packed_profile_len, - 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: 1, - header_flag_word_3_hex: "0x00000001".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: 0, - profile_byte_0x97_hex: "0x00".to_string(), - profile_byte_0xc5: 0, - profile_byte_0xc5_hex: "0x00".to_string(), - stable_nonzero_words: vec![], - }, - ascii_runs: vec![], - }), - ) - .expect("save-side locomotive table probe should parse"); - - assert_eq!(probe.source_kind, "save-direct-locomotive-row-run"); - assert_eq!( - probe.semantic_family, - "scenario-named-locomotive-availability-table" - ); - assert_eq!(probe.entries_offset, entries_offset); - assert_eq!( - probe.entry_stride, - RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE - ); - assert_eq!(probe.observed_entry_count, names.len()); - assert_eq!(probe.zero_availability_count, 4); - assert_eq!(probe.entries[0].text, "Eight Wheeler 4-4-0"); - assert_eq!(probe.entries[9].text, "GP7"); - } - - #[test] - fn classifies_rt3_105_alt_save_container_profile() { - let shared_header = SmpSharedHeader { - byte_len: 64, - root_kind_word: 0x000025e5, - root_kind_word_hex: "0x000025e5".to_string(), - primary_family_tag: 0x00002ee0, - primary_family_tag_hex: "0x00002ee0".to_string(), - shared_signature_words_1_to_7: vec![ - 0x00002ee0, 0x0001c001, 0x00018000, 0x00010000, 0x00000754, 0x00000754, 0x00000754, - ], - shared_signature_hex_words_1_to_7: vec![ - "0x00002ee0".to_string(), - "0x0001c001".to_string(), - "0x00018000".to_string(), - "0x00010000".to_string(), - "0x00000754".to_string(), - "0x00000754".to_string(), - "0x00000754".to_string(), - ], - matches_grounded_common_signature: false, - payload_window_words_8_to_9: vec![0x007a5978, 0x007a9022], - payload_window_hex_words_8_to_9: vec![ - "0x007a5978".to_string(), - "0x007a9022".to_string(), - ], - reserved_words_10_to_14: vec![0; 5], - reserved_words_10_to_14_all_zero: true, - final_flag_word: 0, - final_flag_word_hex: "0x00000000".to_string(), - }; - let early_content_probe = SmpEarlyContentProbe { - first_post_text_nonzero_offset: 722, - zero_pad_after_text_len: 431, - first_post_text_block_len: 35, - first_post_text_block_hex: - "0101010000010000000000000100000000000000010000000000000000010100000001".to_string(), - trailing_zero_pad_after_first_block_len: 45, - secondary_nonzero_offset: Some(802), - secondary_aligned_word_window_offset: Some(800), - secondary_aligned_word_window_words: vec![ - 0x00010000, 0x49f00100, 0x00000002, 0xa0000000, 0x00000186, 0x00000000, 0x000186a0, - 0x00000000, - ], - secondary_aligned_word_window_hex_words: vec![ - "0x00010000".to_string(), - "0x49f00100".to_string(), - "0x00000002".to_string(), - "0xa0000000".to_string(), - "0x00000186".to_string(), - "0x00000000".to_string(), - "0x000186a0".to_string(), - "0x00000000".to_string(), - ], - secondary_preview_hex: - "01000001f04902000000000000a08601000000000000a08601000000000000a0".to_string(), - }; - - let header_variant = classify_header_variant_probe(&shared_header); - let secondary_variant = - classify_secondary_variant_probe(&early_content_probe).expect("secondary probe"); - let container_profile = classify_container_profile( - Some("gms"), - Some(&header_variant), - Some(&secondary_variant), - ) - .expect("container profile"); - - assert_eq!(header_variant.variant_family, "rt3-105-alt-save-header-v1"); - assert_eq!( - secondary_variant.variant_family, - "rt3-105-gms-alt-family-v1" - ); - assert_eq!( - container_profile.profile_family, - "rt3-105-alt-save-container-v1" - ); - assert!(container_profile.is_known_profile); - } - - #[test] - fn classifies_rt3_105_map_container_profiles_from_header_families() { - let scenario_profile = classify_container_profile( - Some("gmp"), - Some(&SmpHeaderVariantProbe { - variant_family: "rt3-105-scenario-save-header-v1".to_string(), - variant_evidence: vec![], - is_known_family: true, - }), - Some(&SmpSecondaryVariantProbe { - aligned_window_offset: 0, - words: vec![1, 0, 0, 0], - hex_words: vec![], - variant_family: "unknown".to_string(), - variant_evidence: vec![], - }), - ) - .expect("scenario map profile"); - - let alt_profile = classify_container_profile( - Some("gmp"), - Some(&SmpHeaderVariantProbe { - variant_family: "rt3-105-alt-save-header-v1".to_string(), - variant_evidence: vec![], - is_known_family: true, - }), - Some(&SmpSecondaryVariantProbe { - aligned_window_offset: 0, - words: vec![0x49f00100, 2, 0xa0000000, 0x186], - hex_words: vec![], - variant_family: "unknown".to_string(), - variant_evidence: vec![], - }), - ) - .expect("alt map profile"); - - assert_eq!( - scenario_profile.profile_family, - "rt3-105-scenario-map-container-v1" - ); - assert!(scenario_profile.is_known_profile); - assert_eq!(alt_profile.profile_family, "rt3-105-alt-map-container-v1"); - assert!(alt_profile.is_known_profile); - - let generic_map_profile = classify_container_profile( - Some("gmp"), - Some(&SmpHeaderVariantProbe { - variant_family: "rt3-map-header-family".to_string(), - variant_evidence: vec![], - is_known_family: true, - }), - Some(&SmpSecondaryVariantProbe { - aligned_window_offset: 0, - words: vec![0x00140000, 0x93e00100, 0x00000004, 0xa0000000], - hex_words: vec![], - variant_family: "rt3-map-secondary-family-v1".to_string(), - variant_evidence: vec![], - }), - ) - .expect("generic map profile"); - - assert_eq!( - generic_map_profile.profile_family, - "rt3-map-container-family" - ); - assert!(generic_map_profile.is_known_profile); - } - - fn empty_analysis_report() -> SmpSaveCompanyChairmanAnalysisReport { - SmpSaveCompanyChairmanAnalysisReport { - profile_family: "rt3-105-scenario-save-container-v1".to_string(), - selected_company_id: None, - selected_chairman_profile_id: None, - world_selection_context: None, - world_issue_37: None, - world_economic_tuning: None, - world_finance_neighborhood: None, - train_collection_header: None, - train_collection_directory: None, - region_collection_header: None, - region_record_triplets: None, - region_queued_notice_records: None, - region_fixed_row_run_candidates: None, - placed_structure_collection_header: None, - placed_structure_record_triplets: None, - placed_structure_dynamic_side_buffer: None, - placed_structure_dynamic_side_buffer_alignment: None, - unclassified_tagged_collection_headers: Vec::new(), - company_entries: Vec::new(), - chairman_entries: Vec::new(), - notes: Vec::new(), - } - } - - #[test] - fn compares_region_fixed_row_run_candidates_by_shape_signature() { - let mut left = empty_analysis_report(); - left.region_fixed_row_run_candidates = Some(SmpSaveRegionFixedRowRunCandidateProbe { - profile_family: left.profile_family.clone(), - source_kind: "save-region-fixed-row-run-candidates".to_string(), - semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), - target_row_count: 145, - target_row_stride: 0x29, - target_row_stride_hex: "0x29".to_string(), - scan_start_offset: 0, - scan_start_offset_hex: "0x0".to_string(), - scan_end_offset: 0x100, - scan_end_offset_hex: "0x100".to_string(), - candidates: vec![ - SmpSaveRegionFixedRowRunCandidate { - count_offset: 0x20, - count_offset_hex: "0x20".to_string(), - row_count: 145, - row_stride: 0x29, - row_stride_hex: "0x29".to_string(), - rows_offset: 0x24, - rows_offset_hex: "0x24".to_string(), - rows_end_offset: 0x39, - rows_end_offset_hex: "0x39".to_string(), - distance_to_region_metadata_tag: 0xc7, - distance_to_region_metadata_tag_hex: "0xc7".to_string(), - dword_lane_summaries: Vec::new(), - shape_signature: "pf32=[0x14:120]|small=[0x20:17]|zero=[0x20:11]|trail=28/63" - .to_string(), - shape_family_signature: - "dense_pf32=[0x14]|small_nonzero=[0x20]|partial_zero=[0x20]|trail_bucket=3/7" - .to_string(), - trailing_byte_zero_count: 28, - trailing_byte_nonzero_count: 117, - trailing_byte_distinct_value_count: 63, - trailing_byte_sample_values_hex: Vec::new(), - best_probable_density_lane_relative_offset_hex: Some("0x14".to_string()), - }, - SmpSaveRegionFixedRowRunCandidate { - count_offset: 0x40, - count_offset_hex: "0x40".to_string(), - row_count: 145, - row_stride: 0x29, - row_stride_hex: "0x29".to_string(), - rows_offset: 0x44, - rows_offset_hex: "0x44".to_string(), - rows_end_offset: 0x59, - rows_end_offset_hex: "0x59".to_string(), - distance_to_region_metadata_tag: 0xa7, - distance_to_region_metadata_tag_hex: "0xa7".to_string(), - dword_lane_summaries: Vec::new(), - shape_signature: "pf32=[0x18:107]|small=[0x10:17]|zero=[0x14:12]|trail=32/58" - .to_string(), - shape_family_signature: - "dense_pf32=[]|small_nonzero=[0x10]|partial_zero=[0x14]|trail_bucket=4/7" - .to_string(), - trailing_byte_zero_count: 32, - trailing_byte_nonzero_count: 113, - trailing_byte_distinct_value_count: 58, - trailing_byte_sample_values_hex: Vec::new(), - best_probable_density_lane_relative_offset_hex: Some("0x18".to_string()), - }, - ], - evidence: Vec::new(), - }); - - let mut right = empty_analysis_report(); - right.region_fixed_row_run_candidates = Some(SmpSaveRegionFixedRowRunCandidateProbe { - profile_family: right.profile_family.clone(), - source_kind: "save-region-fixed-row-run-candidates".to_string(), - semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), - target_row_count: 145, - target_row_stride: 0x29, - target_row_stride_hex: "0x29".to_string(), - scan_start_offset: 0, - scan_start_offset_hex: "0x0".to_string(), - scan_end_offset: 0x100, - scan_end_offset_hex: "0x100".to_string(), - candidates: vec![ - SmpSaveRegionFixedRowRunCandidate { - count_offset: 0x80, - count_offset_hex: "0x80".to_string(), - row_count: 145, - row_stride: 0x29, - row_stride_hex: "0x29".to_string(), - rows_offset: 0x84, - rows_offset_hex: "0x84".to_string(), - rows_end_offset: 0x99, - rows_end_offset_hex: "0x99".to_string(), - distance_to_region_metadata_tag: 0x67, - distance_to_region_metadata_tag_hex: "0x67".to_string(), - dword_lane_summaries: Vec::new(), - shape_signature: "pf32=[0x18:107]|small=[0x10:17]|zero=[0x14:12]|trail=32/58" - .to_string(), - shape_family_signature: - "dense_pf32=[]|small_nonzero=[0x10]|partial_zero=[0x14]|trail_bucket=4/7" - .to_string(), - trailing_byte_zero_count: 32, - trailing_byte_nonzero_count: 113, - trailing_byte_distinct_value_count: 58, - trailing_byte_sample_values_hex: Vec::new(), - best_probable_density_lane_relative_offset_hex: Some("0x18".to_string()), - }, - SmpSaveRegionFixedRowRunCandidate { - count_offset: 0xa0, - count_offset_hex: "0xa0".to_string(), - row_count: 145, - row_stride: 0x29, - row_stride_hex: "0x29".to_string(), - rows_offset: 0xa4, - rows_offset_hex: "0xa4".to_string(), - rows_end_offset: 0xb9, - rows_end_offset_hex: "0xb9".to_string(), - distance_to_region_metadata_tag: 0x47, - distance_to_region_metadata_tag_hex: "0x47".to_string(), - dword_lane_summaries: Vec::new(), - shape_signature: "pf32=[0x24:100]|small=[0xc:16]|zero=[0xc:11]|trail=34/60" - .to_string(), - shape_family_signature: - "dense_pf32=[]|small_nonzero=[0xc]|partial_zero=[0xc]|trail_bucket=4/7" - .to_string(), - trailing_byte_zero_count: 34, - trailing_byte_nonzero_count: 111, - trailing_byte_distinct_value_count: 60, - trailing_byte_sample_values_hex: Vec::new(), - best_probable_density_lane_relative_offset_hex: Some("0x24".to_string()), - }, - ], - evidence: Vec::new(), - }); - - let comparison = compare_save_region_fixed_row_run_candidates(&left, &right) - .expect("comparison should build"); - assert_eq!(comparison.shared_shape_matches.len(), 1); - assert_eq!( - comparison.shared_shape_matches[0].shape_signature, - "pf32=[0x18:107]|small=[0x10:17]|zero=[0x14:12]|trail=32/58" - ); - assert_eq!(comparison.shared_shape_matches[0].left_rank, 2); - assert_eq!(comparison.shared_shape_matches[0].right_rank, 1); - assert_eq!(comparison.shared_shape_family_matches.len(), 1); - assert_eq!( - comparison.shared_shape_family_matches[0].shape_signature, - "dense_pf32=[]|small_nonzero=[0x10]|partial_zero=[0x14]|trail_bucket=4/7" - ); - assert_eq!(comparison.left_only_shape_signatures.len(), 1); - assert_eq!(comparison.right_only_shape_signatures.len(), 1); - } - - #[test] - fn builds_region_service_trace_report_with_explicit_latch_blockers() { - let mut analysis = empty_analysis_report(); - analysis.region_record_triplets = Some(SmpSaveRegionRecordTripletProbe { - profile_family: analysis.profile_family.clone(), - source_kind: "save-region-record-triplets".to_string(), - semantic_family: "marker09".to_string(), - records_tag_offset: 0, - close_tag_offset: 0, - record_count: 1, - entries: vec![SmpSaveRegionRecordTripletEntryProbe { - record_index: 0, - name: "Marker09".to_string(), - record_payload_relative_offset: 0, - record_payload_relative_offset_hex: "0x0".to_string(), - name_tag_relative_offset: 0, - policy_tag_relative_offset: 0, - profile_tag_relative_offset: 0, - pre_name_prefix_len: 0, - pre_name_prefix_hex_bytes: Vec::new(), - pre_name_prefix_dword_candidates: Vec::new(), - policy_chunk_len: 0, - profile_chunk_len: 0, - policy_leading_f32_0: 368.0, - policy_leading_f32_1: 0.0, - policy_leading_f32_2: 92.0, - policy_reserved_dwords: Vec::new(), - policy_reserved_dword_candidates: Vec::new(), - policy_trailing_word: 0, - policy_trailing_word_hex: "0x0000".to_string(), - profile_collection: Some(SmpSaveRegionProfileCollectionProbe { - direct_collection_flag: 1, - entry_stride: 0x22, - live_id_bound: 17, - live_record_count: 17, - entry_start_relative_offset: 0, - trailing_padding_len: 0, - entries: Vec::new(), - }), - }], - evidence: Vec::new(), - }); - - let trace = build_region_service_trace_report(&analysis); - assert_eq!(trace.region_record_triplet_count, 1); - assert_eq!(trace.queued_notice_record_count, 0); - assert!(!trace.atlas_candidate_consumers.is_empty()); - assert_eq!(trace.known_owner_bridge_fields.len(), 6); - assert_eq!(trace.known_bridge_helpers.len(), 16); - assert_eq!(trace.next_owner_questions.len(), 5); - assert_eq!(trace.candidate_consumer_hypotheses.len(), 6); - assert_eq!( - trace.candidate_consumer_hypotheses[0].status, - "highest_priority_static_mapping_target" - ); - assert_eq!( - trace.candidate_consumer_hypotheses[2].status, - "secondary_candidate_after_pending_service" - ); - assert_eq!( - trace.candidate_consumer_hypotheses[3].status, - "next_global_restore_handoff_target" - ); - assert_eq!( - trace.candidate_consumer_hypotheses[1].status, - "parallel_static_mapping_target" - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x5209") - && line.contains("0x520a") - && line.contains("0x520b")) - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x0041f5c0") - && line.contains("[region+0x37f]") - && line.contains("[region+0x385]")) - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x00444887") - && line.contains("0x00487c20") - && line.contains("0x0040b5d0")) - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x00421ce0") && line.contains("0x0041fb00")) - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x00421730") - && line.contains("[region+0x242/+0x246/+0x24a/+0x24e/+0x252]")) - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x00448740..0x0044881f") - && line.contains("0x006cfc9c") - && line.contains("0x53b070") - && line.contains("0x00487bd0")) - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x00487de0") - && line.contains("0x00533cf0") - && line.contains("0x00536ea0")) - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x0044c4b0") - && line.contains("0x00455f60") - && line.contains("bit 0x10")) - ); - assert!( - trace.candidate_consumer_hypotheses[3] - .candidate_consumers - .iter() - .any(|line| line.contains("0x00444887")) - ); - assert!( - trace.candidate_consumer_hypotheses[3] - .candidate_consumers - .iter() - .any(|line| line.contains("0x00487c20")) - ); - assert!( - trace.candidate_consumer_hypotheses[3] - .candidate_consumers - .iter() - .any(|line| line.contains("0x0040b5d0")) - ); - assert!( - trace.candidate_consumer_hypotheses[3] - .evidence - .iter() - .any(|line| line.contains("0x00444887") && line.contains("0x00421510")) - ); - assert!( - trace.candidate_consumer_hypotheses[3] - .evidence - .iter() - .any(|line| line.contains("0x00444b90") && line.contains("0x00420560")) - ); - assert_eq!( - trace.candidate_consumer_hypotheses[4].status, - "next_post_load_owner_family" - ); - assert!( - trace.candidate_consumer_hypotheses[4] - .candidate_consumers - .iter() - .any(|line| line.contains("0x004384d0")) - ); - assert!( - trace.candidate_consumer_hypotheses[4] - .candidate_consumers - .iter() - .any(|line| line.contains("0x004133b0")) - ); - assert!( - trace.candidate_consumer_hypotheses[4] - .candidate_consumers - .iter() - .any(|line| line.contains("0x00421c20")) - ); - assert!( - trace.candidate_consumer_hypotheses[4] - .evidence - .iter() - .any(|line| line.contains("0x00446d40") && line.contains("0x004384d0")) - ); - assert!( - trace.candidate_consumer_hypotheses[4] - .evidence - .iter() - .any(|line| line.contains("0x004133b0") && line.contains("0x00480710")) - ); - assert!( - trace.candidate_consumer_hypotheses[4] - .evidence - .iter() - .any(|line| line.contains("0x00421c20") && line.contains("0x004235c0")) - ); - assert!( - trace.candidate_consumer_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x004881b0") - && line.contains("[region+0x3d]") - && line.contains("[region+0x41]")) - ); - assert_eq!(trace.entries.len(), 1); - assert_eq!( - trace.entries[0].branches[0].status, - "blocked_missing_pending_bonus_owner_lane" - ); - assert_eq!( - trace.entries[0].branches[1].status, - "blocked_missing_completion_and_one_shot_latches" - ); - assert!( - trace - .notes - .iter() - .any(|line| { line.contains("pre-name prefix lengths") && line.contains("[0]") }) - ); - } - - #[test] - fn builds_periodic_company_service_trace_report_with_candidate_consumers() { - let mut analysis = empty_analysis_report(); - analysis.selected_company_id = Some(7); - analysis.placed_structure_record_triplets = - Some(SmpSavePlacedStructureRecordTripletProbe { - profile_family: analysis.profile_family.clone(), - source_kind: "save-placed-structure-record-triplets".to_string(), - semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), - records_tag_offset: 0, - close_tag_offset: 0, - record_count: 2, - entries: vec![ - SmpSavePlacedStructureRecordTripletEntryProbe { - record_index: 0, - primary_name: "StationA".to_string(), - secondary_name: "StationSetA".to_string(), - name_tag_relative_offset: 0, - policy_tag_relative_offset: 0, - profile_tag_relative_offset: 0, - policy_chunk_len: 0x1a, - profile_chunk_len: 0x20, - policy_f32_lane_0: 0.0, - policy_f32_lane_1: 0.0, - policy_f32_lane_2: 0.0, - policy_f32_lane_3: 0.0, - policy_f32_lane_4: 0.0, - policy_reserved_dword: 0, - policy_trailing_word: 0x0101, - policy_trailing_word_hex: "0x0101".to_string(), - profile_open_marker: 0x5dc1, - profile_open_marker_hex: "0x00005dc1".to_string(), - profile_repeated_primary_name: "StationA".to_string(), - profile_repeated_secondary_name: "StationSetA".to_string(), - profile_footer_relative_offset: 0, - profile_footer_relative_offset_hex: "0x0".to_string(), - profile_pre_footer_padding_len: 1, - profile_pre_footer_padding_hex_bytes: vec!["0x01".to_string()], - profile_companion_byte_u8: Some(1), - profile_companion_byte_hex: Some("0x01".to_string()), - profile_payload_dword: 0x0e373880, - profile_payload_dword_hex: "0x0e373880".to_string(), - profile_sentinel_i32: -1, - profile_status_kind: "unset".to_string(), - farm_growth_stage_index: None, - profile_close_marker: 0x5dc2, - profile_close_marker_hex: "0x00005dc2".to_string(), - }, - SmpSavePlacedStructureRecordTripletEntryProbe { - record_index: 1, - primary_name: "StationB".to_string(), - secondary_name: "StationSetB".to_string(), - name_tag_relative_offset: 0, - policy_tag_relative_offset: 0, - profile_tag_relative_offset: 0, - policy_chunk_len: 0x1a, - profile_chunk_len: 0x20, - policy_f32_lane_0: 0.0, - policy_f32_lane_1: 0.0, - policy_f32_lane_2: 0.0, - policy_f32_lane_3: 0.0, - policy_f32_lane_4: 0.0, - policy_reserved_dword: 0, - policy_trailing_word: 0x0101, - policy_trailing_word_hex: "0x0101".to_string(), - profile_open_marker: 0x5dc1, - profile_open_marker_hex: "0x00005dc1".to_string(), - profile_repeated_primary_name: "StationB".to_string(), - profile_repeated_secondary_name: "StationSetB".to_string(), - profile_footer_relative_offset: 0, - profile_footer_relative_offset_hex: "0x0".to_string(), - profile_pre_footer_padding_len: 1, - profile_pre_footer_padding_hex_bytes: vec!["0x00".to_string()], - profile_companion_byte_u8: Some(0), - profile_companion_byte_hex: Some("0x00".to_string()), - profile_payload_dword: 0x0e373500, - profile_payload_dword_hex: "0x0e373500".to_string(), - profile_sentinel_i32: -1, - profile_status_kind: "unset".to_string(), - farm_growth_stage_index: None, - profile_close_marker: 0x5dc2, - profile_close_marker_hex: "0x00005dc2".to_string(), - }, - ], - evidence: Vec::new(), - }); - analysis.region_fixed_row_run_candidates = Some(SmpSaveRegionFixedRowRunCandidateProbe { - profile_family: analysis.profile_family.clone(), - source_kind: "save-region-fixed-row-run-candidates".to_string(), - semantic_family: "scenario-save-region-fixed-row-run-candidates".to_string(), - target_row_count: 4, - target_row_stride: 0xbc, - target_row_stride_hex: "0xbc".to_string(), - scan_start_offset: 0, - scan_start_offset_hex: "0x0".to_string(), - scan_end_offset: 0x2000, - scan_end_offset_hex: "0x2000".to_string(), - candidates: vec![SmpSaveRegionFixedRowRunCandidate { - count_offset: 0x40, - count_offset_hex: "0x40".to_string(), - row_count: 4, - row_stride: 0xbc, - row_stride_hex: "0xbc".to_string(), - rows_offset: 0x5310, - rows_offset_hex: "0x5310".to_string(), - rows_end_offset: 0x5600, - rows_end_offset_hex: "0x5600".to_string(), - distance_to_region_metadata_tag: 0x80, - distance_to_region_metadata_tag_hex: "0x80".to_string(), - dword_lane_summaries: Vec::new(), - shape_signature: "shape-a".to_string(), - shape_family_signature: "family-a".to_string(), - trailing_byte_zero_count: 4, - trailing_byte_nonzero_count: 0, - trailing_byte_distinct_value_count: 1, - trailing_byte_sample_values_hex: vec!["0x00".to_string()], - best_probable_density_lane_relative_offset_hex: Some("0x24".to_string()), - }], - evidence: Vec::new(), - }); - analysis - .company_entries - .push(SmpSaveCompanyRecordAnalysisEntry { - company_id: 7, - name: "Test Company".to_string(), - active: true, - linked_chairman_profile_id: Some(3), - outstanding_shares: 1000, - debt: 0, - bond_count: 0, - live_bond_slots: Vec::new(), - largest_live_bond_principal: None, - highest_coupon_live_bond_principal: None, - available_track_laying_capacity: Some(5), - company_value_scalar_f32: 1.0, - cached_share_support_scalar_f32: 1.0, - cached_share_price_f32: 1.0, - chairman_salary_baseline: 0, - chairman_salary_current: 0, - chairman_bonus_year: 0, - chairman_bonus_amount: 0, - founding_year: 1900, - last_bankruptcy_year: 0, - last_dividend_year: 0, - preferred_locomotive_engine_type_raw_u8: 2, - preferred_locomotive_engine_type_raw_hex: "0x02".to_string(), - city_connection_latch: true, - linked_transit_latch: false, - linked_transit_autoroute_site_score_cache_refresh_absolute_counter: 0x31380, - linked_transit_site_peer_cache_refresh_absolute_counter: 0x7ff80, - linked_transit_route_anchor_entry_id: Some(77), - linked_transit_route_anchor_fallback_counts: vec![3, 5, 8], - merger_cooldown_year: 0, - takeover_cooldown_year: 0, - scalar_dword_candidates: Vec::new(), - post_capacity_dword_candidates: Vec::new(), - stat_band_root_0cfb_candidates: Vec::new(), - stat_band_root_0d7f_candidates: Vec::new(), - stat_band_root_1c47_candidates: Vec::new(), - }); - - let trace = build_periodic_company_service_trace_report(&analysis); - assert_eq!(trace.selected_company_id, Some(7)); - assert_eq!(trace.atlas_candidate_consumers.len(), 9); - assert_eq!(trace.known_bridge_helpers.len(), 84); - assert_eq!(trace.next_owner_questions.len(), 5); - assert_eq!( - trace.linked_transit_shellless_readiness_status, - "timed_cache_and_train_side_followons_grounded_site_cache_input_owners_missing" - ); - assert_eq!( - trace.linked_transit_minimum_persisted_identity_inputs.len(), - 5 - ); - assert_eq!(trace.linked_transit_live_rebuilt_cache_lanes.len(), 5); - assert_eq!(trace.linked_transit_runtime_backed_input_families.len(), 18); - assert_eq!(trace.linked_transit_remaining_owner_gaps.len(), 2); - assert_eq!(trace.companies.len(), 1); - assert_eq!( - trace.companies[0].linked_transit_autoroute_site_score_cache_refresh_absolute_counter, - 0x31380 - ); - assert_eq!( - trace.companies[0].linked_transit_site_peer_cache_refresh_absolute_counter, - 0x7ff80 - ); - assert_eq!( - trace.peer_site_selector_candidate_owner_strip, - "0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70" - ); - assert_eq!( - trace.peer_site_selector_candidate_persisted_tag_hex, - "0x5dc1" - ); - assert_eq!( - trace.peer_site_selector_candidate_selector_lane, - "[owner+0x23e]" - ); - assert_eq!( - trace.peer_site_selector_candidate_secondary_payload_lane, - "[owner+0x242]" - ); - assert!( - trace - .peer_site_selector_candidate_post_secondary_byte_status - .contains("post-secondary discriminator byte") - ); - assert_eq!( - trace.peer_site_selector_candidate_class_identity_status, - "grounded_direct_local_helper_strip" - ); - assert_eq!(trace.peer_site_selector_candidate_helper_linkage.len(), 4); - assert_eq!( - trace - .peer_site_selector_candidate_saved_payload_summaries - .len(), - 2 - ); - assert_eq!( - trace.peer_site_selector_candidate_saved_payload_summaries[0].profile_payload_dword_hex, - "0x0e373500" - ); - assert_eq!( - trace.peer_site_selector_candidate_saved_payload_summaries[0].count, - 1 - ); - assert_eq!( - trace.peer_site_selector_candidate_saved_payload_delta_summaries[0].delta_hex, - "0x00000380" - ); - assert_eq!( - trace.peer_site_selector_candidate_saved_footer_padding_summaries[0].padding_len, - 1 - ); - assert_eq!( - trace.peer_site_selector_candidate_saved_companion_byte_summaries[0].companion_byte_hex, - "0x00" - ); - assert_eq!( - trace.peer_site_selector_candidate_saved_policy_trailing_word_summaries[0] - .policy_trailing_word_hex, - "0x0101" - ); - assert_eq!( - trace.peer_site_selector_candidate_saved_policy_trailing_word_summaries[0].count, - 2 - ); - assert_eq!( - trace.peer_site_selector_candidate_saved_nonzero_companion_name_pair_summaries[0] - .primary_name, - "StationA" - ); - assert!( - trace - .peer_site_selector_candidate_saved_nonzero_companion_building_family_overlap_summaries - .is_empty() - ); - assert_eq!(trace.peer_site_persisted_selector_bundle_fields.len(), 4); - assert_eq!(trace.peer_site_rebuilt_transient_followon_fields.len(), 4); - assert_eq!( - trace.peer_site_shellless_minimum_persisted_identity_status, - "name_pair_and_post_secondary_byte_minimum_identity_subset_child_runtime_bundle_rebuild_followon" - ); - assert_eq!( - trace - .peer_site_shellless_minimum_persisted_identity_inputs - .len(), - 5 - ); - assert_eq!(trace.peer_site_restore_input_fields.len(), 5); - assert_eq!(trace.peer_site_runtime_input_fields.len(), 3); - assert_eq!( - trace.peer_site_runtime_reconstruction_status, - "restore_subset_and_bring_up_reconstruct_runtime_subset" - ); - assert_eq!(trace.peer_site_runtime_reconstruction_steps.len(), 4); - assert_eq!(trace.near_city_acquisition_region_input_fields.len(), 5); - assert_eq!(trace.near_city_acquisition_peer_input_fields.len(), 7); - assert_eq!(trace.near_city_acquisition_company_input_fields.len(), 6); - assert_eq!( - trace.near_city_acquisition_shellless_readiness_status, - "peer_and_company_inputs_grounded_site_owner_and_tri_restore_gaps_remaining" - ); - assert_eq!( - trace.near_city_acquisition_site_owner_company_projection_status, - "ordinary_replay_ruled_down_stream_load_callback_grounded_tuple_finalize_path_grounded_nontransport_restore_source_missing" - ); - assert_eq!( - trace.near_city_acquisition_site_self_id_projection_status, - "live_meaning_grounded_reconstructible_from_collection_identity" - ); - assert_eq!( - trace.near_city_acquisition_site_cached_tri_lane_projection_status, - "live_writer_family_grounded_semantics_and_persisted_inputs_missing" - ); - assert_eq!( - trace.near_city_acquisition_nontransport_persisted_source_status, - "ordinary_runtime_effect_candidate_present_trigger_lane_mapping_missing" - ); - assert_eq!( - trace - .near_city_acquisition_nontransport_persisted_source_candidates - .len(), - 5 - ); - assert_eq!( - trace.near_city_acquisition_tri_lane_save_shape_family_status, - "save_shape_family_candidates_present_fixed_offset_ruled_down" - ); - assert_eq!( - trace - .near_city_acquisition_tri_lane_save_shape_family_candidates - .len(), - 1 - ); - assert_eq!( - trace.near_city_acquisition_tri_lane_save_shape_family_candidates[0] - .shape_family_signature, - "family-a" - ); - assert_eq!( - trace.near_city_acquisition_tri_lane_live_service_status, - "candidate_gate_and_live_writer_family_grounded_exact_formula_and_persisted_inputs_missing" - ); - assert_eq!( - trace.near_city_acquisition_candidate_subtype_projection_status, - "cached_candidate_id_bridge_grounded_via_stream_load" - ); - assert_eq!( - trace.near_city_acquisition_backing_record_projection_status, - "stream_load_callback_grounded_via_0x40ce60" - ); - assert_eq!( - trace - .near_city_acquisition_tri_lane_live_owner_families - .len(), - 5 - ); - assert_eq!( - trace - .near_city_acquisition_tri_lane_candidate_gate_fields - .len(), - 5 - ); - assert_eq!( - trace - .near_city_acquisition_tri_lane_runtime_writer_roles - .len(), - 5 - ); - assert_eq!( - trace - .near_city_acquisition_tri_lane_direct_caller_families - .len(), - 5 - ); - assert_eq!( - trace - .near_city_acquisition_tri_lane_formula_input_lanes - .len(), - 5 - ); - assert_eq!(trace.near_city_acquisition_projection_hypotheses.len(), 3); - assert_eq!( - trace.near_city_acquisition_projection_hypotheses[0].label, - "site_owner_replay_from_post_load_refresh_self_id_reconstructible" - ); - assert_eq!( - trace.near_city_acquisition_projection_hypotheses[0].status, - "ordinary_replay_and_stream_load_ruled_down_tuple_finalize_positive_path_grounded" - ); - assert_eq!( - trace.near_city_acquisition_projection_hypotheses[1].label, - "site_cached_tri_lane_payload_or_restore_owner" - ); - assert_eq!( - trace.near_city_acquisition_projection_hypotheses[1].status, - "checked_in_save_seams_ruled_down_live_scoring_family_grounded_exact_semantics_open" - ); - assert_eq!( - trace.near_city_acquisition_projection_hypotheses[2].label, - "cached_source_candidate_id_to_subtype_projection" - ); - assert_eq!( - trace.near_city_acquisition_projection_hypotheses[2].status, - "grounded_stream_load_callback_0x40ce60" - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .candidate_consumers - .iter() - .any(|line| line.contains("0x004133b0")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .candidate_consumers - .iter() - .any(|line| line.contains("0x004134d0") && line.contains("0x0040f6d0")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .candidate_consumers - .iter() - .any(|line| line.contains("0x00403ef3 / 0x00404489")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .candidate_consumers - .iter() - .any(|line| line.contains("0x0046f073 / 0x004707ff")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00444690 -> 0x004133b0") - && line.contains("ordinary bring-up replay family")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00444467") - && line.contains("0x00413280") - && line.contains("0x004444d8") - && line.contains("0x00481210") - && line.contains("0x00444690")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("after 0x00444690 -> 0x004133b0") - && line.contains("0x004134d0 / 0x0040f6d0 / 0x0040ef10")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("[site+0x27a]") - && line.contains("0x0042125d") - && line.contains("0x0040f793") - && line.contains("0x0040dfec") - && line.contains("0x004269e4") - && line.contains("0x00426a44..0x00426a90") - && line.contains("0x00426ad8")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x0040ee10") - && line.contains("[site+0x3cc]") - && line.contains("0x0040e360..0x0040edf6") - && line.contains("[site+0x2a8]") - && line.contains("[site+0x2a4]") - && line.contains("[site+0x276]") - && line.contains("0x00480710") - && line.contains("0x00426b10") - && line.contains("0x00455860") - && line.contains("reads/queries")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x004134d0") - && line.contains("0x00518900") - && line.contains("0x0040f6d0") - && line.contains("[site+0x2a4]") - && line.contains("[site+0x276]")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x0040f6d0") - && line.contains("[site+0x2a8/+0x272/+0x27a/+0x29e]") - && line.contains("[site+0x3d4/+0x3d5]")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00403ef3 / 0x00404489") - && line.contains("0x0046f073 / 0x004707ff") - && line.contains("0x0040ef10")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("[+0x00/+0x04/+0x0c]") - && line.contains("0x0040ef1c") - && line.contains("0x0040f5d4") - && line.contains("[site+0x276]") - && line.contains("[site+0x27a]")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x004707ff") - && line.contains("0x004706b0") - && line.contains("selector-0x13") - && line.contains( - "0x004197e0 / 0x004134d0 / 0x0040eba0 / 0x0052eb90 / 0x0040ef10" - )) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00472b40") - && line.contains("selector-0x72") - && line.contains("0x00472bef / 0x00472d03")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00422bb4") - && line.contains("0x0062b2fc") - && line.contains("literal flags 1/0") - && line.contains("out-param")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00508fd1 / 0x005098eb") - && line.contains("[this+0x7c]") - && line.contains("vtable slot +0x58 plus 0x00507cf0") - && line.contains("arg3 forced to zero")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00473c20") - && line.contains("0x006ce808..0x006ce988") - && line.contains("0x00473c98") - && line.contains("queued id slot")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x0042128d") - && line.contains("0x00422305") - && line.contains("0x004269c9/0x00426a2a") - && line.contains("0x004282a9 / 0x004300d6")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00413440") - && line.contains("0x36b1 / 0x36b2 / 0x36b3") - && line.contains("save side") - && line.contains("slot +0x44")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00413280") - && line.contains("slot +0x40") - && line.contains("0x0040ce60")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x0040f047") - && line.contains("0x0040f5d4") - && line.contains("[site+0x27a]")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .candidate_consumers - .iter() - .any(|line| line.contains("0x36b1/0x36b2/0x36b3")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .candidate_consumers - .iter() - .any(|line| line.contains("0x0040d450")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .candidate_consumers - .iter() - .any(|line| line.contains("0x00410b30..0x004118f4") && line.contains("0x00412560")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x00481430 -> 0x0047d8e0")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x0040c9a0")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x0040a3a1..0x0040a4d3") - && line.contains("0x0040fcc0..0x0040fe28") - && line.contains("0x00422c62..0x00422d3c")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x0040d4aa/0x0040d4b0") - && line.contains("0x0041114a7/0x004111572") - && line.contains("0x0041118aa/0x0041118f4")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x0040d450") - && line.contains("[site+0x276]") - && line.contains("0x00436590") - && line.contains("[site+0x310]")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x00410b30..0x004118f4") - && line.contains("0xbc-stride") - && line.contains("[site+0x310/+0x338/+0x360]")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[1] - .evidence - .iter() - .any(|line| line.contains("0x00412560") - && line.contains("0x006cec78") - && line.contains("0x0062ba8c")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[2] - .evidence - .iter() - .any(|line| line.contains("0x00413280") && line.contains("0x0040ce60")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[2] - .candidate_consumers - .iter() - .any(|line| line.contains("0x0040cd70 cached source/candidate resolver")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .blockers - .iter() - .any(|line| line.contains("0x00413440") - && line.contains("0x36b1/0x36b2/0x36b3") - && line.contains("load-save strip")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .blockers - .iter() - .any(|line| line.contains("0x00413280") - && line.contains("0x0040ce60") - && line.contains("stream-load bridge")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .blockers - .iter() - .any(|line| line.contains("0x00481210") - && line.contains("0x004133b0") - && line.contains("0x0040ef10")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .blockers - .iter() - .any(|line| line.contains("0x00431b20") - && line.contains("0x00433130") - && line.contains("0x0042db20") - && line.contains("0x0042e050") - && line.contains("0x0062be18") - && line.contains("[event+0x7ef]") - && line.contains("kind 8")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00433130") - && line.contains("0x0062be18") - && line.contains("0x4e21/0x4e22") - && line.contains("0x004d90ba..0x004d91ed") - && line.contains("0x4e98..0x4ea2") - && line.contains("0x004d91b3")) - ); - assert!( - trace.near_city_acquisition_projection_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x004db02a") - && line.contains("0x004db1b8..0x004db309") - && line.contains("0x4e84") - && line.contains("0x004db9e5..0x004db9f1") - && line.contains("0x00432ea0")) - ); - assert_eq!( - trace - .near_city_acquisition_runtime_backed_input_families - .len(), - 25 - ); - assert_eq!(trace.near_city_acquisition_remaining_owner_gaps.len(), 2); - assert_eq!(trace.near_city_acquisition_region_lane_statuses.len(), 4); - assert!(trace.atlas_candidate_consumers.iter().any(|line| { - line.contains("0x00420030 / 0x00420280") - && line.contains("0x006cec20") - && line.contains("peer-site boolean/selector pair") - })); - assert!(trace.atlas_candidate_consumers.iter().any(|line| { - line.contains("0x0040dc40") - && line.contains("linked-site mutation validator/apply owner") - })); - assert!(trace.atlas_candidate_consumers.iter().any(|line| { - line.contains("0x00402cb0") && line.contains("direct-placement builder owner") - })); - assert!(trace.next_owner_questions.iter().any(|line| { - line.contains("repopulates placed-structure owner-company field [site+0x276]") - && line.contains("0x00431b20") - })); - assert!(trace.next_owner_questions.iter().any(|line| { - line.contains("0x0040d450 / 0x00410b30..0x004118f4") - && line.contains("0x00412560") - && line.contains("[site+0x2b4/+0x2b8/+0x2bc]") - })); - assert!( - trace - .linked_transit_minimum_persisted_identity_inputs - .iter() - .any(|line| line.contains("[site+0x276]") - && line.contains("[site+0x04]") - && line.contains("0x0047efe0 / 0x0047fd50")) - ); - assert!( - trace - .linked_transit_minimum_persisted_identity_inputs - .iter() - .any(|line| line.contains("[site+0x2a4]") - && line.contains("[site+0x2a8]") - && line.contains("[peer+0x04/+0x08]")) - ); - assert!( - trace - .linked_transit_live_rebuilt_cache_lanes - .iter() - .any(|line| line.contains("0x004093d0") - && line.contains("[company+0x0d3e]") - && line.contains("+0x02/+0x06/+0x0a")) - ); - assert!( - trace - .linked_transit_live_rebuilt_cache_lanes - .iter() - .any(|line| line.contains("0x00407bd0") - && line.contains("[site+0x0e/+0x12/+0x16]")) - ); - assert!( - trace - .linked_transit_live_rebuilt_cache_lanes - .iter() - .any(|line| line.contains("0x00481910") - && line.contains("0x004819b0") - && line.contains("0x004a9340")) - ); - assert!( - trace - .linked_transit_live_rebuilt_cache_lanes - .iter() - .any(|line| line.contains("0x004aee2b") - && line.contains("[site+0x5c5]") - && line.contains("[world+0x15]")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00429c10") - && line.contains("0x004093d0") - && line.contains("live company roster")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00444690") - && line.contains("0x004133b0") - && line.contains("0x0040ee10") - && line.contains("0x00480710") - && line.contains("0x004160aa")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("[company+0x0d3e]") && line.contains("0x00409720")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("[company+0x0d3a]") && line.contains("0x00407bd0")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0062ba8c") - && line.contains("0x0041f4e0") - && line.contains("0x0041ede0") - && line.contains("0x0041e970")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x004a6360") - && line.contains("0x004a6630") - && line.contains("0x00494fb0")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00408280") && line.contains("0x00408380")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00409770") && line.contains("0x00409830")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("[site+0x5bd]") - && line.contains("0x00407780") - && line.contains("0x004077e0")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("+0x00/+0x01") - && line.contains("+0x02") - && line.contains("+0x06") - && line.contains("+0x0a") - && line.contains("+0x0e/+0x12/+0x16")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("[site+0x5c1]") - && line.contains("0x00481910") - && line.contains("0x004a9340") - && line.contains("0x004819b0")) - ); - assert!( - trace - .linked_transit_runtime_backed_input_families - .iter() - .any(|line| line.contains("[site+0x5c5]") && line.contains("0x004aee2b")) - ); - assert!( - trace - .linked_transit_remaining_owner_gaps - .iter() - .any(|line| line.contains("0x0040e360..0x0040edf6") - && line.contains("[site+0x276]") - && line.contains("earlier restore or service owner")) - ); - assert!( - trace - .linked_transit_remaining_owner_gaps - .iter() - .any(|line| line.contains("0x006cec20") - && line.contains("0x0041f4e0") - && line.contains("0x00494fb0")) - ); - assert!( - trace - .near_city_acquisition_region_input_fields - .iter() - .any(|line| line.contains("[site+0x276]")) - ); - assert!( - trace - .near_city_acquisition_peer_input_fields - .iter() - .any(|line| line.contains("[site+0x04]")) - ); - assert!( - trace - .near_city_acquisition_peer_input_fields - .iter() - .any(|line| line.contains("[cell+0xd4]") && line.contains("[cell+0xd6]")) - ); - assert!( - trace - .near_city_acquisition_company_input_fields - .iter() - .any(|line| line.contains("0x2329/0x0d")) - ); - assert!( - trace - .near_city_acquisition_company_input_fields - .iter() - .any(|line| line.contains("[company+0x0d35]") && line.contains("0x00401860")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("[company+0x0d35]") - && line.contains("[company+0x7664/+0x7668/+0x766c]")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x004134d0") - && line.contains("0x0040f6d0") - && line.contains("[site+0x2a4]") - && line.contains("[site+0x276]") - && line.contains("[site+0x3d4/+0x3d5]")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0040ef10") - && line.contains("0x00403ef3 / 0x00404489") - && line.contains("0x0046f073 / 0x004707ff")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0046f073 / 0x004707ff") - && line.contains("[+0x00/+0x04/+0x0c]") - && line.contains("0x0040ef10") - && line.contains("0x0040f5d4")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x004707ff") - && line.contains("0x004706b0") - && line.contains("selector-0x13")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00472b40") - && line.contains("selector-0x72") - && line.contains("0x00472bef / 0x00472d03")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00422bb4") - && line.contains("live args") - && line.contains("literal flags 1/0") - && line.contains("out-param")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00508fd1 / 0x005098eb") - && line.contains("[this+0x7c]") - && line.contains("0x0040eba0") - && line.contains("hard zero third arg")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00473c20") - && line.contains("0x006ce808..0x006ce988") - && line.contains("0x00473c98") - && line.contains("post-create refresh path")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0042128d") - && line.contains("0x00422305") - && line.contains("0x004269c9/0x00426a2a") - && line.contains("0x004282a9/0x004300d6")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00413440") - && line.contains("0x36b1/0x36b2/0x36b3") - && line.contains("slot +0x44")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0047efe0") && line.contains("[site+0x276]")) - ); - let linked_transit_branch = trace.companies[0] - .branches - .iter() - .find(|branch| branch.branch_name == "linked_transit_roster_maintenance") - .expect("linked-transit branch"); - assert_eq!( - linked_transit_branch.status, - "blocked_missing_site_cache_input_owner_mapping" - ); - assert!( - linked_transit_branch - .grounded_inputs - .iter() - .any(|line| line.contains("[company+0x0d3e]")) - ); - assert!( - linked_transit_branch - .grounded_inputs - .iter() - .any(|line| line.contains("[company+0x0d3a]")) - ); - assert!( - linked_transit_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x00409720")) - ); - assert!( - linked_transit_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x00408f70")) - ); - assert!( - linked_transit_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x00408280")) - ); - assert!( - linked_transit_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x00408380")) - ); - assert!( - linked_transit_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x00409770")) - ); - assert!( - linked_transit_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x00409830")) - ); - assert!( - linked_transit_branch - .blocking_inputs - .iter() - .any(|line| line.contains("0x00408280 / 0x00408380")) - ); - assert!(trace.notes.iter().any(|line| { - line.contains("0x00409720") - && line.contains("[company+0x0d3e]") - && line.contains("[company+0x0d3a]") - && line.contains("0x00409950") - })); - assert!(trace.notes.iter().any(|line| { - line.contains("0x00408280") - && line.contains("[site+0x16]") - && line.contains("0x00409770") - && line.contains("0x00409830") - })); - assert!(trace.notes.iter().any(|line| { - line.contains("0x00481910") - && line.contains("0x004819b0") - && line.contains("[site+0x5c1]") - && line.contains("0x004a9340") - && line.contains("0x004aee2b") - && line.contains("[site+0x5c5]") - })); - assert!(trace.notes.iter().any(|line| { - line.contains("0x00407780") - && line.contains("0x004077e0") - && line.contains("[site+0x5bd]") - && line.contains("0x1a-byte") - })); - assert!(trace.notes.iter().any(|line| { - line.contains("0x004093d0") - && line.contains("0x00407bd0") - && line.contains("+0x02") - && line.contains("+0x06") - && line.contains("+0x0a") - && line.contains("+0x0e/+0x12/+0x16") - })); - assert!(trace.notes.iter().any(|line| { - line.contains("0x0062ba8c") - && line.contains("0x0041f4e0") - && line.contains("0x00494fb0") - && line.contains("remaining linked-transit gap is narrower again") - })); - assert!(trace.notes.iter().any(|line| { - line.contains("0x00409720") - && line.contains("0x004093d0") - && line.contains("0x00407bd0") - && line.contains("0x00409950") - })); - assert!(trace.notes.iter().any(|line| { - line.contains("0x0040eba0") - && line.contains("[site+0x2a4]") - && line.contains("0x004814c0 / 0x00481480") - && line.contains("0x0042c9f0 / 0x0042c9a0") - })); - assert!(trace.notes.iter().any(|line| { - line.contains("0x0040ea96..0x0040eb65") - && line.contains("[site+0x276]") - && line.contains("consumes") - && line.contains("rather than rehydrating") - })); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0041f7e0 / 0x0041f810 / 0x0041f850") - && line.contains("[site+0x2a4]")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x004269b0") - && line.contains("0x0062b26c") - && line.contains("[site+0x2a4]")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any( - |line| line.contains("0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0") - && line.contains("[candidate+0x32]") - ) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0040dc40") - && line.contains("[site+0x276]") - && line.contains("0x0040d1f0 / 0x00480710")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line - .contains("0x00402cb0 -> 0x00403ed5/0x0040446b -> 0x004134d0 -> 0x0040ef10")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x00431b20") - && line.contains("0x0061039c") - && line.contains("0x00430040 / 0x00426d60 / 0x0042fc90")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0040d450") - && line.contains("[site+0x310]") - && line.contains("0x00410b30..0x004118f4") - && line.contains("0x00412560")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x0042bbf0") - && line.contains("0x0042bbb0") - && line.contains("0x0042c9f0") - && line.contains("0x0042c9a0")) - ); - assert!( - trace - .near_city_acquisition_runtime_backed_input_families - .iter() - .any(|line| line.contains("0x2329/0x0d")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_live_owner_families - .iter() - .any(|line| line.contains("0x0040d450") && line.contains("[site+0x310]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_live_owner_families - .iter() - .any(|line| line.contains("0x00410b30..0x004118f4") - && line.contains("[site+0x310/+0x338/+0x360]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_live_owner_families - .iter() - .any(|line| line.contains("0x00412560")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_candidate_gate_fields - .iter() - .any(|line| line.contains("[+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_candidate_gate_fields - .iter() - .any(|line| line.contains("0x006cec78")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_candidate_gate_fields - .iter() - .any(|line| line.contains("0x0062ba8c")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_candidate_gate_fields - .iter() - .any(|line| line.contains("vtable slot +0x80") && line.contains("[site+0x246]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_candidate_gate_fields - .iter() - .any(|line| line.contains("0x0040fb8d") - && line.contains("0x00410721") - && line.contains("0x004126d3")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_runtime_writer_roles - .iter() - .any(|line| line.contains("0x0040d450") && line.contains("[site+0x310]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_runtime_writer_roles - .iter() - .any(|line| line.contains("0x00410b30..0x004118f4") - && line.contains("[site+0x310/+0x338/+0x360]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_runtime_writer_roles - .iter() - .any(|line| line.contains("0x0041114a7/0x004111572") - && line.contains("0x0041114b7/0x004111582")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_runtime_writer_roles - .iter() - .any(|line| line.contains("0x0041118aa/0x0041118f4")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_runtime_writer_roles - .iter() - .any(|line| line.contains("0x0040c9a0") - && line.contains("[site+0x2b4/+0x2b8/+0x2bc]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_direct_caller_families - .iter() - .any(|line| line.contains("0x0040fb70") - && line.contains("vtable slot +0x80") - && line.contains("owner-present flag") - && line.contains("0x00412560")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_direct_caller_families - .iter() - .any(|line| line.contains("0x004b4052 / 0x004b46ec") - && line.contains("0x0062b26c")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_direct_caller_families - .iter() - .any(|line| line.contains("0x00401633") && line.contains("0x2329/0x0d")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_direct_caller_families - .iter() - .any(|line| line.contains("0x0044b81a") - && line.contains("0x00436590") - && line.contains("0x65")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_direct_caller_families - .iter() - .any(|line| line.contains("0x004b70f5 / 0x004b7979") - && line.contains("0x004337a0") - && line.contains("0x00540120 / 0x00518140")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_formula_input_lanes - .iter() - .any(|line| line.contains("[+0x20/+0x22]") - && line.contains("[+0x24/+0x28]") - && line.contains("[+0x44]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_formula_input_lanes - .iter() - .any(|line| line.contains("[world+0x0d]") && line.contains("[world+0x4afb]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_formula_input_lanes - .iter() - .any(|line| line.contains("0x00455810 / 0x00455800 / 0x0044ad60") - && line.contains("[site+0x276]") - && line.contains("0x66/0x68")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_formula_input_lanes - .iter() - .any(|line| line.contains("[+0x18/+0x1c/+0x2a/+0x2c/+0x44]") - && line.contains("[site+0x78c]") - && line.contains("[site+0x391]")) - ); - assert!( - trace - .near_city_acquisition_tri_lane_formula_input_lanes - .iter() - .any(|line| line.contains("[world+0x4caa]") - && line.contains("[company+0x0d5d]") - && line.contains("[site+0x310]") - && line.contains("[site+0x360]")) - ); - assert!( - trace - .near_city_acquisition_remaining_owner_gaps - .iter() - .any(|line| line.contains("[site+0x276]") - && line.contains("0x36b1/0x36b2/0x36b3") - && line.contains("0x0047efe0") - && line.contains("0x004134d0 / 0x0040f6d0") - && line.contains("0x0040ef10") - && line.contains("[+0x0c]") - && line.contains("non-transport")) - ); - assert!( - !trace - .near_city_acquisition_remaining_owner_gaps - .iter() - .any(|line| line.contains("[site+0x2a4]")) - ); - assert!( - trace - .near_city_acquisition_remaining_owner_gaps - .iter() - .any(|line| line.contains("0x0040d450") - && line.contains("0x00410b30..0x004118f4") - && line.contains("0x00412560")) - ); - assert!( - trace - .near_city_acquisition_region_lane_statuses - .iter() - .any(|line| line.contains("[site+0x276]") - && line.contains("0x004014b0") - && line.contains("0x36b1/0x36b2/0x36b3") - && line.contains("0x0047efe0") - && line.contains("0x004134d0 / 0x0040f6d0") - && line.contains("0x0040ef10")) - ); - assert!( - trace - .near_city_acquisition_region_lane_statuses - .iter() - .any(|line| line.contains("[site+0x2a4]") - && line.contains("record's own site id") - && line.contains("0x00480210") - && line.contains("0x004269b0") - && line.contains("reconstructible from collection identity")) - ); - assert!( - trace - .near_city_acquisition_region_lane_statuses - .iter() - .any(|line| line.contains("[site+0x310/+0x338/+0x360]") - && line.contains("0x0040cac0") - && line.contains("0x0040c9a0") - && line.contains("0x0040d450") - && line.contains("0x00410b30..0x004118f4") - && line.contains("0x00412560")) - ); - assert!( - trace - .near_city_acquisition_region_lane_statuses - .iter() - .any( - |line| line.contains("0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0") - && line.contains("0x0040ce60") - && line.contains("[site+0x3cc/+0x3d0]") - ) - ); - assert!( - trace - .peer_site_runtime_reconstruction_steps - .iter() - .any(|line| { - line.contains("[cell+0xd4]") - && line.contains("[cell+0xd6]") - && line.contains("0x0042bbf0/0x0042bbb0") - }) - ); - assert!(trace.next_owner_questions.iter().any(|line| { - line.contains("0x004160aa") - && line.contains("0x0040ee10") - && line.contains("0x0040edf6") - })); - let acquisition_branch = trace.companies[0] - .branches - .iter() - .find(|branch| branch.branch_name == "industry_acquisition_side_branch") - .expect("missing acquisition branch"); - assert_eq!( - acquisition_branch.status, - "blocked_missing_near-city_owner_mapping" - ); - assert!( - acquisition_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x004014b0")) - ); - assert!( - acquisition_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x004019e0")) - ); - assert!( - acquisition_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x004010f0")) - ); - assert!( - acquisition_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x00420030 / 0x00420280")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00405920")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0041f6e0") && line.contains("0x0042b2d0")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00480710") && line.contains("route-entry-anchor")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0042bbf0") - && line.contains("0x0042bbb0") - && line.contains("[cell+0xd4]")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0042c9f0") - && line.contains("0x0042c9a0") - && line.contains("[cell+0xd6]")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x004269b0") - && line.contains("0x0062b26c") - && line.contains("[site+0x276]/[site+0x27a]")) - ); - assert!(trace.known_bridge_helpers.iter().any( - |line| line.contains("0x00426dce..0x00426ea1") - && line.contains("0x0062b26c") - && line.contains("non-subtype-4") - )); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00430040..0x004300d6") - && line.contains("0x09/0x0b/0x0c")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00431b20") - && line.contains("0x0061039c") - && line.contains("0x00430040") - && line.contains("0x00426d60") - && line.contains("0x0042fc90")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0042fc90") - && line.contains("0x0062b26c") - && line.contains("[site+0x276]") - && line.contains("vtable slot +0x70")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x004134d0") && line.contains("0x0040f6d0")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00403ef3 / 0x00404489")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0046f073 / 0x004707ff")) - ); - assert!(trace.known_bridge_helpers.iter().any(|line| { - line.contains("0x0046f073 / 0x004707ff") - && line.contains("[+0x0c]") - && line.contains("0x0040f5d4") - })); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x004706b0") - && line.contains("selector-0x13") - && line.contains("0x0040ef10")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00472b40") - && line.contains("selector-0x72") - && line.contains("0x00472bef / 0x00472d03")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00422bb4") - && line.contains("0x0062b2fc") - && line.contains("literal flags 1/0") - && line.contains("out-param")) - ); - assert!(trace.known_bridge_helpers.iter().any(|line| { - line.contains("0x00508fd1 / 0x005098eb") - && line.contains("[this+0x7c]") - && line.contains("0x0040eba0") - && line.contains("arg3 forced to zero") - })); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00473c20") - && line.contains("0x006ce808..0x006ce988") - && line.contains("0x00473c98")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0042128d") - && line.contains("0x00421430") - && line.contains("[site+0x276]")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00422305") - && line.contains("event 0x7") - && line.contains("[site+0x276]")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x004269c9 / 0x00426a2a") - && line.contains("[site+0x276]/[site+0x27a]")) - ); - assert!(trace.known_bridge_helpers.iter().any(|line| { - line.contains("0x004282a9 / 0x004300d6") - && line.contains("owner-transfer") - && line.contains("placed-structure") - })); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00413440") - && line.contains("0x36b1/0x36b2/0x36b3") - && line.contains("slot +0x44")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0040f6d0") - && line.contains("0x00481390") - && line.contains("0x00480210")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0040df27 / 0x0040e00a / 0x0040edf6")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00444690") && line.contains("0x004133b0")) - ); - assert!(trace.known_bridge_helpers.iter().any( - |line| line.contains("0x0040e360..0x0040edf6") - && line.contains("[site+0x2a8/+0x2a4/+0x276]") - && line.contains("0x00426b10") - && line.contains("0x00455860") - )); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0040e450") && line.contains("queued site-id")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x004160aa") && line.contains("0x0040ee10")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0040dc40") - && line.contains("[site+0x276]") - && line.contains("0x2329/0x0d")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00417840 / 0x004197e0 / 0x004142c0 / 0x004142d0")) - ); - assert!(trace.known_bridge_helpers.iter().any(|line| line.contains( - "0x0040d1f0 / 0x00480710 / 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70" - ))); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00402cb0 / 0x00403ed5 / 0x0040446b")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x004134d0 / 0x0040ef10")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00481430 / 0x0047d8e0")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0040c9a0") - && line.contains("[site+0x310/+0x338/+0x360]") - && line.contains("[site+0x2b4/+0x2b8/+0x2bc]")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0040d450") - && line.contains("[site+0x310]") - && line.contains("0x00436590")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0040fb70") - && line.contains("vtable slot +0x80") - && line.contains("0x00412560")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00412560") - && line.contains("0x0062ba8c") - && line.contains("world date/flags")) - ); - assert!(trace.known_bridge_helpers.iter().any( - |line| line.contains("0x00410b30..0x004118f4") - && line.contains("[site+0x310/+0x338/+0x360]") - && line.contains("0x00412560") - )); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00401633") && line.contains("0x2329/0x0d")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0044b81a") - && line.contains("0x00436590") - && line.contains("0x65")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x004b4052 / 0x004b46ec") - && line.contains("0x0062b26c")) - ); - assert!(trace.known_bridge_helpers.iter().any(|line| { - line.contains("0x004b70f5 / 0x004b7979") - && line.contains("0x004337a0") - && line.contains("0x00540120 / 0x00518140") - })); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0040a3a1..0x0040a4d3") && line.contains("0x0040c9a0")) - ); - assert!(trace.known_bridge_helpers.iter().any( - |line| line.contains("0x0040fcc0..0x0040fe28") - && line.contains("0x00422c62..0x00422d3c") - && line.contains("0x0040cac0") - )); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x005c8c50 +0x40") - && line.contains("0x0040ce60") - && line.contains("0x0040cd70 / 0x0045c150")) - ); - let city_branch = trace.companies[0] - .branches - .iter() - .find(|branch| branch.branch_name == "city_connection_announcement") - .expect("missing city branch"); - assert!( - city_branch - .candidate_consumers - .iter() - .any(|line| line.contains("0x00406050")) - ); - } - - #[test] - fn summarizes_nonzero_companion_building_family_overlaps() { - let overlaps = - summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_overlaps( - &[ - SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { - companion_byte_hex: "0x01".to_string(), - primary_name: "TextileMill".to_string(), - secondary_name: "TextileMill".to_string(), - count: 9, - }, - SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { - companion_byte_hex: "0x01".to_string(), - primary_name: "Toolndie".to_string(), - secondary_name: "Toolndie".to_string(), - count: 2, - }, - SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { - companion_byte_hex: "0x01".to_string(), - primary_name: "MunitionsFactory".to_string(), - secondary_name: "MunitionsFactory".to_string(), - count: 1, - }, - ], - ); - - assert_eq!(overlaps.len(), 2); - assert_eq!(overlaps[0].primary_name, "TextileMill"); - assert!(overlaps[0].primary_matches_nonzero_stock_building_header_family); - assert!(overlaps[0].secondary_matches_nonzero_stock_building_header_family); - assert_eq!(overlaps[1].primary_name, "Toolndie"); - assert!(overlaps[1].primary_matches_nonzero_stock_building_header_family); - assert!(overlaps[1].secondary_matches_nonzero_stock_building_header_family); - } - - #[test] - fn summarizes_nonzero_companion_building_family_residues() { - let residues = - summarize_peer_site_selector_candidate_saved_nonzero_companion_building_family_residues( - &[ - SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { - companion_byte_hex: "0x01".to_string(), - primary_name: "TextileMill".to_string(), - secondary_name: "TextileMill".to_string(), - count: 9, - }, - SmpSavePlacedStructureNonzeroCompanionNamePairSummaryEntry { - companion_byte_hex: "0x01".to_string(), - primary_name: "MunitionsFactory".to_string(), - secondary_name: "MunitionsFactory".to_string(), - count: 1, - }, - ], - ); - - assert_eq!(residues.len(), 1); - assert_eq!(residues[0].primary_name, "MunitionsFactory"); - assert_eq!(residues[0].secondary_name, "MunitionsFactory"); - assert_eq!(residues[0].companion_byte_hex, "0x01"); - } - - #[test] - fn builds_infrastructure_asset_trace_report_with_alias_disproved_status() { - let mut analysis = empty_analysis_report(); - analysis.placed_structure_record_triplets = - Some(SmpSavePlacedStructureRecordTripletProbe { - profile_family: analysis.profile_family.clone(), - source_kind: "save-placed-structure-triplets".to_string(), - semantic_family: "placed-structure-triplets".to_string(), - records_tag_offset: 0, - close_tag_offset: 0, - record_count: 2057, - entries: Vec::new(), - evidence: Vec::new(), - }); - analysis.placed_structure_dynamic_side_buffer = - Some(SmpSavePlacedStructureDynamicSideBufferProbe { - profile_family: analysis.profile_family.clone(), - source_kind: "save-side-buffer".to_string(), - semantic_family: "infrastructure-asset".to_string(), - metadata_tag_offset: 0, - records_tag_offset: 0, - close_tag_offset: 0, - records_span_len: 0, - direct_record_stride: 6, - direct_record_stride_hex: "0x00000006".to_string(), - live_id_bound: 3865, - live_id_bound_hex: "0x00000f19".to_string(), - live_record_count: 3865, - live_record_count_hex: "0x00000f19".to_string(), - owner_shared_dword: 0xff000000, - owner_shared_dword_hex: "0xff000000".to_string(), - owner_shared_dword_relative_offset: 0, - owner_shared_dword_matches_first_compact_prefix_leading_dword: true, - first_record_child_count_after_owner_shared: Some(1), - first_record_child_count_after_owner_shared_hex: Some("0x0001".to_string()), - first_record_saved_primary_child_byte_after_owner_shared: Some(0xff), - first_record_saved_primary_child_byte_after_owner_shared_hex: Some( - "0xff".to_string(), - ), - first_record_first_name_tag_relative_offset_after_owner_shared: Some(3), - prefix_leading_dword: 0xff000000, - prefix_leading_dword_hex: "0xff000000".to_string(), - prefix_trailing_word: 1, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - first_embedded_name_tag_relative_offset: 0x20, - embedded_name_tag_count: 138, - decoded_embedded_name_row_count: 138, - decoded_embedded_name_row_with_tertiary_name_count: 0, - unique_compact_prefix_pattern_count: 7, - prefix_leading_dword_matching_embedded_profile_tag_count: 17, - unique_embedded_name_pair_count: 5, - first_embedded_primary_name: Some("TrackCapST_Cap.3dp".to_string()), - first_embedded_secondary_name: Some("Infrastructure".to_string()), - first_embedded_tertiary_name: None, - embedded_name_row_samples: Vec::new(), - compact_prefix_pattern_summaries: Vec::new(), - name_pair_summaries: vec![SmpSavePlacedStructureDynamicSideBufferNamePairSummary { - primary_name: "TrackCapST_Cap.3dp".to_string(), - secondary_name: "Infrastructure".to_string(), - count: 12, - first_name_tag_relative_offset: 0x20, - unique_compact_prefix_pattern_count: 2, - dominant_prefix_leading_dword: 0xff0000ff, - dominant_prefix_leading_dword_hex: "0xff0000ff".to_string(), - dominant_prefix_trailing_word: 1, - dominant_prefix_trailing_word_hex: "0x0001".to_string(), - dominant_prefix_separator_byte: 0xff, - dominant_prefix_separator_byte_hex: "0xff".to_string(), - dominant_prefix_count: 9, - }], - payload_envelope_summary: Some( - SmpSavePlacedStructureDynamicSideBufferPayloadEnvelopeSummary { - row_count_with_policy_tag_before_next_name: 120, - row_count_with_complete_0x55f1_0x55f2_0x55f3_envelope: 118, - row_count_missing_policy_tag_before_next_name: 18, - row_count_missing_profile_tag_after_policy: 2, - unique_policy_chunk_lens: vec![0x1a, 0x24], - unique_profile_chunk_lens: vec![0x08, 0x14], - dominant_policy_chunk_len: Some(0x1a), - dominant_policy_chunk_len_count: 110, - dominant_profile_chunk_len: Some(0x08), - dominant_profile_chunk_len_count: 90, - short_profile_flag_pair_summary: Some( - SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPairSummary { - row_count_with_0x06_profile_span: 72, - unique_flag_pair_count: 2, - dominant_first_flag_byte: Some(0x01), - dominant_first_flag_byte_hex: Some("0x01".to_string()), - dominant_first_flag_byte_count: 60, - dominant_second_flag_byte: Some(0x00), - dominant_second_flag_byte_hex: Some("0x00".to_string()), - dominant_second_flag_byte_count: 55, - dominant_flag_pair: Some( - SmpSavePlacedStructureDynamicSideBufferShortProfileFlagPair { - first_flag_byte: 0x01, - first_flag_byte_hex: "0x01".to_string(), - second_flag_byte: 0x00, - second_flag_byte_hex: "0x00".to_string(), - count: 48, - }, - ), - sample_rows: Vec::new(), - }, - ), - fixed_policy_summary: Some( - SmpSavePlacedStructureDynamicSideBufferFixedPolicySummary { - row_count_with_0x1a_policy_chunk: 118, - unique_trailing_word_count: 1, - dominant_trailing_word: Some(1), - dominant_trailing_word_hex: Some("0x0001".to_string()), - dominant_trailing_word_count: 118, - compact_prefix_correlations: Vec::new(), - sample_rows: Vec::new(), - }, - ), - name_prelude_candidate_summary: Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidateSummary { - row_count_with_candidate_window: 118, - unique_candidate_pattern_count: 2, - dominant_child_count_candidate: Some(1), - dominant_child_count_candidate_count: 110, - dominant_saved_primary_child_byte_candidate: Some(0xff), - dominant_saved_primary_child_byte_candidate_hex: Some( - "0xff".to_string(), - ), - dominant_saved_primary_child_byte_candidate_count: 110, - dominant_candidate_pattern: Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - count: 110, - }, - ), - candidate_pattern_correlations: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePatternCorrelation { - child_count_candidate: 2, - child_count_candidate_hex: "0x0002".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - row_count: 18, - unique_name_pair_count: 1, - unique_profile_span_count: 1, - dominant_primary_name: Some( - "BridgeSTWood_Section.3dp".to_string(), - ), - dominant_secondary_name: Some( - "Infrastructure".to_string(), - ), - dominant_name_pair_count: 18, - dominant_profile_span: Some(6), - dominant_profile_span_count: 10, - dominant_mode_family: Some("bridge".to_string()), - dominant_mode_family_count: 18, - mode_family_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "bridge".to_string(), - count: 18, - }, - ], - sample_rows: Vec::new(), - }, - ], - profile_span_correlations: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { - previous_profile_chunk_len_to_next_name_or_end: 3, - row_count: 17, - dominant_child_count_candidate: Some(1), - dominant_child_count_candidate_count: 17, - dominant_saved_primary_child_byte_candidate: Some(0xff), - dominant_saved_primary_child_byte_candidate_hex: Some( - "0xff".to_string(), - ), - dominant_saved_primary_child_byte_candidate_count: 17, - dominant_candidate_pattern: Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff" - .to_string(), - count: 17, - }, - ), - dominant_mode_family: Some("tunnel".to_string()), - dominant_mode_family_count: 15, - mode_family_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "track_cap".to_string(), - count: 2, - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "tunnel".to_string(), - count: 15, - }, - ], - compact_prefix_pattern_summaries: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { - prefix_leading_dword: 0x0000_55f3, - prefix_leading_dword_hex: "0x000055f3".to_string(), - prefix_trailing_word: 0x0001, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - count: 17, - }, - ], - sample_rows: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { - sample_index: 0, - name_tag_relative_offset: 1200, - primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - prefix_leading_dword: 0x0000_55f3, - prefix_leading_dword_hex: "0x000055f3".to_string(), - prefix_trailing_word: 0x0001, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - }, - ], - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { - previous_profile_chunk_len_to_next_name_or_end: 6, - row_count: 72, - dominant_child_count_candidate: Some(1), - dominant_child_count_candidate_count: 62, - dominant_saved_primary_child_byte_candidate: Some(0xff), - dominant_saved_primary_child_byte_candidate_hex: Some( - "0xff".to_string(), - ), - dominant_saved_primary_child_byte_candidate_count: 72, - dominant_candidate_pattern: Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff" - .to_string(), - count: 62, - }, - ), - dominant_mode_family: Some("bridge".to_string()), - dominant_mode_family_count: 72, - mode_family_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "bridge".to_string(), - count: 72, - }, - ], - compact_prefix_pattern_summaries: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { - prefix_leading_dword: 0xff00_0000, - prefix_leading_dword_hex: "0xff000000".to_string(), - prefix_trailing_word: 0x0001, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - count: 72, - }, - ], - sample_rows: vec![], - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanCorrelation { - previous_profile_chunk_len_to_next_name_or_end: 0x27, - row_count: 3, - dominant_child_count_candidate: Some(1), - dominant_child_count_candidate_count: 3, - dominant_saved_primary_child_byte_candidate: Some(0xff), - dominant_saved_primary_child_byte_candidate_hex: Some( - "0xff".to_string(), - ), - dominant_saved_primary_child_byte_candidate_count: 3, - dominant_candidate_pattern: Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff" - .to_string(), - count: 3, - }, - ), - dominant_mode_family: Some("bridge".to_string()), - dominant_mode_family_count: 2, - mode_family_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "bridge".to_string(), - count: 2, - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "tunnel".to_string(), - count: 1, - }, - ], - compact_prefix_pattern_summaries: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { - prefix_leading_dword: 0xff00_00ff, - prefix_leading_dword_hex: "0xff0000ff".to_string(), - prefix_trailing_word: 0x0001, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - count: 1, - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanPrefixSummary { - prefix_leading_dword: 0xff00_00ff, - prefix_leading_dword_hex: "0xff0000ff".to_string(), - prefix_trailing_word: 0x0002, - prefix_trailing_word_hex: "0x0002".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - count: 2, - }, - ], - sample_rows: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { - sample_index: 0, - name_tag_relative_offset: 2805, - primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - prefix_leading_dword: 0xff00_00ff, - prefix_leading_dword_hex: "0xff0000ff".to_string(), - prefix_trailing_word: 0x0001, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeProfileSpanSample { - sample_index: 1, - name_tag_relative_offset: 3764, - primary_name: Some("BridgeSTWood_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - prefix_leading_dword: 0xff00_00ff, - prefix_leading_dword_hex: "0xff0000ff".to_string(), - prefix_trailing_word: 0x0002, - prefix_trailing_word_hex: "0x0002".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - }, - ], - }, - ], - compact_prefix_correlations: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { - prefix_leading_dword: 0x0000_55f3, - prefix_leading_dword_hex: "0x000055f3".to_string(), - prefix_trailing_word: 0x0001, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - row_count: 17, - unique_name_pair_count: 2, - unique_profile_span_count: 1, - dominant_primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - dominant_secondary_name: Some("Infrastructure".to_string()), - dominant_name_pair_count: 15, - dominant_profile_span: Some(3), - dominant_profile_span_count: 17, - dominant_candidate_pattern: Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - count: 17, - }, - ), - dominant_mode_family: Some("tunnel".to_string()), - dominant_mode_family_count: 15, - mode_family_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "track_cap".to_string(), - count: 2, - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "tunnel".to_string(), - count: 15, - }, - ], - name_pair_summaries: vec![ - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { - primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - count: 15, - }, - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { - primary_name: Some("TrackCapST_Cap.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - count: 2, - }, - ], - profile_span_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { - previous_profile_chunk_len_to_next_name_or_end: 3, - count: 17, - }, - ], - rows_with_previous_short_profile_flag_pair: 17, - previous_short_profile_flag_pair_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { - first_flag_byte: 0x00, - first_flag_byte_hex: "0x00".to_string(), - second_flag_byte: 0x01, - second_flag_byte_hex: "0x01".to_string(), - count: 17, - }, - ], - sample_rows: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { - sample_index: 0, - name_tag_relative_offset: 1200, - primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - previous_profile_chunk_len_to_next_name_or_end: Some(3), - }, - ], - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { - prefix_leading_dword: 0xff00_00ff, - prefix_leading_dword_hex: "0xff0000ff".to_string(), - prefix_trailing_word: 0x0001, - prefix_trailing_word_hex: "0x0001".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - row_count: 1, - unique_name_pair_count: 1, - unique_profile_span_count: 1, - dominant_primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - dominant_secondary_name: Some("Infrastructure".to_string()), - dominant_name_pair_count: 1, - dominant_profile_span: Some(0x27), - dominant_profile_span_count: 1, - dominant_candidate_pattern: Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - count: 1, - }, - ), - dominant_mode_family: Some("tunnel".to_string()), - dominant_mode_family_count: 1, - mode_family_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "tunnel".to_string(), - count: 1, - }, - ], - name_pair_summaries: vec![ - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { - primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - count: 1, - }, - ], - profile_span_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { - previous_profile_chunk_len_to_next_name_or_end: 0x27, - count: 1, - }, - ], - rows_with_previous_short_profile_flag_pair: 1, - previous_short_profile_flag_pair_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { - first_flag_byte: 0x00, - first_flag_byte_hex: "0x00".to_string(), - second_flag_byte: 0x01, - second_flag_byte_hex: "0x01".to_string(), - count: 1, - }, - ], - sample_rows: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { - sample_index: 0, - name_tag_relative_offset: 2805, - primary_name: Some("TunnelSTBrick_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - previous_profile_chunk_len_to_next_name_or_end: Some(0x27), - }, - ], - }, - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixCorrelation { - prefix_leading_dword: 0xff00_00ff, - prefix_leading_dword_hex: "0xff0000ff".to_string(), - prefix_trailing_word: 0x0002, - prefix_trailing_word_hex: "0x0002".to_string(), - prefix_separator_byte: 0xff, - prefix_separator_byte_hex: "0xff".to_string(), - row_count: 2, - unique_name_pair_count: 1, - unique_profile_span_count: 1, - dominant_primary_name: Some("BridgeSTWood_Section.3dp".to_string()), - dominant_secondary_name: Some("Infrastructure".to_string()), - dominant_name_pair_count: 2, - dominant_profile_span: Some(0x27), - dominant_profile_span_count: 2, - dominant_candidate_pattern: Some( - SmpSavePlacedStructureDynamicSideBufferNamePreludeCandidatePattern { - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - count: 2, - }, - ), - dominant_mode_family: Some("bridge".to_string()), - dominant_mode_family_count: 2, - mode_family_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeModeFamilyCount { - mode_family: "bridge".to_string(), - count: 2, - }, - ], - name_pair_summaries: vec![ - SmpSavePlacedStructureDynamicSideBufferDominantProfileSpanNamePairSummary { - primary_name: Some("BridgeSTWood_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - count: 2, - }, - ], - profile_span_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixProfileSpanCount { - previous_profile_chunk_len_to_next_name_or_end: 0x27, - count: 2, - }, - ], - rows_with_previous_short_profile_flag_pair: 2, - previous_short_profile_flag_pair_counts: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixFlagPairCount { - first_flag_byte: 0x00, - first_flag_byte_hex: "0x00".to_string(), - second_flag_byte: 0x01, - second_flag_byte_hex: "0x01".to_string(), - count: 2, - }, - ], - sample_rows: vec![ - SmpSavePlacedStructureDynamicSideBufferNamePreludeCompactPrefixSample { - sample_index: 0, - name_tag_relative_offset: 3764, - primary_name: Some("BridgeSTWood_Section.3dp".to_string()), - secondary_name: Some("Infrastructure".to_string()), - child_count_candidate: 1, - child_count_candidate_hex: "0x0001".to_string(), - saved_primary_child_byte_candidate: 0xff, - saved_primary_child_byte_candidate_hex: "0xff".to_string(), - previous_profile_chunk_len_to_next_name_or_end: Some(0x27), - }, - ], - }, - ], - sample_rows: Vec::new(), - }, - ), - dominant_profile_span_class_summary: None, - sample_rows: Vec::new(), - }, - ), - live_entry_prelude_summary: Some( - SmpSavePlacedStructureDynamicSideBufferLiveEntryPreludeSummary { - live_entry_directory_row_count: 3865, - decoded_live_entry_id_count: 3865, - payload_relative_offset_monotonic: true, - rows_with_payload_pointer_inside_records_span: 138, - rows_with_zero_child_count: 0, - rows_with_nonzero_child_count: 138, - rows_with_first_name_tag_after_prelude: 138, - rows_with_first_name_tag_at_offset_3: 138, - unique_child_count_values: vec![1], - unique_first_name_tag_relative_offsets: vec![3], - dominant_child_count: Some(1), - dominant_child_count_count: 138, - dominant_saved_primary_child_byte: Some(0), - dominant_saved_primary_child_byte_hex: Some("0x00".to_string()), - dominant_saved_primary_child_byte_count: 138, - dominant_first_name_tag_relative_offset: Some(3), - dominant_first_name_tag_relative_offset_count: 138, - sample_rows: Vec::new(), - }, - ), - evidence: Vec::new(), - }); - analysis.placed_structure_dynamic_side_buffer_alignment = - Some(SmpSavePlacedStructureDynamicSideBufferAlignmentProbe { - unique_side_buffer_name_pair_count: 5, - unique_triplet_name_pair_count: 56, - overlapping_name_pair_count: 0, - side_buffer_row_count: 138, - side_buffer_rows_with_matching_triplet_name_pair_count: 0, - side_buffer_rows_without_matching_triplet_name_pair_count: 138, - triplet_name_pairs_without_side_buffer_match_count: 56, - matched_name_pair_samples: Vec::new(), - unmatched_side_buffer_name_pair_samples: Vec::new(), - evidence: Vec::new(), - }); - - let trace = build_infrastructure_asset_trace_report(&analysis); - assert!(trace.side_buffer_present); - assert_eq!(trace.triplet_alignment_overlap_count, 0); - assert_eq!(trace.known_owner_bridge_fields.len(), 7); - assert_eq!(trace.known_bridge_helpers.len(), 21); - assert_eq!(trace.next_owner_questions.len(), 4); - assert!(trace.next_owner_questions.iter().any(|line| { - line.contains("compact-prefix regimes subdivide") - && line.contains("0x0a BallastCap") - && line.contains("0x0b TrackCap") - && line.contains("0x02 Tunnel") - && line.contains("0x01 Bridge") - })); - assert!(trace.next_owner_questions.iter().any(|line| { - line.contains("direct route-entry bridge helpers") - && line.contains("0x00448a70/0x00493660/0x0048b660") - && line.contains("[this+0x248]") - && line.contains("cache/cleanup state") - })); - assert!(trace.known_bridge_helpers.iter().any( - |line| line.contains("0x004a2c80/0x004a34e0") - && line.contains("paired upstream infrastructure composition choosers") - && line.contains("BallastCap/Overpass") - )); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x0048a340") - && line.contains("[0x226]/[0x219]/[0x251]/bit 0x20")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00455a40") && line.contains("slot +0x44")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x004559d0") && line.contains("0x55f1/0x55f2/0x55f3")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00490960") && line.contains("selector propagator")) - ); - assert!( - trace - .known_bridge_helpers - .iter() - .any(|line| line.contains("0x00490200") && line.contains("route/link comparator")) - ); - assert_eq!(trace.candidate_consumer_hypotheses.len(), 3); - assert_eq!( - trace.candidate_consumer_hypotheses[0].status, - "highest_priority_static_mapping_target" - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x004a2c80 routes the DT family") - && line.contains("0x004a34e0 routes the ST family") - && line.contains("0x0048a340/0x0048f4c0/0x00490200/0x00490960")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x621a44/0x621a54 feed BridgeST") - && line.contains("0x621a94 feeds TunnelDT variants") - && line.contains("BallastCapDT/ST") - && line.contains("OverpassDT/ST")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any( - |line| line.contains("[this+0x226]==1 routes bridge families") - && line.contains("[this+0x226]==2 routes tunnel families") - && line.contains("[this+0x226]==3 routes overpass/ballast families") - && line.contains("bit 0x20 in [this+0x24c]") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any( - |line| line.contains("0x0048a340 as the exact chooser-state setter") - && line.contains("[this+0x226]") - && line.contains("[this+0x219]") - && line.contains("[this+0x251]") - && line.contains("[this+0x24c]") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any( - |line| line.contains("[this+0x219] indexes Steel/Stone/Wood tables") - && line.contains("value 2 takes the special suspension-cap path") - && line.contains("[this+0x251] selects Brick versus Concrete") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("[this+0x252]") - && line.contains("R10, L10, 12, 14, 16, and 18") - && line.contains("BridgeDT/BridgeST suspension-cap literals")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .blockers - .iter() - .any( - |line| line.contains("remaining mixed exact compact-prefix classes") - && line.contains("0xff0000ff/0x0002/0xff is pure bridge") - && line.contains("0x0005d368/0x0001/0xff is pure track-cap") - && line.contains("0xff0000ff/0x0001/0xff") - && line.contains("0x000055f3/0x0001/0xff") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains( - "profile-span mode-family correlations now also split the previous 0x55f3 spans directly" - ) && line.contains("span=0x6 rows=72") - && line.contains("span=0x3 rows=17") - && line.contains("span=0x27 rows=3")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains( - "exact compact-prefix correlations now split the residual prelude classes directly" - ) && line.contains("0x000055f3/0x0001/0xff") - && line.contains("0xff0000ff/0x0001/0xff") - && line.contains("0xff0000ff/0x0002/0xff")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("short 0x03-byte post-profile gaps") - && line.contains("track_cap:2") - && line.contains("tunnel:15") - && line.contains("0x000055f3/0x0001/0xff:17")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("sparse 0x27 post-profile outlier") - && line.contains("0xff0000ff/0x0001/0xff:1") - && line.contains("0xff0000ff/0x0002/0xff:2") - && line.contains("TunnelSTBrick_Section.3dp") - && line.contains("BridgeSTWood_Section.3dp")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any( - |line| line.contains("bridge-only two-child class is now grounded save-side") - && line.contains("0x0002") - && line.contains("BridgeSTWood_Section.3dp") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line - .contains("0xff0000ff/0x0001/0xff compact-prefix class is now explicit") - && line.contains("dominant prelude=0x0001/0xff")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line - .contains("0x000055f3/0x0001/0xff compact-prefix class is now explicit") - && line.contains("dominant prelude=0x0001/0xff")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line - .contains("0xff0000ff/0x0002/0xff compact-prefix class is now explicit") - && line.contains("dominant prelude=")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("slot +0x44 = 0x004559d0") - && line.contains("+0x40 = 0x00455fc0") - && line.contains("+0x4c = 0x00455930")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x004559d0 writing 0x55f1") - && line.contains("[this+0x206/+0x20a/+0x20e]") - && line.contains("0x52ec50") - && line.contains("0x55f3")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any( - |line| line.contains("chooser siblings calling 0x00490960 directly") - && line.contains("0x0048a340") - && line.contains("0x0048f4c0") - && line.contains("0x00490200") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line - .contains("0x00490960 copying selector fields into the child object") - && line.contains("0x00455b70") - && line.contains("0x005cfd74") - && line.contains("[this+0x248]")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00490200 reading the seeded lanes") - && line.contains("0x006cfca8") - && line.contains("[this+0x216/+0x218/+0x201/+0x202]")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x0048e140/0x0048e160/0x0048e180") - && line.contains("[this+0x206/+0x20a/+0x20e]") - && line.contains("0x0048e1a0") - && line.contains("[this+0x202]")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x0048ed30") - && line.contains("[this+0x248]") - && line.contains("[this+0x08]") - && line.contains("0x455d20/0x455650/0x53b080")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x004a2eba/0x004a30f9/0x004a339c") - && line.contains("0x005cb138 = BallastCapDT_Cap.3dp") - && line.contains("0x004909e2")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("mode 0x0b") - && line.contains("TrackCapDT/ST_Cap") - && line.contains("mode 0x03") - && line.contains("OverpassST_section") - && line.contains("mode 0x02") - && line.contains("mode 0x01")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("objdump on 0x00490960") - && line.contains("stem at [esp+0x14]") - && line.contains("[esp+0x18]/[esp+0x1c] feed 0x539530") - && line.contains("[esp+0x20] feeds 0x53a5b0") - && line.contains("[esp+0x34] gates whether the new child is cached") - && line.contains("selector-copy block") - && line.contains("[esp+0x28]/[esp+0x2c]/[esp+0x30]") - && line.contains("0x0048ed01/0x0048ed20") - && line.contains("bypass") - && line.contains("0x004a17eb/0x004a1995/0x004a1b44/0x004a1b7d") - && line.contains("0x004a1b95") - && line.contains("arg7/arg8/arg9 = -1/-1/0") - && line.contains("arg8 fixed at 1") - && line.contains("arg9 fixed at 0")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x004a17eb/0x004a1995") - && line.contains("0x621a94/0x621a64") - && line.contains("one-bit selector (0 or 1)") - && line.contains("0x004a1b44/0x004a1b7d") - && line.contains("0x621a9c/0x621a6c") - && line.contains("0x004a1b95") - && line.contains("0x0048ed01/0x0048ed20") - && line.contains("0x005cb198 versus 0x005cb1ac")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("objdump on 0x00455b70") - && line.contains("[this+0x206/+0x20a/+0x20e]") - && line.contains("0x51d820") - && line.contains("0x005c87a8") - && line.contains("0x005cfd74 = \"Infrastructure\"")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("objdump on 0x51d820") - && line.contains("owned heap strings") - && line.contains("0x5a1145") - && line.contains("0x5a125d") - && line.contains("byte-for-byte")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("objdump on 0x52ec50") - && line.contains("bit 5 of [this+0x20]") - && line.contains("bit 6 of [this+0x20]") - && line.contains("0x531030")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any( - |line| line.contains("objdump on 0x531030/0x5a464d/0x5a44a8") - && line.contains("0x531030 just forwards") - && line.contains("0x5a44a8 is the shared chunked stream write path") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any( - |line| line.contains("cannot come from selector-copy state alone") - && line.contains("0xff0000ff/0x0001/0xff") - && line.contains("dominant TrackCap rows") - && line.contains("tunnel residue") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any( - |line| line.contains("BridgeSTWood_Section.3dp aligns with mode 0x01 Bridge") - && line.contains("TunnelSTBrick_Cap/Section.3dp with mode 0x02 Tunnel") - && line.contains("BallastCapST_Cap.3dp with mode 0x0a BallastCap") - && line.contains("TrackCapST_Cap.3dp with mode 0x0b TrackCap") - ) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains( - "mode-family correlations now also split the candidate patterns directly" - ) && line.contains("0x0002/0xff rows=18") - && line.contains("bridge")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| { - line.contains("0x00518140") - && line.contains("12-byte row") - && line.contains("[collection+0x3c]") - }) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x00518380") && line.contains("ordinal")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("0x005181f0/0x00518260") - && line.contains("previous live id")) - ); - assert!(trace.notes.iter().any(|line| { - line.contains("pure bridge-only 0x0002/0xff candidate class is grounded save-side") - && line.contains("paired DT/ST siblings at 0x004a2c80 and 0x004a34e0") - && line.contains("grounded top-level branch meaning") - && line.contains("grounded bridge/tunnel material selector roles") - && line.contains("concrete child-construction/write-side chain through 0x00490960") - && line.contains("0x004559d0") - })); - assert!(trace.notes.iter().any(|line| line.contains("ST-only") - && line.contains("ST chooser sibling") - && line.contains("DT sibling remains grounded statically"))); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| { - line.contains("0x52ebd0/0x52ec50") - && (line.contains("bits 0x20/0x40") || line.contains("bits 0x20 and 0x40")) - }) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| { - (line.contains("0x455870/0x455930") - || (line.contains("0x455870") && line.contains("0x455930"))) - && (line.contains("six 4-byte lanes") || line.contains("six dword lanes")) - }) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| { - line.contains("0x530720") - && line.contains("0x52e8b0") - && line.contains("[this+0x4b/+0x4f/+0x53]") - }) - ); - assert!( - trace.candidate_consumer_hypotheses[2] - .evidence - .iter() - .any(|line| line.contains("0x00448a70") - && line.contains("[world+0x15e1/+0x162d]") - && line.contains("0x00448af0") - && line.contains("[world+0x2139/+0x213d/+0x2141]")) - ); - assert!( - trace.candidate_consumer_hypotheses[2] - .evidence - .iter() - .any(|line| line.contains("0x00493660") - && line.contains("[child+0x218]") - && line.contains("[child+0x226]") - && line.contains("0x006cfc9c") - && line.contains("0x487960")) - ); - assert!( - trace.candidate_consumer_hypotheses[2] - .evidence - .iter() - .any(|line| line.contains("0x0048b660") - && line.contains("[child+0x216]") - && line.contains("[child+0x201]") - && line.contains("0x53a350")) - ); - assert!( - trace.candidate_consumer_hypotheses[2] - .evidence - .iter() - .any(|line| line.contains("0x0048e2c0") - && line.contains("bit 0x20 in [child+0x201]") - && line.contains("0x53a3a0") - && line.contains("0x48a9e0") - && line.contains("0x0048e3c0") - && line.contains("[child+0x22e]") - && line.contains("0x006cfcb4") - && line.contains("bit 0x02 in [child+0x24c]")) - ); - assert!( - trace.candidate_consumer_hypotheses[0] - .evidence - .iter() - .any(|line| line.contains("u16 child count") - && line.contains("saved primary-child byte")) - ); - assert_eq!(trace.branches[0].status, "grounded_separate_owner_seam"); - assert_eq!(trace.branches[1].status, "disproved_by_grounded_probe"); - } -} diff --git a/crates/rrt-runtime/src/state/core/mod.rs b/crates/rrt-runtime/src/state/core/mod.rs new file mode 100644 index 0000000..fbb503b --- /dev/null +++ b/crates/rrt-runtime/src/state/core/mod.rs @@ -0,0 +1,12 @@ +mod profile; +mod refresh_market; +mod refresh_world; +mod runtime_state; +mod service; +mod validate; +mod world_restore; + +pub use profile::*; +pub use runtime_state::*; +pub use service::*; +pub use world_restore::*; diff --git a/crates/rrt-runtime/src/state/core/profile.rs b/crates/rrt-runtime/src/state/core/profile.rs new file mode 100644 index 0000000..b6213c6 --- /dev/null +++ b/crates/rrt-runtime/src/state/core/profile.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct RuntimeSaveProfileState { + #[serde(default)] + pub profile_kind: Option, + #[serde(default)] + pub profile_family: Option, + #[serde(default)] + pub map_path: Option, + #[serde(default)] + pub display_name: Option, + #[serde(default)] + pub selected_year_profile_lane: Option, + #[serde(default)] + pub sandbox_enabled: Option, + #[serde(default)] + pub campaign_scenario_enabled: Option, + #[serde(default)] + pub staged_profile_copy_on_restore: Option, +} diff --git a/crates/rrt-runtime/src/state/core/refresh_market.rs b/crates/rrt-runtime/src/state/core/refresh_market.rs new file mode 100644 index 0000000..44e040c --- /dev/null +++ b/crates/rrt-runtime/src/state/core/refresh_market.rs @@ -0,0 +1,128 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use crate::derived::{ + derive_runtime_chairman_holdings_share_price_total, rounded_cached_share_price_i64, + runtime_company_control_transfer_stat_value_f64, runtime_company_credit_rating, + runtime_company_direct_float_field_value_f64, runtime_company_investor_confidence, + runtime_company_management_attitude, runtime_company_prime_rate, runtime_round_f64_to_i64, +}; +use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH; +use crate::state::{RuntimeCompanyPeriodicSideLatchState, RuntimeState}; + +impl RuntimeState { + pub fn refresh_derived_market_state(&mut self) { + let company_share_prices = self + .service_state + .company_market_state + .iter() + .filter_map(|(company_id, market_state)| { + rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32) + .map(|share_price| (*company_id, share_price)) + }) + .collect::>(); + + let company_refresh = self + .companies + .iter() + .map(|company| { + let current_cash = runtime_company_control_transfer_stat_value_f64( + self, + company.company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let book_value_per_share = + runtime_company_direct_float_field_value_f64(self, company.company_id, 0x32f) + .and_then(runtime_round_f64_to_i64); + let prime_rate = runtime_company_prime_rate(self, company.company_id); + let credit_rating_score = runtime_company_credit_rating(self, company.company_id); + let investor_confidence = + runtime_company_investor_confidence(self, company.company_id); + let management_attitude = + runtime_company_management_attitude(self, company.company_id); + ( + company.company_id, + current_cash, + book_value_per_share, + credit_rating_score, + prime_rate, + investor_confidence, + management_attitude, + ) + }) + .collect::>(); + + for company in &mut self.companies { + if let Some(( + _, + current_cash, + book_value_per_share, + credit_rating_score, + prime_rate, + investor_confidence, + management_attitude, + )) = company_refresh + .iter() + .find(|(company_id, _, _, _, _, _, _)| *company_id == company.company_id) + { + if let Some(current_cash) = current_cash { + company.current_cash = *current_cash; + } + if let Some(book_value_per_share) = book_value_per_share { + company.book_value_per_share = *book_value_per_share; + } + if let Some(credit_rating_score) = credit_rating_score { + company.credit_rating_score = Some(*credit_rating_score); + } + if let Some(prime_rate) = prime_rate { + company.prime_rate = Some(*prime_rate); + } + if let Some(investor_confidence) = investor_confidence { + company.investor_confidence = *investor_confidence; + } + if let Some(management_attitude) = management_attitude { + company.management_attitude = *management_attitude; + } + } + } + + let known_company_ids = self + .companies + .iter() + .map(|company| company.company_id) + .collect::>(); + self.service_state + .company_periodic_side_latch_state + .retain(|company_id, _| known_company_ids.contains(company_id)); + for (company_id, market_state) in &self.service_state.company_market_state { + self.service_state + .company_periodic_side_latch_state + .entry(*company_id) + .or_insert_with(|| RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: None, + city_connection_latch: market_state.city_connection_latch, + linked_transit_latch: market_state.linked_transit_latch, + }); + } + + for profile in &mut self.chairman_profiles { + let preserved_threshold_adjusted_holdings_component = profile + .purchasing_power_total + .saturating_sub(profile.current_cash) + .max(0); + if let Some(holdings_value_total) = derive_runtime_chairman_holdings_share_price_total( + &profile.company_holdings, + &company_share_prices, + ) { + profile.holdings_value_total = holdings_value_total; + } + profile.net_worth_total = profile + .current_cash + .saturating_add(profile.holdings_value_total); + profile.purchasing_power_total = profile + .current_cash + .saturating_add(preserved_threshold_adjusted_holdings_component) + .max(profile.net_worth_total); + } + } +} diff --git a/crates/rrt-runtime/src/state/core/refresh_world.rs b/crates/rrt-runtime/src/state/core/refresh_world.rs new file mode 100644 index 0000000..c12659e --- /dev/null +++ b/crates/rrt-runtime/src/state/core/refresh_world.rs @@ -0,0 +1,109 @@ +use crate::derived::{ + runtime_decode_packed_calendar_tuple, runtime_selected_year_bucket_bands_from_scalar, + runtime_world_selected_year_bucket_scalar_from_year_word, + runtime_world_selected_year_gap_scalar_from_year_word, +}; +use crate::state::RuntimeState; + +impl RuntimeState { + pub fn refresh_derived_world_state(&mut self) { + self.world_restore + .linked_site_removal_follow_on_gate_enabled = self + .world_restore + .linked_site_removal_follow_on_gate_raw_u8 + .map(|raw| raw != 0); + self.world_restore.all_steam_locomotives_available_enabled = self + .world_restore + .all_steam_locomotives_available_raw_u8 + .map(|raw| raw != 0); + self.world_restore.all_diesel_locomotives_available_enabled = self + .world_restore + .all_diesel_locomotives_available_raw_u8 + .map(|raw| raw != 0); + self.world_restore + .all_electric_locomotives_available_enabled = self + .world_restore + .all_electric_locomotives_available_raw_u8 + .map(|raw| raw != 0); + if let Some(&lane_0_raw_u32) = self.world_restore.economic_tuning_lane_raw_u32.first() { + self.world_restore.economic_tuning_mirror_raw_u32 = Some(lane_0_raw_u32); + self.world_restore.economic_tuning_mirror_value_f32_text = + Some(format!("{:.6}", f32::from_bits(lane_0_raw_u32))); + } + let year_word = self + .world_restore + .packed_year_word_raw_u16 + .map(u32::from) + .or_else(|| { + self.world_restore + .current_calendar_tuple_word_raw_u32 + .zip(self.world_restore.current_calendar_tuple_word_2_raw_u32) + .map(|(word_0, word_1)| { + u32::from(runtime_decode_packed_calendar_tuple(word_0, word_1).year_word) + }) + }) + .unwrap_or(self.calendar.year); + if let Some(value) = runtime_world_selected_year_bucket_scalar_from_year_word(year_word) { + self.world_restore.selected_year_bucket_scalar_raw_u32 = Some(value.to_bits()); + self.world_restore + .selected_year_bucket_scalar_value_f32_text = Some(format!("{value:.6}")); + if let Some(bands) = runtime_selected_year_bucket_bands_from_scalar(value) { + self.world_restore.selected_year_bucket_direct_lane_raw_u32 = + bands.direct.iter().map(|lane| lane.to_bits()).collect(); + self.world_restore + .selected_year_bucket_direct_lane_value_f32_text = bands + .direct + .iter() + .map(|lane| format!("{lane:.6}")) + .collect(); + self.world_restore + .selected_year_bucket_complement_lane_raw_u32 = + bands.complement.iter().map(|lane| lane.to_bits()).collect(); + self.world_restore + .selected_year_bucket_complement_lane_value_f32_text = bands + .complement + .iter() + .map(|lane| format!("{lane:.6}")) + .collect(); + self.world_restore + .selected_year_bucket_scaled_companion_lane_raw_u32 = bands + .scaled_companion + .iter() + .map(|lane| lane.to_bits()) + .collect(); + self.world_restore + .selected_year_bucket_scaled_companion_lane_value_f32_text = bands + .scaled_companion + .iter() + .map(|lane| format!("{lane:.6}")) + .collect(); + } + } + if let Some(value) = runtime_world_selected_year_gap_scalar_from_year_word(year_word) { + self.world_restore.selected_year_gap_scalar_raw_u32 = Some(value.to_bits()); + self.world_restore.selected_year_gap_scalar_value_f32_text = + Some(format!("{value:.6}")); + } + for (key, value) in [ + ( + "world.all_steam_locos_available", + self.world_restore.all_steam_locomotives_available_enabled, + ), + ( + "world.all_diesel_locos_available", + self.world_restore.all_diesel_locomotives_available_enabled, + ), + ( + "world.all_electric_locos_available", + self.world_restore + .all_electric_locomotives_available_enabled, + ), + ] { + if let Some(enabled) = value { + self.world_flags.insert(key.to_string(), enabled); + } else { + self.world_flags.remove(key); + } + } + } +} diff --git a/crates/rrt-runtime/src/state/core/runtime_state.rs b/crates/rrt-runtime/src/state/core/runtime_state.rs new file mode 100644 index 0000000..503a507 --- /dev/null +++ b/crates/rrt-runtime/src/state/core/runtime_state.rs @@ -0,0 +1,87 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::event::packed::RuntimePackedEventCollectionSummary; +use crate::event::records::RuntimeEventRecord; +use crate::state::{ + CalendarPoint, RuntimeCargoCatalogEntry, RuntimeChairmanProfile, RuntimeCompany, + RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, + RuntimeLocomotiveCatalogEntry, RuntimePlayer, RuntimeSaveProfileState, RuntimeServiceState, + RuntimeTerritory, RuntimeTrain, RuntimeWorldRestoreState, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeState { + pub calendar: CalendarPoint, + #[serde(default)] + pub world_flags: BTreeMap, + #[serde(default)] + pub save_profile: RuntimeSaveProfileState, + #[serde(default)] + pub world_restore: RuntimeWorldRestoreState, + #[serde(default)] + pub metadata: BTreeMap, + #[serde(default)] + pub companies: Vec, + #[serde(default)] + pub selected_company_id: Option, + #[serde(default)] + pub players: Vec, + #[serde(default)] + pub selected_player_id: Option, + #[serde(default)] + pub chairman_profiles: Vec, + #[serde(default)] + pub selected_chairman_profile_id: Option, + #[serde(default)] + pub trains: Vec, + #[serde(default)] + pub locomotive_catalog: Vec, + #[serde(default)] + pub cargo_catalog: Vec, + #[serde(default)] + pub territories: Vec, + #[serde(default)] + pub company_territory_track_piece_counts: Vec, + #[serde(default)] + pub company_territory_access: Vec, + #[serde(default)] + pub packed_event_collection: Option, + #[serde(default)] + pub event_runtime_records: Vec, + #[serde(default)] + pub candidate_availability: BTreeMap, + #[serde(default)] + pub named_locomotive_availability: BTreeMap, + #[serde(default)] + pub named_locomotive_cost: BTreeMap, + #[serde(default)] + pub all_cargo_price_override: Option, + #[serde(default)] + pub named_cargo_price_overrides: BTreeMap, + #[serde(default)] + pub all_cargo_production_override: Option, + #[serde(default)] + pub factory_cargo_production_override: Option, + #[serde(default)] + pub farm_mine_cargo_production_override: Option, + #[serde(default)] + pub named_cargo_production_overrides: BTreeMap, + #[serde(default)] + pub cargo_production_overrides: BTreeMap, + #[serde(default)] + pub world_runtime_variables: BTreeMap, + #[serde(default)] + pub company_runtime_variables: BTreeMap>, + #[serde(default)] + pub player_runtime_variables: BTreeMap>, + #[serde(default)] + pub territory_runtime_variables: BTreeMap>, + #[serde(default)] + pub world_scalar_overrides: BTreeMap, + #[serde(default)] + pub special_conditions: BTreeMap, + #[serde(default)] + pub service_state: RuntimeServiceState, +} diff --git a/crates/rrt-runtime/src/state/core/service.rs b/crates/rrt-runtime/src/state/core/service.rs new file mode 100644 index 0000000..25b7029 --- /dev/null +++ b/crates/rrt-runtime/src/state/core/service.rs @@ -0,0 +1,70 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::event::news::{RuntimeAnnualFinanceNewsEvent, RuntimeNearCityAcquisitionNewsEvent}; +use crate::state::{ + RuntimeCompanyAnnualFinancePolicyAction, RuntimeCompanyMarketState, + RuntimeCompanyPeriodicSideLatchState, RuntimeNearCityAcquisitionRegion, + RuntimeNearCityAcquisitionSite, RuntimeWorldRoutePreferenceOverrideState, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct RuntimeServiceState { + #[serde(default)] + pub periodic_boundary_calls: u64, + #[serde(default)] + pub annual_finance_service_calls: u64, + #[serde(default)] + pub periodic_route_preference_override_apply_count: u64, + #[serde(default)] + pub periodic_route_preference_override_restore_count: u64, + #[serde(default)] + pub trigger_dispatch_counts: BTreeMap, + #[serde(default)] + pub total_event_record_services: u64, + #[serde(default)] + pub dirty_rerun_count: u64, + #[serde(default)] + pub world_issue_opinion_base_terms_raw_i32: Vec, + #[serde(default)] + pub company_market_state: BTreeMap, + #[serde(default)] + pub company_periodic_side_latch_state: BTreeMap, + #[serde(default)] + pub active_periodic_route_preference_override: Option, + #[serde(default)] + pub last_periodic_route_preference_override: Option, + #[serde(default)] + pub annual_finance_last_actions: BTreeMap, + #[serde(default)] + pub annual_finance_action_counts: BTreeMap, + #[serde(default)] + pub annual_dividend_adjustment_commit_count: u64, + #[serde(default)] + pub annual_bond_last_retired_principal_total: u64, + #[serde(default)] + pub annual_bond_last_issued_principal_total: u64, + #[serde(default)] + pub annual_stock_repurchase_last_share_count: u64, + #[serde(default)] + pub annual_stock_issue_last_share_count: u64, + #[serde(default)] + pub annual_finance_last_news_family_candidates: BTreeMap, + #[serde(default)] + pub annual_finance_last_news_events: Vec, + #[serde(default)] + pub near_city_acquisition_regions: Vec, + #[serde(default)] + pub near_city_acquisition_sites: Vec, + #[serde(default)] + pub near_city_acquisition_service_calls: u64, + #[serde(default)] + pub near_city_acquisition_last_news_family_candidates: BTreeMap, + #[serde(default)] + pub near_city_acquisition_last_news_events: Vec, + #[serde(default)] + pub chairman_issue_opinion_terms_raw_i32: BTreeMap>, + #[serde(default)] + pub chairman_personality_raw_u8: BTreeMap, +} diff --git a/crates/rrt-runtime/src/state/core/validate/catalogs.rs b/crates/rrt-runtime/src/state/core/validate/catalogs.rs new file mode 100644 index 0000000..b7d8d0a --- /dev/null +++ b/crates/rrt-runtime/src/state/core/validate/catalogs.rs @@ -0,0 +1,149 @@ +use std::collections::BTreeSet; + +use super::context::ValidationContext; +use crate::state::RuntimeState; + +pub(super) fn validate_catalogs( + state: &RuntimeState, + context: &mut ValidationContext, +) -> Result<(), String> { + let mut seen_territory_names = BTreeSet::new(); + for territory in &state.territories { + if !context.seen_territory_ids.insert(territory.territory_id) { + return Err(format!("duplicate territory_id {}", territory.territory_id)); + } + if let Some(name) = territory.name.as_deref() { + if name.trim().is_empty() { + return Err(format!( + "territory_id {} has an empty name", + territory.territory_id + )); + } + if !seen_territory_names.insert(name.to_string()) { + return Err(format!("duplicate territory name {name:?}")); + } + } + } + + let mut seen_train_ids = BTreeSet::new(); + for train in &state.trains { + if !seen_train_ids.insert(train.train_id) { + return Err(format!("duplicate train_id {}", train.train_id)); + } + if !context.seen_company_ids.contains(&train.owner_company_id) { + return Err(format!( + "train_id {} references unknown owner_company_id {}", + train.train_id, train.owner_company_id + )); + } + if let Some(territory_id) = train.territory_id { + if !context.seen_territory_ids.contains(&territory_id) { + return Err(format!( + "train_id {} references unknown territory_id {}", + train.train_id, territory_id + )); + } + } + if train.retired && train.active { + return Err(format!( + "train_id {} cannot be active and retired at the same time", + train.train_id + )); + } + if train + .locomotive_name + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(format!( + "train_id {} has an empty locomotive_name", + train.train_id + )); + } + } + + let mut seen_locomotive_ids = BTreeSet::new(); + let mut seen_locomotive_names = BTreeSet::new(); + for entry in &state.locomotive_catalog { + if !seen_locomotive_ids.insert(entry.locomotive_id) { + return Err(format!( + "duplicate locomotive_catalog.locomotive_id {}", + entry.locomotive_id + )); + } + if entry.name.trim().is_empty() { + return Err(format!( + "locomotive_catalog entry {} has an empty name", + entry.locomotive_id + )); + } + if !seen_locomotive_names.insert(entry.name.clone()) { + return Err(format!( + "duplicate locomotive_catalog.name {:?}", + entry.name + )); + } + } + + let mut seen_cargo_slots = BTreeSet::new(); + let mut seen_cargo_labels = BTreeSet::new(); + for entry in &state.cargo_catalog { + if !(1..=11).contains(&entry.slot_id) { + return Err(format!( + "cargo_catalog entry has out-of-range slot_id {}", + entry.slot_id + )); + } + if !seen_cargo_slots.insert(entry.slot_id) { + return Err(format!("duplicate cargo_catalog.slot_id {}", entry.slot_id)); + } + if entry.label.trim().is_empty() { + return Err(format!( + "cargo_catalog entry {} has an empty label", + entry.slot_id + )); + } + if !seen_cargo_labels.insert(entry.label.clone()) { + return Err(format!("duplicate cargo_catalog.label {:?}", entry.label)); + } + } + + for entry in &state.company_territory_track_piece_counts { + if !context.seen_company_ids.contains(&entry.company_id) { + return Err(format!( + "company_territory_track_piece_counts references unknown company_id {}", + entry.company_id + )); + } + if !context.seen_territory_ids.contains(&entry.territory_id) { + return Err(format!( + "company_territory_track_piece_counts references unknown territory_id {}", + entry.territory_id + )); + } + } + + let mut seen_company_territory_access = BTreeSet::new(); + for entry in &state.company_territory_access { + if !context.seen_company_ids.contains(&entry.company_id) { + return Err(format!( + "company_territory_access references unknown company_id {}", + entry.company_id + )); + } + if !context.seen_territory_ids.contains(&entry.territory_id) { + return Err(format!( + "company_territory_access references unknown territory_id {}", + entry.territory_id + )); + } + if !seen_company_territory_access.insert((entry.company_id, entry.territory_id)) { + return Err(format!( + "duplicate company_territory_access pair ({}, {})", + entry.company_id, entry.territory_id + )); + } + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/state/core/validate/context.rs b/crates/rrt-runtime/src/state/core/validate/context.rs new file mode 100644 index 0000000..70859df --- /dev/null +++ b/crates/rrt-runtime/src/state/core/validate/context.rs @@ -0,0 +1,14 @@ +use std::collections::BTreeSet; + +#[derive(Default)] +pub(super) struct ValidationContext { + pub(super) seen_company_ids: BTreeSet, + pub(super) active_company_ids: BTreeSet, + pub(super) seen_player_ids: BTreeSet, + pub(super) active_player_ids: BTreeSet, + pub(super) seen_chairman_profile_ids: BTreeSet, + pub(super) active_chairman_profile_ids: BTreeSet, + pub(super) seen_territory_ids: BTreeSet, + pub(super) seen_near_city_region_ids: BTreeSet, + pub(super) seen_near_city_site_ids: BTreeSet, +} diff --git a/crates/rrt-runtime/src/state/core/validate/entities.rs b/crates/rrt-runtime/src/state/core/validate/entities.rs new file mode 100644 index 0000000..58636a7 --- /dev/null +++ b/crates/rrt-runtime/src/state/core/validate/entities.rs @@ -0,0 +1,218 @@ +use std::collections::BTreeSet; + +use super::context::ValidationContext; +use crate::state::RuntimeState; + +pub(super) fn validate_entities( + state: &RuntimeState, + context: &mut ValidationContext, +) -> Result<(), String> { + for company in &state.companies { + if !context.seen_company_ids.insert(company.company_id) { + return Err(format!("duplicate company_id {}", company.company_id)); + } + if company.active { + context.active_company_ids.insert(company.company_id); + } + } + if let Some(selected_company_id) = state.selected_company_id { + if !context.seen_company_ids.contains(&selected_company_id) { + return Err(format!( + "selected_company_id {} does not reference a live company", + selected_company_id + )); + } + if !context.active_company_ids.contains(&selected_company_id) { + return Err(format!( + "selected_company_id {} must reference an active company", + selected_company_id + )); + } + } + + let mut seen_chairman_names = BTreeSet::new(); + for chairman in &state.chairman_profiles { + if !context + .seen_chairman_profile_ids + .insert(chairman.profile_id) + { + return Err(format!( + "duplicate chairman_profile.profile_id {}", + chairman.profile_id + )); + } + if chairman.name.trim().is_empty() { + return Err(format!( + "chairman_profile {} has an empty name", + chairman.profile_id + )); + } + if !seen_chairman_names.insert(chairman.name.clone()) { + return Err(format!( + "duplicate chairman_profile.name {:?}", + chairman.name + )); + } + if chairman.active { + context + .active_chairman_profile_ids + .insert(chairman.profile_id); + } + if let Some(linked_company_id) = chairman.linked_company_id { + if !context.seen_company_ids.contains(&linked_company_id) { + return Err(format!( + "chairman_profile {} references unknown linked_company_id {}", + chairman.profile_id, linked_company_id + )); + } + } + for company_id in chairman.company_holdings.keys() { + if !context.seen_company_ids.contains(company_id) { + return Err(format!( + "chairman_profile {} references unknown holdings company_id {}", + chairman.profile_id, company_id + )); + } + } + } + if let Some(selected_chairman_profile_id) = state.selected_chairman_profile_id { + if !context + .seen_chairman_profile_ids + .contains(&selected_chairman_profile_id) + { + return Err(format!( + "selected_chairman_profile_id {} does not reference a live chairman profile", + selected_chairman_profile_id + )); + } + if !context + .active_chairman_profile_ids + .contains(&selected_chairman_profile_id) + { + return Err(format!( + "selected_chairman_profile_id {} must reference an active chairman profile", + selected_chairman_profile_id + )); + } + } + + for player in &state.players { + if !context.seen_player_ids.insert(player.player_id) { + return Err(format!("duplicate player_id {}", player.player_id)); + } + if player.active { + context.active_player_ids.insert(player.player_id); + } + } + if let Some(selected_player_id) = state.selected_player_id { + if !context.seen_player_ids.contains(&selected_player_id) { + return Err(format!( + "selected_player_id {} does not reference a live player", + selected_player_id + )); + } + if !context.active_player_ids.contains(&selected_player_id) { + return Err(format!( + "selected_player_id {} must reference an active player", + selected_player_id + )); + } + } + + for company in &state.companies { + if let Some(linked_chairman_profile_id) = company.linked_chairman_profile_id { + let linked_profile = state + .chairman_profiles + .iter() + .find(|profile| profile.profile_id == linked_chairman_profile_id) + .ok_or_else(|| { + format!( + "company {} references unknown linked_chairman_profile_id {}", + company.company_id, linked_chairman_profile_id + ) + })?; + if linked_profile.linked_company_id != Some(company.company_id) { + return Err(format!( + "company {} linked_chairman_profile_id {} must point back through linked_company_id", + company.company_id, linked_chairman_profile_id + )); + } + } + } + for chairman in &state.chairman_profiles { + if let Some(linked_company_id) = chairman.linked_company_id { + let linked_company = state + .companies + .iter() + .find(|company| company.company_id == linked_company_id) + .ok_or_else(|| { + format!( + "chairman_profile {} references unknown linked_company_id {}", + chairman.profile_id, linked_company_id + ) + })?; + if linked_company.linked_chairman_profile_id != Some(chairman.profile_id) { + return Err(format!( + "chairman_profile {} linked_company_id {} must point back through linked_chairman_profile_id", + chairman.profile_id, linked_company_id + )); + } + } + } + + for region in &state.service_state.near_city_acquisition_regions { + if !context.seen_near_city_region_ids.insert(region.region_id) { + return Err(format!( + "duplicate near_city_acquisition_region.region_id {}", + region.region_id + )); + } + if region.name.trim().is_empty() { + return Err(format!( + "near_city_acquisition_region {} has an empty name", + region.region_id + )); + } + } + + for site in &state.service_state.near_city_acquisition_sites { + if !context.seen_near_city_site_ids.insert(site.site_id) { + return Err(format!( + "duplicate near_city_acquisition_site.site_id {}", + site.site_id + )); + } + if site.primary_name.trim().is_empty() { + return Err(format!( + "near_city_acquisition_site {} has an empty primary_name", + site.site_id + )); + } + if let Some(region_id) = site.region_id { + if !context.seen_near_city_region_ids.contains(®ion_id) { + return Err(format!( + "near_city_acquisition_site {} references unknown region_id {}", + site.site_id, region_id + )); + } + } + if let Some(owner_company_id) = site.owner_company_id { + if !context.seen_company_ids.contains(&owner_company_id) { + return Err(format!( + "near_city_acquisition_site {} references unknown owner_company_id {}", + site.site_id, owner_company_id + )); + } + } + if let Some(preferred_company_id) = site.preferred_company_id { + if !context.seen_company_ids.contains(&preferred_company_id) { + return Err(format!( + "near_city_acquisition_site {} references unknown preferred_company_id {}", + site.site_id, preferred_company_id + )); + } + } + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/state/core/validate/events.rs b/crates/rrt-runtime/src/state/core/validate/events.rs new file mode 100644 index 0000000..33b2feb --- /dev/null +++ b/crates/rrt-runtime/src/state/core/validate/events.rs @@ -0,0 +1,272 @@ +use std::collections::BTreeSet; + +use super::context::ValidationContext; +use crate::state::RuntimeState; +use crate::validation::{validate_runtime_condition, validate_runtime_effect}; + +pub(super) fn validate_events( + state: &RuntimeState, + context: &ValidationContext, +) -> Result<(), String> { + let mut seen_record_ids = BTreeSet::new(); + for record in &state.event_runtime_records { + if !seen_record_ids.insert(record.record_id) { + return Err(format!("duplicate record_id {}", record.record_id)); + } + for (condition_index, condition) in record.conditions.iter().enumerate() { + validate_runtime_condition( + condition, + &context.seen_company_ids, + &context.seen_player_ids, + &context.seen_chairman_profile_ids, + &context.seen_territory_ids, + ) + .map_err(|err| { + format!( + "event_runtime_records[record_id={}].conditions[{condition_index}] {err}", + record.record_id + ) + })?; + } + for (effect_index, effect) in record.effects.iter().enumerate() { + validate_runtime_effect( + effect, + &context.seen_company_ids, + &context.seen_player_ids, + &context.seen_chairman_profile_ids, + &context.seen_territory_ids, + ) + .map_err(|err| { + format!( + "event_runtime_records[record_id={}].effects[{effect_index}] {err}", + record.record_id + ) + })?; + } + } + + if let Some(summary) = &state.packed_event_collection { + if summary.source_kind.trim().is_empty() { + return Err("packed_event_collection.source_kind must not be empty".to_string()); + } + if summary.mechanism_family.trim().is_empty() { + return Err("packed_event_collection.mechanism_family must not be empty".to_string()); + } + if summary.mechanism_confidence.trim().is_empty() { + return Err( + "packed_event_collection.mechanism_confidence must not be empty".to_string(), + ); + } + if summary + .container_profile_family + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err( + "packed_event_collection.container_profile_family must not be empty".to_string(), + ); + } + if summary.packed_state_version_hex.trim().is_empty() { + return Err( + "packed_event_collection.packed_state_version_hex must not be empty".to_string(), + ); + } + if summary.live_record_count != summary.live_entry_ids.len() { + return Err( + "packed_event_collection.live_record_count must match live_entry_ids length" + .to_string(), + ); + } + if summary.live_record_count != summary.records.len() { + return Err( + "packed_event_collection.live_record_count must match records length".to_string(), + ); + } + let decoded_record_count = summary + .records + .iter() + .filter(|record| record.decode_status != "unsupported_framing") + .count(); + if summary.decoded_record_count != decoded_record_count { + return Err( + "packed_event_collection.decoded_record_count must match decoded records" + .to_string(), + ); + } + let importable_or_imported_count = summary + .records + .iter() + .filter(|record| { + record.executable_import_ready + || record.import_outcome.as_deref() == Some("imported") + }) + .count(); + if summary.imported_runtime_record_count > importable_or_imported_count { + return Err( + "packed_event_collection.imported_runtime_record_count must not exceed importable or imported records" + .to_string(), + ); + } + + let mut previous_id = None; + for (record_index, entry_id) in summary.live_entry_ids.iter().enumerate() { + if *entry_id == 0 { + return Err( + "packed_event_collection.live_entry_ids must not contain id 0".to_string(), + ); + } + if *entry_id > summary.live_id_bound { + return Err(format!( + "packed_event_collection.live_entry_id {} exceeds live_id_bound {}", + entry_id, summary.live_id_bound + )); + } + if previous_id.is_some_and(|prior| prior >= *entry_id) { + return Err( + "packed_event_collection.live_entry_ids must be strictly ascending".to_string(), + ); + } + previous_id = Some(*entry_id); + + let record = &summary.records[record_index]; + if record.live_entry_id != *entry_id { + return Err(format!( + "packed_event_collection.records[{record_index}].live_entry_id must match live_entry_ids" + )); + } + if record.record_index != record_index { + return Err(format!( + "packed_event_collection.records[{record_index}].record_index must match position" + )); + } + if record.decode_status.trim().is_empty() { + return Err(format!( + "packed_event_collection.records[{record_index}].decode_status must not be empty" + )); + } + if record.payload_family.trim().is_empty() { + return Err(format!( + "packed_event_collection.records[{record_index}].payload_family must not be empty" + )); + } + if record + .import_outcome + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(format!( + "packed_event_collection.records[{record_index}].import_outcome must not be empty" + )); + } + if record.grouped_effect_row_counts.len() != 4 { + return Err(format!( + "packed_event_collection.records[{record_index}].grouped_effect_row_counts must contain exactly 4 entries" + )); + } + if record.payload_family == "real_packed_v1" + && record.standalone_condition_rows.len() != record.standalone_condition_row_count + { + return Err(format!( + "packed_event_collection.records[{record_index}].standalone_condition_rows must match standalone_condition_row_count" + )); + } + if record.payload_family == "real_packed_v1" + && record.grouped_effect_rows.len() + != record.grouped_effect_row_counts.iter().sum::() + { + return Err(format!( + "packed_event_collection.records[{record_index}].grouped_effect_rows must match grouped_effect_row_counts" + )); + } + for band in &record.text_bands { + if band.label.trim().is_empty() { + return Err(format!( + "packed_event_collection.records[{record_index}].text_bands contains an empty label" + )); + } + } + if let Some(control) = &record.compact_control { + if control.grouped_target_scope_ordinals_0x7fb.len() != 4 { + return Err(format!( + "packed_event_collection.records[{record_index}].compact_control.grouped_target_scope_ordinals_0x7fb must contain exactly 4 entries" + )); + } + if control.grouped_scope_checkboxes_0x7ff.len() != 4 { + return Err(format!( + "packed_event_collection.records[{record_index}].compact_control.grouped_scope_checkboxes_0x7ff must contain exactly 4 entries" + )); + } + if control.grouped_territory_selectors_0x80f.len() != 4 { + return Err(format!( + "packed_event_collection.records[{record_index}].compact_control.grouped_territory_selectors_0x80f must contain exactly 4 entries" + )); + } + } + for row in &record.standalone_condition_rows { + if row + .candidate_name + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(format!( + "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty candidate_name" + )); + } + if row + .comparator + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(format!( + "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty comparator" + )); + } + if row + .metric + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(format!( + "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty metric" + )); + } + if row + .semantic_family + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(format!( + "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty semantic_family" + )); + } + if row + .semantic_preview + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(format!( + "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty semantic_preview" + )); + } + } + for row in &record.grouped_effect_rows { + if row.row_shape.trim().is_empty() { + return Err(format!( + "packed_event_collection.records[{record_index}].grouped_effect_rows contains an empty row_shape" + )); + } + if row + .locomotive_name + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(format!( + "packed_event_collection.records[{record_index}].grouped_effect_rows contains an empty locomotive_name" + )); + } + } + } + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/state/core/validate/metadata.rs b/crates/rrt-runtime/src/state/core/validate/metadata.rs new file mode 100644 index 0000000..ba91437 --- /dev/null +++ b/crates/rrt-runtime/src/state/core/validate/metadata.rs @@ -0,0 +1,71 @@ +use crate::state::RuntimeState; + +pub(super) fn validate_metadata(state: &RuntimeState) -> Result<(), String> { + for key in state.world_flags.keys() { + if key.trim().is_empty() { + return Err("world_flags contains an empty key".to_string()); + } + } + + for (label, value) in [ + ( + "save_profile.profile_kind", + state.save_profile.profile_kind.as_deref(), + ), + ( + "save_profile.profile_family", + state.save_profile.profile_family.as_deref(), + ), + ( + "save_profile.map_path", + state.save_profile.map_path.as_deref(), + ), + ( + "save_profile.display_name", + state.save_profile.display_name.as_deref(), + ), + ] { + if value.is_some_and(|text| text.trim().is_empty()) { + return Err(format!("{label} must not be empty")); + } + } + + if state.world_restore.selected_year_profile_lane.is_none() + && (state.world_restore.campaign_scenario_enabled.is_some() + || state.world_restore.sandbox_enabled.is_some()) + { + return Err( + "world_restore.selected_year_profile_lane must be present when world restore flags are populated" + .to_string(), + ); + } + + if state + .world_restore + .absolute_counter_restore_kind + .as_deref() + .is_some_and(|text| text.trim().is_empty()) + { + return Err("world_restore.absolute_counter_restore_kind must not be empty".to_string()); + } + if state + .world_restore + .absolute_counter_adjustment_context + .as_deref() + .is_some_and(|text| text.trim().is_empty()) + { + return Err( + "world_restore.absolute_counter_adjustment_context must not be empty".to_string(), + ); + } + for (key, value) in &state.metadata { + if key.trim().is_empty() { + return Err("metadata contains an empty key".to_string()); + } + if value.trim().is_empty() { + return Err(format!("metadata[{key}] must not be empty")); + } + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/state/core/validate/mod.rs b/crates/rrt-runtime/src/state/core/validate/mod.rs new file mode 100644 index 0000000..81bc78d --- /dev/null +++ b/crates/rrt-runtime/src/state/core/validate/mod.rs @@ -0,0 +1,26 @@ +mod catalogs; +mod context; +mod entities; +mod events; +mod metadata; +mod runtime_variables; +mod service_state; + +use crate::state::RuntimeState; +use context::ValidationContext; + +impl RuntimeState { + pub fn validate(&self) -> Result<(), String> { + self.calendar.validate()?; + + let mut context = ValidationContext::default(); + entities::validate_entities(self, &mut context)?; + catalogs::validate_catalogs(self, &mut context)?; + events::validate_events(self, &context)?; + metadata::validate_metadata(self)?; + runtime_variables::validate_runtime_variables(self, &context)?; + service_state::validate_service_state(self, &context)?; + + Ok(()) + } +} diff --git a/crates/rrt-runtime/src/state/core/validate/runtime_variables.rs b/crates/rrt-runtime/src/state/core/validate/runtime_variables.rs new file mode 100644 index 0000000..0113bb3 --- /dev/null +++ b/crates/rrt-runtime/src/state/core/validate/runtime_variables.rs @@ -0,0 +1,109 @@ +use super::context::ValidationContext; +use crate::state::RuntimeState; + +pub(super) fn validate_runtime_variables( + state: &RuntimeState, + context: &ValidationContext, +) -> Result<(), String> { + for key in state.candidate_availability.keys() { + if key.trim().is_empty() { + return Err("candidate_availability contains an empty key".to_string()); + } + } + for key in state.named_locomotive_availability.keys() { + if key.trim().is_empty() { + return Err("named_locomotive_availability contains an empty key".to_string()); + } + } + for key in state.named_locomotive_cost.keys() { + if key.trim().is_empty() { + return Err("named_locomotive_cost contains an empty key".to_string()); + } + } + for key in state.named_cargo_price_overrides.keys() { + if key.trim().is_empty() { + return Err("named_cargo_price_overrides contains an empty key".to_string()); + } + } + for key in state.named_cargo_production_overrides.keys() { + if key.trim().is_empty() { + return Err("named_cargo_production_overrides contains an empty key".to_string()); + } + } + for slot in state.cargo_production_overrides.keys() { + if !(1..=11).contains(slot) { + return Err(format!( + "cargo_production_overrides contains out-of-range slot {}", + slot + )); + } + } + for index in state.world_runtime_variables.keys() { + if !(1..=4).contains(index) { + return Err(format!( + "world_runtime_variables contains out-of-range index {}", + index + )); + } + } + for (company_id, vars) in &state.company_runtime_variables { + if !context.seen_company_ids.contains(company_id) { + return Err(format!( + "company_runtime_variables references unknown company_id {}", + company_id + )); + } + for index in vars.keys() { + if !(1..=4).contains(index) { + return Err(format!( + "company_runtime_variables[{company_id}] contains out-of-range index {}", + index + )); + } + } + } + for (player_id, vars) in &state.player_runtime_variables { + if !context.seen_player_ids.contains(player_id) { + return Err(format!( + "player_runtime_variables references unknown player_id {}", + player_id + )); + } + for index in vars.keys() { + if !(1..=4).contains(index) { + return Err(format!( + "player_runtime_variables[{player_id}] contains out-of-range index {}", + index + )); + } + } + } + for (territory_id, vars) in &state.territory_runtime_variables { + if !context.seen_territory_ids.contains(territory_id) { + return Err(format!( + "territory_runtime_variables references unknown territory_id {}", + territory_id + )); + } + for index in vars.keys() { + if !(1..=4).contains(index) { + return Err(format!( + "territory_runtime_variables[{territory_id}] contains out-of-range index {}", + index + )); + } + } + } + for key in state.world_scalar_overrides.keys() { + if key.trim().is_empty() { + return Err("world_scalar_overrides contains an empty key".to_string()); + } + } + for key in state.special_conditions.keys() { + if key.trim().is_empty() { + return Err("special_conditions contains an empty key".to_string()); + } + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/state/core/validate/service_state.rs b/crates/rrt-runtime/src/state/core/validate/service_state.rs new file mode 100644 index 0000000..86236af --- /dev/null +++ b/crates/rrt-runtime/src/state/core/validate/service_state.rs @@ -0,0 +1,134 @@ +use super::context::ValidationContext; +use crate::state::RuntimeState; + +pub(super) fn validate_service_state( + state: &RuntimeState, + context: &ValidationContext, +) -> Result<(), String> { + for company_id in state.service_state.company_market_state.keys() { + if !context.seen_company_ids.contains(company_id) { + return Err(format!( + "service_state.company_market_state references unknown company_id {}", + company_id + )); + } + } + for company_id in state.service_state.company_periodic_side_latch_state.keys() { + if !context.seen_company_ids.contains(company_id) { + return Err(format!( + "service_state.company_periodic_side_latch_state references unknown company_id {}", + company_id + )); + } + } + if let Some(override_state) = &state + .service_state + .active_periodic_route_preference_override + { + if !context + .seen_company_ids + .contains(&override_state.company_id) + { + return Err(format!( + "service_state.active_periodic_route_preference_override references unknown company_id {}", + override_state.company_id + )); + } + } + if let Some(override_state) = &state.service_state.last_periodic_route_preference_override { + if !context + .seen_company_ids + .contains(&override_state.company_id) + { + return Err(format!( + "service_state.last_periodic_route_preference_override references unknown company_id {}", + override_state.company_id + )); + } + } + for company_id in state.service_state.annual_finance_last_actions.keys() { + if !context.seen_company_ids.contains(company_id) { + return Err(format!( + "service_state.annual_finance_last_actions references unknown company_id {}", + company_id + )); + } + } + for company_id in state + .service_state + .annual_finance_last_news_family_candidates + .keys() + { + if !context.seen_company_ids.contains(company_id) { + return Err(format!( + "service_state.annual_finance_last_news_family_candidates references unknown company_id {}", + company_id + )); + } + } + for news_event in &state.service_state.annual_finance_last_news_events { + if !context.seen_company_ids.contains(&news_event.company_id) { + return Err(format!( + "service_state.annual_finance_last_news_events references unknown company_id {}", + news_event.company_id + )); + } + } + for site_id in state + .service_state + .near_city_acquisition_last_news_family_candidates + .keys() + { + if !context.seen_near_city_site_ids.contains(site_id) { + return Err(format!( + "service_state.near_city_acquisition_last_news_family_candidates references unknown site_id {}", + site_id + )); + } + } + for news_event in &state.service_state.near_city_acquisition_last_news_events { + if !context.seen_company_ids.contains(&news_event.company_id) { + return Err(format!( + "service_state.near_city_acquisition_last_news_events references unknown company_id {}", + news_event.company_id + )); + } + if !context + .seen_near_city_site_ids + .contains(&news_event.site_id) + { + return Err(format!( + "service_state.near_city_acquisition_last_news_events references unknown site_id {}", + news_event.site_id + )); + } + } + for chairman_profile_id in state + .service_state + .chairman_issue_opinion_terms_raw_i32 + .keys() + { + if !context + .seen_chairman_profile_ids + .contains(chairman_profile_id) + { + return Err(format!( + "service_state.chairman_issue_opinion_terms_raw_i32 references unknown chairman_profile_id {}", + chairman_profile_id + )); + } + } + for chairman_profile_id in state.service_state.chairman_personality_raw_u8.keys() { + if !context + .seen_chairman_profile_ids + .contains(chairman_profile_id) + { + return Err(format!( + "service_state.chairman_personality_raw_u8 references unknown chairman_profile_id {}", + chairman_profile_id + )); + } + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/state/core/world_restore.rs b/crates/rrt-runtime/src/state/core/world_restore.rs new file mode 100644 index 0000000..a801fde --- /dev/null +++ b/crates/rrt-runtime/src/state/core/world_restore.rs @@ -0,0 +1,171 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeWorldFinanceNeighborhoodCandidate { + pub label: String, + pub relative_offset: usize, + pub relative_offset_hex: String, + pub raw_u32: u32, + pub raw_u32_hex: String, + pub value_i32: i32, + pub value_f32_text: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyStatBandCandidate { + pub label: String, + pub relative_offset: usize, + pub relative_offset_hex: String, + pub raw_u32: u32, + pub raw_u32_hex: String, + pub value_i32: i32, + pub value_f32_text: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct RuntimeWorldRestoreState { + #[serde(default)] + pub selected_year_profile_lane: Option, + #[serde(default)] + pub campaign_scenario_enabled: Option, + #[serde(default)] + pub sandbox_enabled: Option, + #[serde(default)] + pub seed_tuple_written_from_raw_lane: Option, + #[serde(default)] + pub absolute_counter_requires_shell_context: Option, + #[serde(default)] + pub absolute_counter_reconstructible_from_save: Option, + #[serde(default)] + pub current_calendar_tuple_word_raw_u32: Option, + #[serde(default)] + pub packed_year_word_raw_u16: Option, + #[serde(default)] + pub partial_year_progress_raw_u8: Option, + #[serde(default)] + pub current_calendar_tuple_word_2_raw_u32: Option, + #[serde(default)] + pub absolute_counter_raw_u32: Option, + #[serde(default)] + pub absolute_counter_mirror_raw_u32: Option, + #[serde(default)] + pub disable_cargo_economy_special_condition_slot: Option, + #[serde(default)] + pub disable_cargo_economy_special_condition_reconstructible_from_save: Option, + #[serde(default)] + pub disable_cargo_economy_special_condition_write_side_grounded: Option, + #[serde(default)] + pub disable_cargo_economy_special_condition_enabled: Option, + #[serde(default)] + pub use_bio_accelerator_cars_enabled: Option, + #[serde(default)] + pub use_wartime_cargos_enabled: Option, + #[serde(default)] + pub disable_train_crashes_enabled: Option, + #[serde(default)] + pub disable_train_crashes_and_breakdowns_enabled: Option, + #[serde(default)] + pub ai_ignore_territories_at_startup_enabled: Option, + #[serde(default)] + pub limited_track_building_amount: Option, + #[serde(default)] + pub economic_status_code: Option, + #[serde(default)] + pub territory_access_cost: Option, + #[serde(default)] + pub linked_site_removal_follow_on_gate_raw_u8: Option, + #[serde(default)] + pub linked_site_removal_follow_on_gate_enabled: Option, + #[serde(default)] + pub auto_show_grade_during_track_lay_raw_u8: Option, + #[serde(default)] + pub starting_building_density_level_raw_u8: Option, + #[serde(default)] + pub post_text_building_density_growth_raw_u8: Option, + #[serde(default)] + pub leftover_simulation_time_accumulator_raw_u32: Option, + #[serde(default)] + pub leftover_simulation_time_accumulator_value_f32_text: Option, + #[serde(default)] + pub selected_year_lane_snapshot_raw_u8: Option, + #[serde(default)] + pub all_steam_locomotives_available_raw_u8: Option, + #[serde(default)] + pub all_steam_locomotives_available_enabled: Option, + #[serde(default)] + pub all_diesel_locomotives_available_raw_u8: Option, + #[serde(default)] + pub all_diesel_locomotives_available_enabled: Option, + #[serde(default)] + pub all_electric_locomotives_available_raw_u8: Option, + #[serde(default)] + pub all_electric_locomotives_available_enabled: Option, + #[serde(default)] + pub issue_37_value: Option, + #[serde(default)] + pub issue_38_value: Option, + #[serde(default)] + pub issue_39_value: Option, + #[serde(default)] + pub issue_3a_value: Option, + #[serde(default)] + pub issue_37_multiplier_raw_u32: Option, + #[serde(default)] + pub issue_37_multiplier_value_f32_text: Option, + #[serde(default)] + pub stock_issue_and_buyback_policy_raw_u8: Option, + #[serde(default)] + pub bond_issue_and_repayment_policy_raw_u8: Option, + #[serde(default)] + pub bankruptcy_policy_raw_u8: Option, + #[serde(default)] + pub dividend_policy_raw_u8: Option, + #[serde(default)] + pub building_density_growth_setting_raw_u32: Option, + #[serde(default)] + pub stock_issue_and_buyback_allowed: Option, + #[serde(default)] + pub bond_issue_and_repayment_allowed: Option, + #[serde(default)] + pub bankruptcy_allowed: Option, + #[serde(default)] + pub dividend_adjustment_allowed: Option, + #[serde(default)] + pub finance_neighborhood_candidates: Vec, + #[serde(default)] + pub economic_tuning_mirror_raw_u32: Option, + #[serde(default)] + pub economic_tuning_mirror_value_f32_text: Option, + #[serde(default)] + pub economic_tuning_lane_raw_u32: Vec, + #[serde(default)] + pub economic_tuning_lane_value_f32_text: Vec, + #[serde(default)] + pub cached_available_locomotive_rating_raw_u32: Option, + #[serde(default)] + pub cached_available_locomotive_rating_value_f32_text: Option, + #[serde(default)] + pub selected_year_bucket_scalar_raw_u32: Option, + #[serde(default)] + pub selected_year_bucket_scalar_value_f32_text: Option, + #[serde(default)] + pub selected_year_bucket_direct_lane_raw_u32: Vec, + #[serde(default)] + pub selected_year_bucket_direct_lane_value_f32_text: Vec, + #[serde(default)] + pub selected_year_bucket_complement_lane_raw_u32: Vec, + #[serde(default)] + pub selected_year_bucket_complement_lane_value_f32_text: Vec, + #[serde(default)] + pub selected_year_bucket_scaled_companion_lane_raw_u32: Vec, + #[serde(default)] + pub selected_year_bucket_scaled_companion_lane_value_f32_text: Vec, + #[serde(default)] + pub selected_year_gap_scalar_raw_u32: Option, + #[serde(default)] + pub selected_year_gap_scalar_value_f32_text: Option, + #[serde(default)] + pub absolute_counter_restore_kind: Option, + #[serde(default)] + pub absolute_counter_adjustment_context: Option, +} diff --git a/crates/rrt-runtime/src/state/entities/actors.rs b/crates/rrt-runtime/src/state/entities/actors.rs new file mode 100644 index 0000000..1c646e1 --- /dev/null +++ b/crates/rrt-runtime/src/state/entities/actors.rs @@ -0,0 +1,43 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use super::companies::RuntimeCompanyControllerKind; + +fn runtime_player_default_active() -> bool { + true +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePlayer { + pub player_id: u32, + pub current_cash: i64, + #[serde(default = "runtime_player_default_active")] + pub active: bool, + #[serde(default)] + pub controller_kind: RuntimeCompanyControllerKind, +} + +fn runtime_chairman_profile_default_active() -> bool { + true +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeChairmanProfile { + pub profile_id: u32, + pub name: String, + #[serde(default = "runtime_chairman_profile_default_active")] + pub active: bool, + #[serde(default)] + pub current_cash: i64, + #[serde(default)] + pub linked_company_id: Option, + #[serde(default)] + pub company_holdings: BTreeMap, + #[serde(default)] + pub holdings_value_total: i64, + #[serde(default)] + pub net_worth_total: i64, + #[serde(default)] + pub purchasing_power_total: i64, +} diff --git a/crates/rrt-runtime/src/state/entities/companies.rs b/crates/rrt-runtime/src/state/entities/companies.rs new file mode 100644 index 0000000..99854a1 --- /dev/null +++ b/crates/rrt-runtime/src/state/entities/companies.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeCompanyControllerKind { + #[default] + Unknown, + Human, + Ai, +} + +fn runtime_company_default_active() -> bool { + true +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompany { + pub company_id: u32, + pub current_cash: i64, + pub debt: u64, + #[serde(default)] + pub credit_rating_score: Option, + #[serde(default)] + pub prime_rate: Option, + #[serde(default = "runtime_company_default_active")] + pub active: bool, + #[serde(default)] + pub available_track_laying_capacity: Option, + #[serde(default)] + pub controller_kind: RuntimeCompanyControllerKind, + #[serde(default)] + pub linked_chairman_profile_id: Option, + #[serde(default)] + pub book_value_per_share: i64, + #[serde(default)] + pub investor_confidence: i64, + #[serde(default)] + pub management_attitude: i64, + #[serde(default)] + pub takeover_cooldown_year: Option, + #[serde(default)] + pub merger_cooldown_year: Option, + #[serde(default)] + pub track_piece_counts: RuntimeTrackPieceCounts, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct RuntimeTrackPieceCounts { + #[serde(default)] + pub total: u32, + #[serde(default)] + pub single: u32, + #[serde(default)] + pub double: u32, + #[serde(default)] + pub transition: u32, + #[serde(default)] + pub electric: u32, + #[serde(default)] + pub non_electric: u32, +} diff --git a/crates/rrt-runtime/src/state/entities/company_finance.rs b/crates/rrt-runtime/src/state/entities/company_finance.rs new file mode 100644 index 0000000..0f35c19 --- /dev/null +++ b/crates/rrt-runtime/src/state/entities/company_finance.rs @@ -0,0 +1,289 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualFinanceState { + pub company_id: u32, + pub outstanding_shares: u32, + pub bond_count: u8, + #[serde(default)] + pub largest_live_bond_principal: Option, + #[serde(default)] + pub highest_coupon_live_bond_principal: Option, + #[serde(default)] + pub live_bond_coupon_burden_total: Option, + pub assigned_share_pool: u32, + pub unassigned_share_pool: u32, + #[serde(default)] + pub cached_share_price: Option, + pub chairman_salary_baseline: u32, + pub chairman_salary_current: u32, + pub chairman_bonus_year: u32, + pub chairman_bonus_amount: i32, + pub founding_year: u32, + pub last_bankruptcy_year: u32, + pub last_dividend_year: u32, + #[serde(default)] + pub years_since_founding: Option, + #[serde(default)] + pub years_since_last_bankruptcy: Option, + #[serde(default)] + pub years_since_last_dividend: Option, + #[serde(default)] + pub current_partial_year_weight_numerator: Option, + #[serde(default)] + pub trailing_full_year_year_words: Vec, + #[serde(default)] + pub trailing_full_year_net_profits: Vec, + #[serde(default)] + pub trailing_full_year_revenues: Vec, + #[serde(default)] + pub trailing_full_year_fuel_costs: Vec, + #[serde(default)] + pub current_issue_absolute_counter: Option, + #[serde(default)] + pub prior_issue_absolute_counter: Option, + #[serde(default)] + pub current_issue_age_absolute_counter_delta: Option, + pub current_issue_calendar_word: u32, + #[serde(default)] + pub current_issue_calendar_word_2: u32, + pub prior_issue_calendar_word: u32, + #[serde(default)] + pub prior_issue_calendar_word_2: u32, + #[serde(default)] + pub preferred_locomotive_engine_type_raw_u8: Option, + pub city_connection_latch: bool, + pub linked_transit_latch: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualCreditorPressureState { + pub company_id: u32, + #[serde(default)] + pub annual_mode_active: Option, + #[serde(default)] + pub bankruptcy_allowed: Option, + #[serde(default)] + pub years_since_last_bankruptcy: Option, + #[serde(default)] + pub years_since_founding: Option, + pub recent_bad_net_profit_year_count: u32, + #[serde(default)] + pub recent_peak_revenue: Option, + #[serde(default)] + pub recent_three_year_net_profit_total: Option, + #[serde(default)] + pub pressure_ladder_cash_floor: Option, + #[serde(default)] + pub current_cash_plus_slot_12_total: Option, + #[serde(default)] + pub support_adjusted_share_price_floor: Option, + #[serde(default)] + pub support_adjusted_share_price_scalar: Option, + #[serde(default)] + pub current_fuel_cost: Option, + #[serde(default)] + pub current_fuel_cost_floor: Option, + pub eligible_for_bankruptcy_branch: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualDeepDistressState { + pub company_id: u32, + #[serde(default)] + pub bankruptcy_allowed: Option, + #[serde(default)] + pub years_since_founding: Option, + #[serde(default)] + pub years_since_last_bankruptcy: Option, + #[serde(default)] + pub current_cash: Option, + pub recent_first_three_net_profit_years: Vec, + #[serde(default)] + pub deep_distress_cash_floor: Option, + #[serde(default)] + pub deep_distress_net_profit_floor: Option, + pub eligible_for_bankruptcy_fallback: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualStockRepurchaseState { + pub company_id: u32, + #[serde(default)] + pub annual_mode_active: Option, + #[serde(default)] + pub stock_issue_and_buyback_allowed: Option, + pub city_connection_latch: bool, + #[serde(default)] + pub building_density_growth_setting: Option, + #[serde(default)] + pub linked_chairman_profile_id: Option, + #[serde(default)] + pub linked_chairman_personality_raw_u8: Option, + #[serde(default)] + pub repurchase_batch_size: Option, + #[serde(default)] + pub repurchase_factor_basis_points: Option, + #[serde(default)] + pub current_cash: Option, + #[serde(default)] + pub stock_value_gate_cash_floor: Option, + #[serde(default)] + pub support_adjusted_share_price_scalar: Option, + #[serde(default)] + pub affordability_cash_floor: Option, + #[serde(default)] + pub unassigned_share_pool: Option, + pub eligible_for_single_batch_repurchase: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualStockIssueState { + pub company_id: u32, + #[serde(default)] + pub annual_mode_active: Option, + #[serde(default)] + pub stock_issue_and_buyback_allowed: Option, + #[serde(default)] + pub bond_issue_and_repayment_allowed: Option, + #[serde(default)] + pub years_since_founding: Option, + #[serde(default)] + pub live_bond_count: Option, + #[serde(default)] + pub initial_issue_batch_size: Option, + #[serde(default)] + pub trimmed_issue_batch_size: Option, + #[serde(default)] + pub share_pressure_basis_points: Option, + #[serde(default)] + pub pressured_support_adjusted_share_price_scalar: Option, + #[serde(default)] + pub pressured_proceeds: Option, + #[serde(default)] + pub book_value_per_share_floor_applied: Option, + #[serde(default)] + pub price_to_book_ratio_basis_points: Option, + #[serde(default)] + pub current_cash: Option, + #[serde(default)] + pub highest_coupon_live_bond_principal: Option, + #[serde(default)] + pub highest_coupon_live_bond_rate_basis_points: Option, + #[serde(default)] + pub current_issue_age_absolute_counter_delta: Option, + #[serde(default)] + pub current_issue_cooldown_floor: Option, + #[serde(default)] + pub minimum_price_to_book_ratio_basis_points: Option, + #[serde(default)] + pub passes_share_price_floor: Option, + #[serde(default)] + pub passes_proceeds_floor: Option, + #[serde(default)] + pub passes_cash_gate: Option, + #[serde(default)] + pub passes_issue_cooldown_gate: Option, + #[serde(default)] + pub passes_coupon_price_to_book_gate: Option, + pub eligible_for_double_tranche_issue: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualBondPolicyState { + pub company_id: u32, + #[serde(default)] + pub annual_mode_active: Option, + #[serde(default)] + pub bond_issue_and_repayment_allowed: Option, + pub linked_transit_latch: bool, + #[serde(default)] + pub live_bond_count: Option, + #[serde(default)] + pub live_bond_principal_total: Option, + #[serde(default)] + pub matured_live_bond_count: Option, + #[serde(default)] + pub matured_live_bond_principal_total: Option, + #[serde(default)] + pub next_live_bond_maturity_year: Option, + #[serde(default)] + pub live_bond_coupon_burden_total: Option, + #[serde(default)] + pub current_cash: Option, + #[serde(default)] + pub cash_after_full_repayment: Option, + #[serde(default)] + pub issue_cash_floor: Option, + #[serde(default)] + pub issue_principal_step: Option, + #[serde(default)] + pub proposed_issue_bond_count: Option, + #[serde(default)] + pub proposed_issue_total_principal: Option, + #[serde(default)] + pub proposed_issue_years_to_maturity: Option, + pub eligible_for_bond_issue_branch: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualDividendPolicyState { + pub company_id: u32, + #[serde(default)] + pub annual_mode_active: Option, + #[serde(default)] + pub dividend_adjustment_allowed: Option, + #[serde(default)] + pub years_since_last_dividend: Option, + #[serde(default)] + pub years_since_founding: Option, + #[serde(default)] + pub outstanding_shares: Option, + #[serde(default)] + pub unassigned_share_pool: Option, + #[serde(default)] + pub weighted_recent_net_profit_total: Option, + #[serde(default)] + pub weighted_recent_net_profit_average: Option, + #[serde(default)] + pub current_cash: Option, + pub tiny_unassigned_share_cash_supplement_branch: bool, + #[serde(default)] + pub tentative_target_dividend_per_share_tenths: Option, + #[serde(default)] + pub current_dividend_per_share_tenths: Option, + #[serde(default)] + pub building_density_growth_setting: Option, + #[serde(default)] + pub growth_adjusted_current_dividend_per_share_tenths: Option, + #[serde(default)] + pub board_approved_dividend_rate_ceiling_tenths: Option, + #[serde(default)] + pub proposed_dividend_per_share_tenths: Option, + pub eligible_for_dividend_adjustment_branch: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, PartialOrd, Ord)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeCompanyAnnualFinancePolicyAction { + #[default] + None, + CreditorPressureBankruptcy, + DeepDistressBankruptcyFallback, + BondIssue, + StockRepurchase, + StockIssue, + DividendAdjustment, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualFinancePolicyState { + pub company_id: u32, + pub action: RuntimeCompanyAnnualFinancePolicyAction, + pub creditor_pressure_bankruptcy_eligible: bool, + pub deep_distress_bankruptcy_fallback_eligible: bool, + pub bond_issue_eligible: bool, + pub stock_repurchase_eligible: bool, + pub stock_issue_eligible: bool, + pub dividend_adjustment_eligible: bool, +} diff --git a/crates/rrt-runtime/src/state/entities/company_market.rs b/crates/rrt-runtime/src/state/entities/company_market.rs new file mode 100644 index 0000000..2c91c43 --- /dev/null +++ b/crates/rrt-runtime/src/state/entities/company_market.rs @@ -0,0 +1,123 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::state::RuntimeCompanyStatBandCandidate; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct RuntimeCompanyMarketState { + #[serde(default)] + pub outstanding_shares: u32, + #[serde(default)] + pub bond_count: u8, + #[serde(default)] + pub largest_live_bond_principal: Option, + #[serde(default)] + pub highest_coupon_live_bond_principal: Option, + #[serde(default)] + pub mutable_support_scalar_raw_u32: u32, + #[serde(default)] + pub young_company_support_scalar_raw_u32: u32, + #[serde(default)] + pub support_progress_word: u32, + #[serde(default)] + pub recent_per_share_cache_absolute_counter: u32, + #[serde(default)] + pub recent_per_share_cached_value_bits: u64, + #[serde(default)] + pub recent_per_share_subscore_raw_u32: u32, + #[serde(default)] + pub cached_share_price_raw_u32: u32, + #[serde(default)] + pub chairman_salary_baseline: u32, + #[serde(default)] + pub chairman_salary_current: u32, + #[serde(default)] + pub chairman_bonus_year: u32, + #[serde(default)] + pub chairman_bonus_amount: i32, + #[serde(default)] + pub founding_year: u32, + #[serde(default)] + pub last_bankruptcy_year: u32, + #[serde(default)] + pub last_dividend_year: u32, + #[serde(default)] + pub current_issue_calendar_word: u32, + #[serde(default)] + pub current_issue_calendar_word_2: u32, + #[serde(default)] + pub prior_issue_calendar_word: u32, + #[serde(default)] + pub prior_issue_calendar_word_2: u32, + #[serde(default)] + pub city_connection_latch: bool, + #[serde(default)] + pub linked_transit_latch: bool, + #[serde(default)] + pub linked_transit_route_anchor_entry_id: Option, + #[serde(default)] + pub linked_transit_route_anchor_fallback_counts: Vec, + #[serde(default)] + pub stat_band_root_0cfb_candidates: Vec, + #[serde(default)] + pub stat_band_root_0d7f_candidates: Vec, + #[serde(default)] + pub stat_band_root_1c47_candidates: Vec, + #[serde(default)] + pub year_stat_family_qword_bits: Vec, + #[serde(default)] + pub special_stat_family_232a_qword_bits: Vec, + #[serde(default)] + pub issue_opinion_terms_raw_i32: Vec, + #[serde(default)] + pub live_bond_slots: Vec, + #[serde(default)] + pub direct_control_transfer_float_fields_raw_u32: BTreeMap, + #[serde(default)] + pub direct_control_transfer_int_fields_raw_u32: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyBondSlot { + pub slot_index: u32, + pub principal: u32, + #[serde(default)] + pub maturity_year: u32, + pub coupon_rate_raw_u32: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct RuntimeCompanyPeriodicSideLatchState { + #[serde(default)] + pub preferred_locomotive_engine_type_raw_u8: Option, + #[serde(default)] + pub city_connection_latch: bool, + #[serde(default)] + pub linked_transit_latch: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyPeriodicServiceState { + pub company_id: u32, + #[serde(default)] + pub preferred_locomotive_engine_type_raw_u8: Option, + pub city_connection_latch: bool, + pub linked_transit_latch: bool, + #[serde(default)] + pub base_route_preference_raw_u8: Option, + #[serde(default)] + pub effective_route_preference_raw_u8: Option, + pub electric_route_preference_override_active: bool, + pub effective_route_quality_multiplier_basis_points: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeWorldRoutePreferenceOverrideState { + pub company_id: u32, + #[serde(default)] + pub base_route_preference_raw_u8: Option, + #[serde(default)] + pub effective_route_preference_raw_u8: Option, + pub electric_route_preference_override_active: bool, +} diff --git a/crates/rrt-runtime/src/state/entities/mod.rs b/crates/rrt-runtime/src/state/entities/mod.rs new file mode 100644 index 0000000..7ce6c5f --- /dev/null +++ b/crates/rrt-runtime/src/state/entities/mod.rs @@ -0,0 +1,15 @@ +mod actors; +mod companies; +mod company_finance; +mod company_market; +mod near_city; +mod territories; +mod trains; + +pub use actors::*; +pub use companies::*; +pub use company_finance::*; +pub use company_market::*; +pub use near_city::*; +pub use territories::*; +pub use trains::*; diff --git a/crates/rrt-runtime/src/state/entities/near_city.rs b/crates/rrt-runtime/src/state/entities/near_city.rs new file mode 100644 index 0000000..4401af2 --- /dev/null +++ b/crates/rrt-runtime/src/state/entities/near_city.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeNearCityAcquisitionValueProvenance { + Grounded, + #[default] + BestEffortGuess, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeNearCityAcquisitionRegion { + pub region_id: u32, + pub name: String, + #[serde(default)] + pub profile_names: Vec, + #[serde(default)] + pub fixed_row_shape_family_signature: Option, + #[serde(default)] + pub fixed_row_best_density_lane_relative_offset_hex: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeNearCityAcquisitionSite { + pub site_id: u32, + pub primary_name: String, + pub secondary_name: String, + #[serde(default)] + pub region_id: Option, + #[serde(default)] + pub owner_company_id: Option, + #[serde(default)] + pub preferred_company_id: Option, + #[serde(default)] + pub owner_company_id_provenance: RuntimeNearCityAcquisitionValueProvenance, + pub self_id: u32, + #[serde(default)] + pub self_id_provenance: RuntimeNearCityAcquisitionValueProvenance, + pub candidate_subtype_label: String, + #[serde(default)] + pub candidate_subtype_provenance: RuntimeNearCityAcquisitionValueProvenance, + #[serde(default)] + pub cached_tri_lane_0: u32, + #[serde(default)] + pub cached_tri_lane_1: u32, + #[serde(default)] + pub cached_tri_lane_2: u32, + #[serde(default)] + pub cached_tri_lane_provenance: RuntimeNearCityAcquisitionValueProvenance, + #[serde(default)] + pub nontransport_source_label: String, + #[serde(default)] + pub tri_lane_source_label: String, +} diff --git a/crates/rrt-runtime/src/state/entities/territories.rs b/crates/rrt-runtime/src/state/entities/territories.rs new file mode 100644 index 0000000..bcb8a4c --- /dev/null +++ b/crates/rrt-runtime/src/state/entities/territories.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +use super::companies::RuntimeTrackPieceCounts; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeTerritory { + pub territory_id: u32, + #[serde(default)] + pub name: Option, + #[serde(default)] + pub track_piece_counts: RuntimeTrackPieceCounts, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyTerritoryTrackPieceCount { + pub company_id: u32, + pub territory_id: u32, + #[serde(default)] + pub track_piece_counts: RuntimeTrackPieceCounts, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyTerritoryAccess { + pub company_id: u32, + pub territory_id: u32, +} diff --git a/crates/rrt-runtime/src/state/entities/trains.rs b/crates/rrt-runtime/src/state/entities/trains.rs new file mode 100644 index 0000000..3d7c403 --- /dev/null +++ b/crates/rrt-runtime/src/state/entities/trains.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +use crate::event::targets::RuntimeCargoClass; + +fn runtime_train_default_active() -> bool { + true +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeTrain { + pub train_id: u32, + pub owner_company_id: u32, + #[serde(default)] + pub territory_id: Option, + #[serde(default)] + pub locomotive_name: Option, + #[serde(default = "runtime_train_default_active")] + pub active: bool, + #[serde(default)] + pub retired: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeLocomotiveCatalogEntry { + pub locomotive_id: u32, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCargoCatalogEntry { + pub slot_id: u32, + pub label: String, + #[serde(default)] + pub cargo_class: RuntimeCargoClass, + #[serde(default)] + pub supplied_token_stem: Option, + #[serde(default)] + pub demanded_token_stem: Option, +} diff --git a/crates/rrt-runtime/src/state/mod.rs b/crates/rrt-runtime/src/state/mod.rs new file mode 100644 index 0000000..a562c26 --- /dev/null +++ b/crates/rrt-runtime/src/state/mod.rs @@ -0,0 +1,7 @@ +pub use crate::calendar::CalendarPoint; + +mod core; +mod entities; + +pub use core::*; +pub use entities::*; diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs deleted file mode 100644 index 3ebdf55..0000000 --- a/crates/rrt-runtime/src/step.rs +++ /dev/null @@ -1,7930 +0,0 @@ -use std::collections::BTreeMap; -use std::collections::BTreeSet; - -use serde::{Deserialize, Serialize}; - -use crate::runtime::{ - RuntimeAnnualFinanceNewsEvent, runtime_company_bond_interest_rate_quote_f64, - runtime_company_support_adjusted_share_price_scalar_with_pressure_f64, - runtime_round_f64_to_i64, -}; -use crate::{ - CalendarPoint, RUNTIME_COMPANY_STAT_SLOT_COUNT, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN, RuntimeCargoClass, RuntimeCargoPriceTarget, - RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanTarget, - RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, - RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget, - RuntimeState, RuntimeSummary, RuntimeTerritoryMetric, RuntimeTerritoryTarget, - RuntimeTrackMetric, RuntimeTrackPieceCounts, calendar::BoundaryEventKind, - runtime_annual_finance_news_family_candidate_label, - runtime_company_annual_dividend_policy_state, runtime_company_annual_finance_policy_state, - runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state, - runtime_company_book_value_per_share, runtime_company_credit_rating, - runtime_company_investor_confidence, runtime_company_management_attitude, - runtime_company_prime_rate, -}; - -const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4]; -const COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT: u32 = 0x33f; -const COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE: f64 = -0.02; -const COMPANY_REPURCHASE_PRESSURE_SCALE: f64 = 0.7; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum StepCommand { - AdvanceTo { calendar: crate::CalendarPoint }, - StepCount { steps: u32 }, - ServiceTriggerKind { trigger_kind: u8 }, - ServicePeriodicBoundary, -} - -impl StepCommand { - pub fn validate(&self) -> Result<(), String> { - match self { - Self::AdvanceTo { calendar } => calendar.validate(), - Self::StepCount { steps } => { - if *steps == 0 { - return Err("step_count command requires steps > 0".to_string()); - } - Ok(()) - } - Self::ServiceTriggerKind { .. } | Self::ServicePeriodicBoundary => Ok(()), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BoundaryEvent { - pub kind: String, - pub calendar: crate::CalendarPoint, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ServiceEvent { - pub kind: String, - pub trigger_kind: Option, - pub serviced_record_ids: Vec, - pub applied_effect_count: u32, - pub mutated_company_ids: Vec, - pub mutated_player_ids: Vec, - pub appended_record_ids: Vec, - pub activated_record_ids: Vec, - pub deactivated_record_ids: Vec, - pub removed_record_ids: Vec, - #[serde(default)] - pub finance_news_family_candidates: BTreeMap, - #[serde(default)] - pub annual_finance_news_events: Vec, - pub dirty_rerun: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct StepResult { - pub initial_summary: RuntimeSummary, - pub final_summary: RuntimeSummary, - pub steps_executed: u64, - pub boundary_events: Vec, - pub service_events: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum EventGraphMutation { - Append(RuntimeEventRecordTemplate), - Activate { record_id: u32 }, - Deactivate { record_id: u32 }, - Remove { record_id: u32 }, -} - -#[derive(Debug, Default)] -struct AppliedEffectsSummary { - applied_effect_count: u32, - appended_record_ids: Vec, - activated_record_ids: Vec, - deactivated_record_ids: Vec, - removed_record_ids: Vec, -} - -#[derive(Debug, Default)] -struct ResolvedConditionContext { - matching_company_ids: BTreeSet, - matching_player_ids: BTreeSet, - matching_chairman_profile_ids: BTreeSet, -} - -pub fn execute_step_command( - state: &mut RuntimeState, - command: &StepCommand, -) -> Result { - state.validate()?; - command.validate()?; - - let initial_summary = RuntimeSummary::from_state(state); - let mut boundary_events = Vec::new(); - let mut service_events = Vec::new(); - let steps_executed = match command { - StepCommand::AdvanceTo { calendar } => advance_to_target_calendar_point( - state, - *calendar, - &mut boundary_events, - &mut service_events, - )?, - StepCommand::StepCount { steps } => { - step_count(state, *steps, &mut boundary_events, &mut service_events)? - } - StepCommand::ServiceTriggerKind { trigger_kind } => { - service_trigger_kind(state, *trigger_kind, &mut service_events)?; - 0 - } - StepCommand::ServicePeriodicBoundary => { - service_periodic_boundary(state, &mut service_events)?; - 0 - } - }; - state.refresh_derived_world_state(); - state.refresh_derived_market_state(); - let final_summary = RuntimeSummary::from_state(state); - - Ok(StepResult { - initial_summary, - final_summary, - steps_executed, - boundary_events, - service_events, - }) -} - -fn advance_to_target_calendar_point( - state: &mut RuntimeState, - target: crate::CalendarPoint, - boundary_events: &mut Vec, - service_events: &mut Vec, -) -> Result { - target.validate()?; - if target < state.calendar { - return Err(format!( - "advance_to target {:?} is earlier than current calendar {:?}", - target, state.calendar - )); - } - - let mut steps = 0_u64; - while state.calendar < target { - step_once(state, boundary_events, service_events)?; - steps += 1; - } - Ok(steps) -} - -fn step_count( - state: &mut RuntimeState, - steps: u32, - boundary_events: &mut Vec, - service_events: &mut Vec, -) -> Result { - for _ in 0..steps { - step_once(state, boundary_events, service_events)?; - } - Ok(steps.into()) -} - -fn step_once( - state: &mut RuntimeState, - boundary_events: &mut Vec, - service_events: &mut Vec, -) -> Result<(), String> { - let prior_calendar = state.calendar; - let boundary = state.calendar.step_forward(); - if boundary != BoundaryEventKind::Tick { - boundary_events.push(BoundaryEvent { - kind: boundary_kind_label(boundary).to_string(), - calendar: state.calendar, - }); - } - if boundary == BoundaryEventKind::YearRollover { - service_sync_world_restore_time_from_calendar(state, prior_calendar); - service_periodic_boundary(state, service_events)?; - } - service_sync_world_restore_time_from_calendar(state, state.calendar); - Ok(()) -} - -fn boundary_kind_label(boundary: BoundaryEventKind) -> &'static str { - match boundary { - BoundaryEventKind::Tick => "tick", - BoundaryEventKind::PhaseRollover => "phase_rollover", - BoundaryEventKind::MonthRollover => "month_rollover", - BoundaryEventKind::YearRollover => "year_rollover", - } -} - -fn service_sync_world_restore_time_from_calendar( - state: &mut RuntimeState, - calendar: CalendarPoint, -) { - if let Ok(year_word) = u16::try_from(calendar.year) { - state.world_restore.packed_year_word_raw_u16 = Some(year_word); - } - if let Ok(partial_year_progress) = u8::try_from(calendar.month_slot.saturating_add(1)) { - state.world_restore.partial_year_progress_raw_u8 = Some(partial_year_progress); - } - if let Some(tuple) = - crate::runtime::runtime_derive_packed_calendar_tuple_from_calendar_point(calendar) - { - let (word_0, word_1) = crate::runtime::runtime_encode_packed_calendar_tuple(tuple); - state.world_restore.current_calendar_tuple_word_raw_u32 = Some(word_0); - state.world_restore.current_calendar_tuple_word_2_raw_u32 = Some(word_1); - if let Some(absolute_counter) = - crate::runtime::runtime_pack_packed_calendar_tuple_to_absolute_counter(tuple) - { - state.world_restore.absolute_counter_raw_u32 = Some(absolute_counter); - state.world_restore.absolute_counter_mirror_raw_u32 = Some(absolute_counter); - } - } -} - -fn service_periodic_boundary( - state: &mut RuntimeState, - service_events: &mut Vec, -) -> Result<(), String> { - state.service_state.periodic_boundary_calls += 1; - service_refresh_company_periodic_side_latch_state(state); - - for trigger_kind in PERIODIC_TRIGGER_KIND_ORDER { - service_trigger_kind(state, trigger_kind, service_events)?; - } - service_company_annual_finance_policy(state, service_events)?; - - Ok(()) -} - -fn service_refresh_company_periodic_side_latch_state(state: &mut RuntimeState) { - let active_company_ids = state - .companies - .iter() - .filter(|company| company.active) - .map(|company| company.company_id) - .collect::>(); - state - .service_state - .company_periodic_side_latch_state - .retain(|company_id, _| active_company_ids.contains(company_id)); - - for company_id in active_company_ids { - match ( - state - .service_state - .company_periodic_side_latch_state - .get_mut(&company_id), - state.service_state.company_market_state.get(&company_id), - ) { - (Some(latch_state), Some(market_state)) => { - latch_state.preferred_locomotive_engine_type_raw_u8 = None; - latch_state.city_connection_latch = market_state.city_connection_latch; - latch_state.linked_transit_latch = market_state.linked_transit_latch; - } - (Some(latch_state), None) => { - latch_state.preferred_locomotive_engine_type_raw_u8 = None; - } - (None, Some(market_state)) => { - state - .service_state - .company_periodic_side_latch_state - .insert( - company_id, - crate::RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: None, - city_connection_latch: market_state.city_connection_latch, - linked_transit_latch: market_state.linked_transit_latch, - }, - ); - } - (None, None) => {} - } - } -} - -fn service_decode_saved_f64_bits(raw_bits: u64) -> Option { - let value = f64::from_bits(raw_bits); - value.is_finite().then_some(value) -} - -fn service_ensure_company_stat_post_capacity( - market_state: &mut crate::RuntimeCompanyMarketState, - slot_id: u32, -) -> Option { - let index = slot_id - .checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? - .try_into() - .ok()?; - let required_year_len = - ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - if market_state.year_stat_family_qword_bits.len() < required_year_len { - market_state - .year_stat_family_qword_bits - .resize(required_year_len, 0.0f64.to_bits()); - } - let required_special_len = RUNTIME_COMPANY_STAT_SLOT_COUNT as usize; - if market_state.special_stat_family_232a_qword_bits.len() < required_special_len { - market_state - .special_stat_family_232a_qword_bits - .resize(required_special_len, 0.0f64.to_bits()); - } - Some(index) -} - -fn service_post_company_stat_delta( - state: &mut RuntimeState, - company_id: u32, - slot_id: u32, - delta: f64, - mirror_cash_totals: bool, -) -> bool { - if !delta.is_finite() { - return false; - } - - let Some(refreshed_current_cash) = ({ - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - return false; - }; - let Some(index) = service_ensure_company_stat_post_capacity(market_state, slot_id) else { - return false; - }; - let prior_year_value = market_state - .year_stat_family_qword_bits - .get(index) - .copied() - .and_then(service_decode_saved_f64_bits) - .unwrap_or(0.0); - market_state.year_stat_family_qword_bits[index] = (prior_year_value + delta).to_bits(); - - let special_index = slot_id as usize; - let prior_special_value = market_state - .special_stat_family_232a_qword_bits - .get(special_index) - .copied() - .and_then(service_decode_saved_f64_bits) - .unwrap_or(0.0); - market_state.special_stat_family_232a_qword_bits[special_index] = - (prior_special_value + delta).to_bits(); - - if mirror_cash_totals { - let cash_index = RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize; - let prior_cash_shadow_value = market_state - .special_stat_family_232a_qword_bits - .get(cash_index) - .copied() - .and_then(service_decode_saved_f64_bits) - .unwrap_or(0.0); - market_state.special_stat_family_232a_qword_bits[cash_index] = - (prior_cash_shadow_value + delta).to_bits(); - } - - market_state - .year_stat_family_qword_bits - .get( - (RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize, - ) - .copied() - .and_then(service_decode_saved_f64_bits) - .and_then(runtime_round_f64_to_i64) - }) else { - return false; - }; - - if let Some(company) = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - { - company.current_cash = refreshed_current_cash; - true - } else { - false - } -} - -fn service_set_company_direct_float_field( - state: &mut RuntimeState, - company_id: u32, - field_offset: u32, - value: f64, -) -> bool { - if !value.is_finite() { - return false; - } - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - return false; - }; - market_state - .direct_control_transfer_float_fields_raw_u32 - .insert(field_offset, (value as f32).to_bits()); - true -} - -fn service_set_company_cached_share_price( - state: &mut RuntimeState, - company_id: u32, - value: f64, -) -> bool { - if !value.is_finite() { - return false; - } - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - return false; - }; - market_state.cached_share_price_raw_u32 = (value as f32).to_bits(); - true -} - -fn service_set_company_issue_opinion_total( - state: &mut RuntimeState, - company_id: u32, - issue_id: u32, - target_total: i64, -) -> bool { - let current_total = crate::runtime::runtime_world_issue_opinion_term_sum_raw( - state, - issue_id, - state - .companies - .iter() - .find(|company| company.company_id == company_id) - .and_then(|company| company.linked_chairman_profile_id), - Some(company_id), - None, - ) - .unwrap_or(0); - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - return false; - }; - let issue_index = issue_id as usize; - if market_state.issue_opinion_terms_raw_i32.len() <= issue_index { - market_state - .issue_opinion_terms_raw_i32 - .resize(issue_index + 1, 0); - } - let prior_company_term = i64::from(market_state.issue_opinion_terms_raw_i32[issue_index]); - let next_company_term = prior_company_term.saturating_add(target_total - current_total); - let Ok(next_company_term_i32) = i32::try_from(next_company_term) else { - return false; - }; - market_state.issue_opinion_terms_raw_i32[issue_index] = next_company_term_i32; - true -} - -fn service_set_company_prime_rate_target( - state: &mut RuntimeState, - company_id: u32, - value: i64, -) -> bool { - let Some(baseline) = crate::runtime::runtime_world_prime_rate_baseline(state) else { - return false; - }; - let target_raw_sum = ((value as f64 - baseline) * 100.0).round(); - if !target_raw_sum.is_finite() { - return false; - } - service_set_company_issue_opinion_total( - state, - company_id, - crate::RUNTIME_WORLD_ISSUE_PRIME_RATE, - target_raw_sum as i64, - ) -} - -fn service_set_company_credit_rating_target( - state: &mut RuntimeState, - company_id: u32, - value: i64, -) -> bool { - let Some(current_rating) = crate::runtime::runtime_company_credit_rating(state, company_id) - else { - return false; - }; - let current_issue_total = crate::runtime::runtime_world_issue_opinion_term_sum_raw( - state, - crate::RUNTIME_WORLD_ISSUE_CREDIT_MARKET, - state - .companies - .iter() - .find(|company| company.company_id == company_id) - .and_then(|company| company.linked_chairman_profile_id), - Some(company_id), - None, - ) - .unwrap_or(0); - service_set_company_issue_opinion_total( - state, - company_id, - crate::RUNTIME_WORLD_ISSUE_CREDIT_MARKET, - current_issue_total.saturating_add(value.saturating_sub(current_rating)), - ) -} - -fn service_zero_company_current_cash(state: &mut RuntimeState, company_id: u32) -> bool { - let Some(current_cash) = crate::runtime::runtime_company_stat_value( - state, - company_id, - crate::RuntimeCompanyStatSelector { - family_id: crate::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - }, - ) else { - return false; - }; - if current_cash == 0 { - return true; - } - service_post_company_stat_delta( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - -(current_cash as f64), - false, - ) -} - -fn service_clear_company_live_bonds(state: &mut RuntimeState, company_id: u32) -> bool { - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - return false; - }; - market_state.live_bond_slots.clear(); - market_state.bond_count = 0; - market_state.largest_live_bond_principal = None; - market_state.highest_coupon_live_bond_principal = None; - true -} - -fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool { - let Some(bankruptcy_year) = state - .world_restore - .packed_year_word_raw_u16 - .map(u32::from) - .or_else(|| Some(state.calendar.year)) - else { - return false; - }; - - let mut company_mutated = false; - if let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - { - market_state.last_bankruptcy_year = bankruptcy_year; - for slot in &mut market_state.live_bond_slots { - slot.principal /= 2; - } - market_state - .live_bond_slots - .retain(|slot| slot.principal > 0); - market_state.bond_count = market_state.live_bond_slots.len().min(u8::MAX as usize) as u8; - market_state.largest_live_bond_principal = market_state - .live_bond_slots - .iter() - .map(|slot| slot.principal) - .max(); - market_state.highest_coupon_live_bond_principal = market_state - .live_bond_slots - .iter() - .filter_map(|slot| { - let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; - coupon.is_finite().then_some((coupon, slot.principal)) - }) - .max_by(|left, right| { - left.0 - .partial_cmp(&right.0) - .unwrap_or(std::cmp::Ordering::Equal) - }) - .map(|(_, principal)| principal); - company_mutated = true; - } - - let remaining_debt = state - .service_state - .company_market_state - .get(&company_id) - .map(|market_state| { - market_state - .live_bond_slots - .iter() - .map(|slot| u64::from(slot.principal)) - .sum::() - }); - if let Some(company) = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - { - if let Some(remaining_debt) = remaining_debt { - company.debt = remaining_debt.min(u64::from(u32::MAX)) as u64; - } - company_mutated = true; - } - - company_mutated |= service_zero_company_current_cash(state, company_id); - - company_mutated -} - -fn service_repay_matured_company_live_bonds_and_compact( - state: &mut RuntimeState, - company_id: u32, -) -> Option { - let Some(current_year_word) = state - .world_restore - .packed_year_word_raw_u16 - .map(u32::from) - .or_else(|| Some(state.calendar.year)) - else { - return None; - }; - let retired_principal_total = state - .service_state - .company_market_state - .get(&company_id) - .map(|market_state| { - market_state - .live_bond_slots - .iter() - .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) - .map(|slot| u64::from(slot.principal)) - .sum::() - }) - .unwrap_or(0); - if retired_principal_total == 0 { - return None; - } - - let retired_principal_total_f64 = retired_principal_total as f64; - let mut company_mutated = false; - company_mutated |= service_post_company_stat_delta( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - -retired_principal_total_f64, - false, - ); - company_mutated |= service_post_company_stat_delta( - state, - company_id, - 0x12, - retired_principal_total_f64, - false, - ); - - if let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - { - market_state - .live_bond_slots - .retain(|slot| slot.maturity_year == 0 || slot.maturity_year > current_year_word); - for (slot_index, slot) in market_state.live_bond_slots.iter_mut().enumerate() { - slot.slot_index = slot_index as u32; - } - market_state.bond_count = market_state.live_bond_slots.len().min(u8::MAX as usize) as u8; - market_state.largest_live_bond_principal = market_state - .live_bond_slots - .iter() - .map(|slot| slot.principal) - .max(); - market_state.highest_coupon_live_bond_principal = market_state - .live_bond_slots - .iter() - .filter_map(|slot| { - let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; - coupon.is_finite().then_some((coupon, slot.principal)) - }) - .max_by(|left, right| { - left.0 - .partial_cmp(&right.0) - .unwrap_or(std::cmp::Ordering::Equal) - }) - .map(|(_, principal)| principal); - company_mutated = true; - } - - if let Some(company) = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - { - company.debt = state - .service_state - .company_market_state - .get(&company_id) - .map(|market_state| { - market_state - .live_bond_slots - .iter() - .map(|slot| u64::from(slot.principal)) - .sum::() - }) - .unwrap_or(0); - company_mutated = true; - } - - company_mutated.then_some(retired_principal_total) -} - -fn service_company_annual_finance_policy( - state: &mut RuntimeState, - service_events: &mut Vec, -) -> Result<(), String> { - let active_company_ids = state - .companies - .iter() - .filter(|company| company.active) - .map(|company| company.company_id) - .collect::>(); - let active_company_id_set = active_company_ids.iter().copied().collect::>(); - state - .service_state - .annual_finance_last_actions - .retain(|company_id, _| active_company_id_set.contains(company_id)); - state - .service_state - .annual_finance_last_news_family_candidates - .retain(|company_id, _| active_company_id_set.contains(company_id)); - state.service_state.annual_finance_last_news_events.clear(); - state.service_state.annual_finance_service_calls += 1; - state.service_state.annual_bond_last_retired_principal_total = 0; - state.service_state.annual_bond_last_issued_principal_total = 0; - state.service_state.annual_stock_repurchase_last_share_count = 0; - state.service_state.annual_stock_issue_last_share_count = 0; - - let mut mutated_company_ids = BTreeSet::new(); - let mut applied_effect_count = 0u32; - let mut finance_news_family_candidates = BTreeMap::new(); - let mut annual_finance_news_events = Vec::new(); - - for company_id in active_company_ids { - let Some(policy_state) = runtime_company_annual_finance_policy_state(state, company_id) - else { - continue; - }; - state - .service_state - .annual_finance_last_actions - .insert(company_id, policy_state.action); - state - .service_state - .annual_finance_last_news_family_candidates - .remove(&company_id); - *state - .service_state - .annual_finance_action_counts - .entry(policy_state.action) - .or_insert(0) += 1; - - match policy_state.action { - crate::RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment => { - let Some(dividend_state) = - runtime_company_annual_dividend_policy_state(state, company_id) - else { - continue; - }; - let Some(proposed_tenths) = dividend_state.proposed_dividend_per_share_tenths - else { - continue; - }; - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - continue; - }; - let raw_bits = ((proposed_tenths as f32) / 10.0).to_bits(); - let prior_bits = market_state - .direct_control_transfer_float_fields_raw_u32 - .insert(COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT, raw_bits); - if prior_bits != Some(raw_bits) { - applied_effect_count += 1; - state.service_state.annual_dividend_adjustment_commit_count += 1; - mutated_company_ids.insert(company_id); - } - } - crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy - | crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => { - if service_apply_company_bankruptcy(state, company_id) { - if let Some(label) = runtime_annual_finance_news_family_candidate_label( - policy_state.action, - state.service_state.annual_bond_last_retired_principal_total, - state.service_state.annual_bond_last_issued_principal_total, - state.service_state.annual_stock_repurchase_last_share_count, - state.service_state.annual_stock_issue_last_share_count, - ) { - state - .service_state - .annual_finance_last_news_family_candidates - .insert(company_id, label.to_string()); - finance_news_family_candidates.insert(company_id, label.to_string()); - let news_event = RuntimeAnnualFinanceNewsEvent { - company_id, - selector_label: label.to_string(), - action_label: - crate::runtime::runtime_company_annual_finance_policy_action_label( - policy_state.action, - ) - .to_string(), - retired_principal_total: state - .service_state - .annual_bond_last_retired_principal_total, - issued_principal_total: state - .service_state - .annual_bond_last_issued_principal_total, - repurchased_share_count: state - .service_state - .annual_stock_repurchase_last_share_count, - issued_share_count: state - .service_state - .annual_stock_issue_last_share_count, - }; - state - .service_state - .annual_finance_last_news_events - .push(news_event.clone()); - annual_finance_news_events.push(news_event); - } - applied_effect_count += 1; - mutated_company_ids.insert(company_id); - } - } - crate::RuntimeCompanyAnnualFinancePolicyAction::StockIssue => { - let Some(issue_state) = runtime_company_annual_stock_issue_state(state, company_id) - else { - continue; - }; - let Some(batch_size) = issue_state.trimmed_issue_batch_size else { - continue; - }; - let Some(proceeds_per_tranche) = issue_state.pressured_proceeds else { - continue; - }; - let Some(current_tuple_word_0) = - state.world_restore.current_calendar_tuple_word_raw_u32 - else { - continue; - }; - let Some(current_tuple_word_1) = - state.world_restore.current_calendar_tuple_word_2_raw_u32 - else { - continue; - }; - let Some(total_share_delta) = batch_size.checked_mul(2) else { - continue; - }; - let Some(next_outstanding_shares) = state - .service_state - .company_market_state - .get(&company_id) - .map(|market_state| market_state.outstanding_shares) - .and_then(|value| value.checked_add(total_share_delta)) - else { - continue; - }; - let mut mutated = false; - for _ in 0..2 { - mutated |= service_post_company_stat_delta( - state, - company_id, - 0x0c, - (batch_size as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, - true, - ); - mutated |= service_post_company_stat_delta( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - proceeds_per_tranche as f64, - false, - ); - state.service_state.annual_stock_issue_last_share_count = state - .service_state - .annual_stock_issue_last_share_count - .saturating_add(u64::from(batch_size)); - } - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - continue; - }; - market_state.outstanding_shares = next_outstanding_shares; - market_state.prior_issue_calendar_word = market_state.current_issue_calendar_word; - market_state.prior_issue_calendar_word_2 = - market_state.current_issue_calendar_word_2; - market_state.current_issue_calendar_word = current_tuple_word_0; - market_state.current_issue_calendar_word_2 = current_tuple_word_1; - if mutated { - if let Some(label) = runtime_annual_finance_news_family_candidate_label( - policy_state.action, - state.service_state.annual_bond_last_retired_principal_total, - state.service_state.annual_bond_last_issued_principal_total, - state.service_state.annual_stock_repurchase_last_share_count, - state.service_state.annual_stock_issue_last_share_count, - ) { - state - .service_state - .annual_finance_last_news_family_candidates - .insert(company_id, label.to_string()); - finance_news_family_candidates.insert(company_id, label.to_string()); - let news_event = RuntimeAnnualFinanceNewsEvent { - company_id, - selector_label: label.to_string(), - action_label: - crate::runtime::runtime_company_annual_finance_policy_action_label( - policy_state.action, - ) - .to_string(), - retired_principal_total: state - .service_state - .annual_bond_last_retired_principal_total, - issued_principal_total: state - .service_state - .annual_bond_last_issued_principal_total, - repurchased_share_count: state - .service_state - .annual_stock_repurchase_last_share_count, - issued_share_count: state - .service_state - .annual_stock_issue_last_share_count, - }; - state - .service_state - .annual_finance_last_news_events - .push(news_event.clone()); - annual_finance_news_events.push(news_event); - } - applied_effect_count += 1; - mutated_company_ids.insert(company_id); - } - } - crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue => { - let mut mutated = false; - let Some(bond_state) = - crate::runtime::runtime_company_annual_bond_policy_state(state, company_id) - else { - continue; - }; - if !bond_state.eligible_for_bond_issue_branch { - continue; - } - let retired_principal_total = - service_repay_matured_company_live_bonds_and_compact(state, company_id) - .unwrap_or(0); - mutated |= retired_principal_total > 0; - state.service_state.annual_bond_last_retired_principal_total = state - .service_state - .annual_bond_last_retired_principal_total - .saturating_add(retired_principal_total); - - let issue_bond_count = bond_state.proposed_issue_bond_count.unwrap_or(0); - let Some(principal) = bond_state.issue_principal_step else { - if mutated { - applied_effect_count += 1; - mutated_company_ids.insert(company_id); - } - continue; - }; - let Some(years_to_maturity) = bond_state.proposed_issue_years_to_maturity else { - if mutated { - applied_effect_count += 1; - mutated_company_ids.insert(company_id); - } - continue; - }; - let Some(maturity_year) = state - .world_restore - .packed_year_word_raw_u16 - .map(u32::from) - .and_then(|year| year.checked_add(years_to_maturity)) - else { - if mutated { - applied_effect_count += 1; - mutated_company_ids.insert(company_id); - } - continue; - }; - - for _ in 0..issue_bond_count { - let Some(quote_rate) = runtime_company_bond_interest_rate_quote_f64( - state, - company_id, - principal, - years_to_maturity, - ) else { - break; - }; - - mutated |= service_post_company_stat_delta( - state, - company_id, - 0x0c, - (principal as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, - true, - ); - mutated |= service_post_company_stat_delta( - state, - company_id, - 0x12, - -(principal as f64), - false, - ); - mutated |= service_post_company_stat_delta( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - principal as f64, - false, - ); - - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - break; - }; - let slot_index = market_state.bond_count as u32; - if market_state.bond_count == u8::MAX { - break; - } - market_state - .live_bond_slots - .push(crate::RuntimeCompanyBondSlot { - slot_index, - principal, - maturity_year, - coupon_rate_raw_u32: (quote_rate as f32).to_bits(), - }); - market_state.bond_count = market_state.bond_count.saturating_add(1); - market_state.largest_live_bond_principal = Some( - market_state - .largest_live_bond_principal - .unwrap_or(0) - .max(principal), - ); - let highest_coupon_live_principal = market_state - .live_bond_slots - .iter() - .filter_map(|slot| { - let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; - coupon.is_finite().then_some((coupon, slot.principal)) - }) - .max_by(|left, right| { - left.0 - .partial_cmp(&right.0) - .unwrap_or(std::cmp::Ordering::Equal) - }) - .map(|(_, principal)| principal); - market_state.highest_coupon_live_bond_principal = highest_coupon_live_principal; - state.service_state.annual_bond_last_issued_principal_total = state - .service_state - .annual_bond_last_issued_principal_total - .saturating_add(u64::from(principal)); - } - if let Some(company) = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - { - company.debt = state - .service_state - .company_market_state - .get(&company_id) - .map(|market_state| { - market_state - .live_bond_slots - .iter() - .map(|slot| u64::from(slot.principal)) - .sum::() - }) - .unwrap_or(company.debt); - } - if mutated { - if let Some(label) = runtime_annual_finance_news_family_candidate_label( - policy_state.action, - state.service_state.annual_bond_last_retired_principal_total, - state.service_state.annual_bond_last_issued_principal_total, - state.service_state.annual_stock_repurchase_last_share_count, - state.service_state.annual_stock_issue_last_share_count, - ) { - state - .service_state - .annual_finance_last_news_family_candidates - .insert(company_id, label.to_string()); - finance_news_family_candidates.insert(company_id, label.to_string()); - let news_event = RuntimeAnnualFinanceNewsEvent { - company_id, - selector_label: label.to_string(), - action_label: - crate::runtime::runtime_company_annual_finance_policy_action_label( - policy_state.action, - ) - .to_string(), - retired_principal_total: state - .service_state - .annual_bond_last_retired_principal_total, - issued_principal_total: state - .service_state - .annual_bond_last_issued_principal_total, - repurchased_share_count: state - .service_state - .annual_stock_repurchase_last_share_count, - issued_share_count: state - .service_state - .annual_stock_issue_last_share_count, - }; - state - .service_state - .annual_finance_last_news_events - .push(news_event.clone()); - annual_finance_news_events.push(news_event); - } - applied_effect_count += 1; - mutated_company_ids.insert(company_id); - } - } - crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => { - let mut mutated = false; - for _ in 0..128 { - let Some(repurchase_state) = - runtime_company_annual_stock_repurchase_state(state, company_id) - else { - break; - }; - if !repurchase_state.eligible_for_single_batch_repurchase { - break; - } - let Some(batch_size) = repurchase_state.repurchase_batch_size else { - break; - }; - let Some(pressure_shares) = runtime_round_f64_to_i64( - batch_size as f64 * COMPANY_REPURCHASE_PRESSURE_SCALE, - ) else { - break; - }; - let Some(share_price_scalar) = - runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( - state, - company_id, - pressure_shares, - ) - else { - break; - }; - let Some(repurchase_total) = - runtime_round_f64_to_i64(share_price_scalar * batch_size as f64) - else { - break; - }; - if repurchase_total <= 0 { - break; - } - let Some(next_outstanding_shares) = state - .service_state - .company_market_state - .get(&company_id) - .map(|market_state| market_state.outstanding_shares) - .and_then(|value| value.checked_sub(batch_size)) - else { - break; - }; - mutated |= service_post_company_stat_delta( - state, - company_id, - 0x0c, - (repurchase_total as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, - true, - ); - mutated |= service_post_company_stat_delta( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - -(repurchase_total as f64), - false, - ); - let Some(market_state) = state - .service_state - .company_market_state - .get_mut(&company_id) - else { - break; - }; - market_state.outstanding_shares = next_outstanding_shares; - state.service_state.annual_stock_repurchase_last_share_count = state - .service_state - .annual_stock_repurchase_last_share_count - .saturating_add(u64::from(batch_size)); - } - if mutated { - if let Some(label) = runtime_annual_finance_news_family_candidate_label( - policy_state.action, - state.service_state.annual_bond_last_retired_principal_total, - state.service_state.annual_bond_last_issued_principal_total, - state.service_state.annual_stock_repurchase_last_share_count, - state.service_state.annual_stock_issue_last_share_count, - ) { - state - .service_state - .annual_finance_last_news_family_candidates - .insert(company_id, label.to_string()); - finance_news_family_candidates.insert(company_id, label.to_string()); - let news_event = RuntimeAnnualFinanceNewsEvent { - company_id, - selector_label: label.to_string(), - action_label: - crate::runtime::runtime_company_annual_finance_policy_action_label( - policy_state.action, - ) - .to_string(), - retired_principal_total: state - .service_state - .annual_bond_last_retired_principal_total, - issued_principal_total: state - .service_state - .annual_bond_last_issued_principal_total, - repurchased_share_count: state - .service_state - .annual_stock_repurchase_last_share_count, - issued_share_count: state - .service_state - .annual_stock_issue_last_share_count, - }; - state - .service_state - .annual_finance_last_news_events - .push(news_event.clone()); - annual_finance_news_events.push(news_event); - } - applied_effect_count += 1; - mutated_company_ids.insert(company_id); - } - } - _ => {} - } - } - - service_events.push(ServiceEvent { - kind: "annual_finance_policy".to_string(), - trigger_kind: None, - serviced_record_ids: Vec::new(), - applied_effect_count, - mutated_company_ids: mutated_company_ids.into_iter().collect(), - mutated_player_ids: Vec::new(), - appended_record_ids: Vec::new(), - activated_record_ids: Vec::new(), - deactivated_record_ids: Vec::new(), - removed_record_ids: Vec::new(), - finance_news_family_candidates, - annual_finance_news_events, - dirty_rerun: false, - }); - - Ok(()) -} - -fn service_trigger_kind( - state: &mut RuntimeState, - trigger_kind: u8, - service_events: &mut Vec, -) -> Result<(), String> { - let eligible_indices = state - .event_runtime_records - .iter() - .enumerate() - .filter(|(_, record)| { - record.active - && record.trigger_kind == trigger_kind - && !(record.one_shot && record.has_fired) - }) - .map(|(index, _)| index) - .collect::>(); - - let mut serviced_record_ids = Vec::new(); - let mut applied_effect_count = 0_u32; - let mut mutated_company_ids = BTreeSet::new(); - let mut mutated_player_ids = BTreeSet::new(); - let mut appended_record_ids = Vec::new(); - let mut activated_record_ids = Vec::new(); - let mut deactivated_record_ids = Vec::new(); - let mut removed_record_ids = Vec::new(); - let mut staged_event_graph_mutations = Vec::new(); - let mut dirty_rerun = false; - - *state - .service_state - .trigger_dispatch_counts - .entry(trigger_kind) - .or_insert(0) += 1; - - for index in eligible_indices { - let ( - record_id, - record_conditions, - record_effects, - record_marks_collection_dirty, - record_one_shot, - ) = { - let record = &state.event_runtime_records[index]; - ( - record.record_id, - record.conditions.clone(), - record.effects.clone(), - record.marks_collection_dirty, - record.one_shot, - ) - }; - - let Some(condition_context) = evaluate_record_conditions(state, &record_conditions)? else { - continue; - }; - - let effect_summary = apply_runtime_effects( - state, - &record_effects, - &condition_context, - &mut mutated_company_ids, - &mut mutated_player_ids, - &mut staged_event_graph_mutations, - )?; - applied_effect_count += effect_summary.applied_effect_count; - appended_record_ids.extend(effect_summary.appended_record_ids); - activated_record_ids.extend(effect_summary.activated_record_ids); - deactivated_record_ids.extend(effect_summary.deactivated_record_ids); - removed_record_ids.extend(effect_summary.removed_record_ids); - - { - let record = &mut state.event_runtime_records[index]; - record.service_count += 1; - if record_one_shot { - record.has_fired = true; - } - } - - serviced_record_ids.push(record_id); - state.service_state.total_event_record_services += 1; - if trigger_kind != 0x0a && record_marks_collection_dirty { - dirty_rerun = true; - } - } - - commit_staged_event_graph_mutations(state, &staged_event_graph_mutations)?; - - service_events.push(ServiceEvent { - kind: "trigger_dispatch".to_string(), - trigger_kind: Some(trigger_kind), - serviced_record_ids, - applied_effect_count, - mutated_company_ids: mutated_company_ids.into_iter().collect(), - mutated_player_ids: mutated_player_ids.into_iter().collect(), - appended_record_ids, - activated_record_ids, - deactivated_record_ids, - removed_record_ids, - finance_news_family_candidates: BTreeMap::new(), - annual_finance_news_events: Vec::new(), - dirty_rerun, - }); - - if dirty_rerun { - state.service_state.dirty_rerun_count += 1; - service_trigger_kind(state, 0x0a, service_events)?; - } - - Ok(()) -} - -fn apply_runtime_effects( - state: &mut RuntimeState, - effects: &[RuntimeEffect], - condition_context: &ResolvedConditionContext, - mutated_company_ids: &mut BTreeSet, - mutated_player_ids: &mut BTreeSet, - staged_event_graph_mutations: &mut Vec, -) -> Result { - let mut summary = AppliedEffectsSummary::default(); - - for effect in effects { - match effect { - RuntimeEffect::SetWorldFlag { key, value } => { - state.world_flags.insert(key.clone(), *value); - let raw = u8::from(*value); - match key.as_str() { - "world.all_steam_locos_available" => { - state.world_restore.all_steam_locomotives_available_raw_u8 = Some(raw); - state.world_restore.all_steam_locomotives_available_enabled = Some(*value); - } - "world.all_diesel_locos_available" => { - state.world_restore.all_diesel_locomotives_available_raw_u8 = Some(raw); - state.world_restore.all_diesel_locomotives_available_enabled = Some(*value); - } - "world.all_electric_locos_available" => { - state - .world_restore - .all_electric_locomotives_available_raw_u8 = Some(raw); - state - .world_restore - .all_electric_locomotives_available_enabled = Some(*value); - } - _ => {} - } - } - RuntimeEffect::SetWorldScalarOverride { key, value } => { - state.world_scalar_overrides.insert(key.clone(), *value); - } - RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { - state.world_restore.limited_track_building_amount = Some(*value); - } - RuntimeEffect::SetEconomicStatusCode { value } => { - state.world_restore.economic_status_code = Some(*value); - } - RuntimeEffect::SetCompanyCash { target, value } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - for company_id in company_ids { - let prior_cash = state - .companies - .iter() - .find(|company| company.company_id == company_id) - .map(|company| company.current_cash) - .ok_or_else(|| { - format!("missing company_id {company_id} while applying cash effect") - })?; - if !service_post_company_stat_delta( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - value.saturating_sub(prior_cash) as f64, - false, - ) { - let company = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - .ok_or_else(|| { - format!( - "missing company_id {company_id} while applying cash effect" - ) - })?; - company.current_cash = *value; - } - mutated_company_ids.insert(company_id); - } - } - RuntimeEffect::SetPlayerCash { target, value } => { - let player_ids = resolve_player_target_ids(state, target, condition_context)?; - for player_id in player_ids { - let player = state - .players - .iter_mut() - .find(|player| player.player_id == player_id) - .ok_or_else(|| { - format!("missing player_id {player_id} while applying cash effect") - })?; - player.current_cash = *value; - mutated_player_ids.insert(player_id); - } - } - RuntimeEffect::SetChairmanCash { target, value } => { - let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?; - for profile_id in profile_ids { - let chairman = state - .chairman_profiles - .iter_mut() - .find(|profile| profile.profile_id == profile_id) - .ok_or_else(|| { - format!( - "missing chairman profile_id {profile_id} while applying cash effect" - ) - })?; - let preserved_threshold_adjusted_holdings_component = chairman - .purchasing_power_total - .saturating_sub(chairman.current_cash) - .max(0); - chairman.current_cash = *value; - chairman.purchasing_power_total = chairman - .current_cash - .saturating_add(preserved_threshold_adjusted_holdings_component); - } - } - RuntimeEffect::SetCompanyGovernanceScalar { - target, - metric, - value, - } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - for company_id in company_ids { - let applied_through_owner_state = match metric { - RuntimeCompanyMetric::CreditRating => { - service_set_company_credit_rating_target(state, company_id, *value) - } - RuntimeCompanyMetric::PrimeRate => { - service_set_company_prime_rate_target(state, company_id, *value) - } - RuntimeCompanyMetric::BookValuePerShare => { - service_set_company_direct_float_field( - state, - company_id, - 0x32f, - *value as f64, - ) - } - RuntimeCompanyMetric::InvestorConfidence => { - service_set_company_cached_share_price(state, company_id, *value as f64) - } - RuntimeCompanyMetric::ManagementAttitude => { - service_set_company_issue_opinion_total( - state, - company_id, - crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE, - i64::from(*value), - ) - } - _ => { - return Err(format!( - "unsupported governance metric {:?} in company governance effect", - metric - )); - } - }; - let company = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - .ok_or_else(|| { - format!( - "missing company_id {company_id} while applying governance effect" - ) - })?; - match metric { - RuntimeCompanyMetric::CreditRating => { - if !applied_through_owner_state { - company.credit_rating_score = Some(*value); - } - } - RuntimeCompanyMetric::PrimeRate => { - if !applied_through_owner_state { - company.prime_rate = Some(*value); - } - } - RuntimeCompanyMetric::BookValuePerShare => { - if !applied_through_owner_state { - company.book_value_per_share = *value; - } - } - RuntimeCompanyMetric::InvestorConfidence => { - if !applied_through_owner_state { - company.investor_confidence = *value; - } - } - RuntimeCompanyMetric::ManagementAttitude => { - if !applied_through_owner_state { - company.management_attitude = *value; - } - } - _ => unreachable!(), - }; - mutated_company_ids.insert(company_id); - } - } - RuntimeEffect::DeactivatePlayer { target } => { - let player_ids = resolve_player_target_ids(state, target, condition_context)?; - for player_id in player_ids { - let player = state - .players - .iter_mut() - .find(|player| player.player_id == player_id) - .ok_or_else(|| { - format!( - "missing player_id {player_id} while applying deactivate effect" - ) - })?; - player.active = false; - mutated_player_ids.insert(player_id); - if state.selected_player_id == Some(player_id) { - state.selected_player_id = None; - } - } - } - RuntimeEffect::DeactivateChairman { target } => { - let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?; - for profile_id in profile_ids.iter().copied() { - let linked_company_id = state - .chairman_profiles - .iter() - .find(|profile| profile.profile_id == profile_id) - .and_then(|profile| profile.linked_company_id); - let chairman = state - .chairman_profiles - .iter_mut() - .find(|profile| profile.profile_id == profile_id) - .ok_or_else(|| { - format!( - "missing chairman profile_id {profile_id} while applying deactivate effect" - ) - })?; - chairman.active = false; - chairman.linked_company_id = None; - if state.selected_chairman_profile_id == Some(profile_id) { - state.selected_chairman_profile_id = None; - } - if let Some(linked_company_id) = linked_company_id { - if let Some(company) = state - .companies - .iter_mut() - .find(|company| company.company_id == linked_company_id) - { - company.linked_chairman_profile_id = None; - mutated_company_ids.insert(linked_company_id); - } - for other in &mut state.chairman_profiles { - if other.profile_id != profile_id - && other.linked_company_id == Some(linked_company_id) - { - other.linked_company_id = None; - } - } - } - } - } - RuntimeEffect::SetCompanyTerritoryAccess { - target, - territory, - value, - } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - let territory_ids = resolve_territory_target_ids(state, territory)?; - set_company_territory_access_pairs( - &mut state.company_territory_access, - &company_ids, - &territory_ids, - *value, - ); - mutated_company_ids.extend(company_ids); - } - RuntimeEffect::ConfiscateCompanyAssets { target } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - for company_id in company_ids.iter().copied() { - let _ = service_zero_company_current_cash(state, company_id); - let _ = service_clear_company_live_bonds(state, company_id); - let company = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - .ok_or_else(|| { - format!( - "missing company_id {company_id} while applying confiscate effect" - ) - })?; - company.current_cash = 0; - company.debt = 0; - company.active = false; - mutated_company_ids.insert(company_id); - if state.selected_company_id == Some(company_id) { - state.selected_company_id = None; - } - } - retire_matching_trains(&mut state.trains, Some(&company_ids), None, None); - } - RuntimeEffect::DeactivateCompany { target } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - for company_id in company_ids { - let company = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - .ok_or_else(|| { - format!( - "missing company_id {company_id} while applying deactivate effect" - ) - })?; - company.active = false; - mutated_company_ids.insert(company_id); - if state.selected_company_id == Some(company_id) { - state.selected_company_id = None; - } - } - } - RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - for company_id in company_ids { - let company = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - .ok_or_else(|| { - format!( - "missing company_id {company_id} while applying track capacity effect" - ) - })?; - company.available_track_laying_capacity = *value; - mutated_company_ids.insert(company_id); - } - } - RuntimeEffect::RetireTrains { - company_target, - territory_target, - locomotive_name, - } => { - let company_ids = company_target - .as_ref() - .map(|target| resolve_company_target_ids(state, target, condition_context)) - .transpose()?; - let territory_ids = territory_target - .as_ref() - .map(|target| resolve_territory_target_ids(state, target)) - .transpose()?; - retire_matching_trains( - &mut state.trains, - company_ids.as_ref(), - territory_ids.as_ref(), - locomotive_name.as_deref(), - ); - } - RuntimeEffect::AdjustCompanyCash { target, delta } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - for company_id in company_ids { - let prior_cash = state - .companies - .iter() - .find(|company| company.company_id == company_id) - .map(|company| company.current_cash) - .ok_or_else(|| { - format!("missing company_id {company_id} while applying cash effect") - })?; - let next_cash = prior_cash.checked_add(*delta).ok_or_else(|| { - format!("company_id {company_id} cash adjustment overflow") - })?; - if !service_post_company_stat_delta( - state, - company_id, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - *delta as f64, - false, - ) { - let company = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - .ok_or_else(|| { - format!( - "missing company_id {company_id} while applying cash effect" - ) - })?; - company.current_cash = next_cash; - } - mutated_company_ids.insert(company_id); - } - } - RuntimeEffect::AdjustCompanyDebt { target, delta } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - for company_id in company_ids { - let company = state - .companies - .iter_mut() - .find(|company| company.company_id == company_id) - .ok_or_else(|| { - format!("missing company_id {company_id} while applying debt effect") - })?; - company.debt = apply_u64_delta(company.debt, *delta, company_id)?; - mutated_company_ids.insert(company_id); - } - } - RuntimeEffect::SetCandidateAvailability { name, value } => { - state.candidate_availability.insert(name.clone(), *value); - } - RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => { - state - .named_locomotive_availability - .insert(name.clone(), u32::from(*value)); - } - RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => { - state - .named_locomotive_availability - .insert(name.clone(), *value); - } - RuntimeEffect::SetNamedLocomotiveCost { name, value } => { - state.named_locomotive_cost.insert(name.clone(), *value); - } - RuntimeEffect::SetCargoPriceOverride { target, value } => match target { - RuntimeCargoPriceTarget::All => { - state.all_cargo_price_override = Some(*value); - } - RuntimeCargoPriceTarget::Named { name } => { - state - .named_cargo_price_overrides - .insert(name.clone(), *value); - } - }, - RuntimeEffect::SetCargoProductionOverride { target, value } => match target { - RuntimeCargoProductionTarget::All => { - state.all_cargo_production_override = Some(*value); - } - RuntimeCargoProductionTarget::Factory => { - state.factory_cargo_production_override = Some(*value); - } - RuntimeCargoProductionTarget::FarmMine => { - state.farm_mine_cargo_production_override = Some(*value); - } - RuntimeCargoProductionTarget::Named { name } => { - state - .named_cargo_production_overrides - .insert(name.clone(), *value); - } - }, - RuntimeEffect::SetCargoProductionSlot { slot, value } => { - state.cargo_production_overrides.insert(*slot, *value); - } - RuntimeEffect::SetWorldVariable { index, value } => { - state.world_runtime_variables.insert(*index, *value); - } - RuntimeEffect::SetCompanyVariable { - target, - index, - value, - } => { - let company_ids = resolve_company_target_ids(state, target, condition_context)?; - for company_id in company_ids { - state - .company_runtime_variables - .entry(company_id) - .or_default() - .insert(*index, *value); - mutated_company_ids.insert(company_id); - } - } - RuntimeEffect::SetPlayerVariable { - target, - index, - value, - } => { - let player_ids = resolve_player_target_ids(state, target, condition_context)?; - for player_id in player_ids { - state - .player_runtime_variables - .entry(player_id) - .or_default() - .insert(*index, *value); - } - } - RuntimeEffect::SetTerritoryVariable { - target, - index, - value, - } => { - let territory_ids = resolve_territory_target_ids(state, target)?; - for territory_id in territory_ids { - state - .territory_runtime_variables - .entry(territory_id) - .or_default() - .insert(*index, *value); - } - } - RuntimeEffect::SetTerritoryAccessCost { value } => { - state.world_restore.territory_access_cost = Some(*value); - } - RuntimeEffect::SetSpecialCondition { label, value } => { - state.special_conditions.insert(label.clone(), *value); - } - RuntimeEffect::AppendEventRecord { record } => { - staged_event_graph_mutations.push(EventGraphMutation::Append((**record).clone())); - summary.appended_record_ids.push(record.record_id); - } - RuntimeEffect::ActivateEventRecord { record_id } => { - staged_event_graph_mutations.push(EventGraphMutation::Activate { - record_id: *record_id, - }); - summary.activated_record_ids.push(*record_id); - } - RuntimeEffect::DeactivateEventRecord { record_id } => { - staged_event_graph_mutations.push(EventGraphMutation::Deactivate { - record_id: *record_id, - }); - summary.deactivated_record_ids.push(*record_id); - } - RuntimeEffect::RemoveEventRecord { record_id } => { - staged_event_graph_mutations.push(EventGraphMutation::Remove { - record_id: *record_id, - }); - summary.removed_record_ids.push(*record_id); - } - } - - summary.applied_effect_count += 1; - } - - Ok(summary) -} - -fn commit_staged_event_graph_mutations( - state: &mut RuntimeState, - staged_event_graph_mutations: &[EventGraphMutation], -) -> Result<(), String> { - for mutation in staged_event_graph_mutations { - match mutation { - EventGraphMutation::Append(record) => { - if state - .event_runtime_records - .iter() - .any(|existing| existing.record_id == record.record_id) - { - return Err(format!( - "cannot append duplicate event record_id {}", - record.record_id - )); - } - state - .event_runtime_records - .push(record.clone().into_runtime_record()); - } - EventGraphMutation::Activate { record_id } => { - let record = state - .event_runtime_records - .iter_mut() - .find(|record| record.record_id == *record_id) - .ok_or_else(|| { - format!("cannot activate missing event record_id {record_id}") - })?; - record.active = true; - } - EventGraphMutation::Deactivate { record_id } => { - let record = state - .event_runtime_records - .iter_mut() - .find(|record| record.record_id == *record_id) - .ok_or_else(|| { - format!("cannot deactivate missing event record_id {record_id}") - })?; - record.active = false; - } - EventGraphMutation::Remove { record_id } => { - let index = state - .event_runtime_records - .iter() - .position(|record| record.record_id == *record_id) - .ok_or_else(|| format!("cannot remove missing event record_id {record_id}"))?; - state.event_runtime_records.remove(index); - } - } - } - - state.validate() -} - -fn evaluate_record_conditions( - state: &RuntimeState, - conditions: &[RuntimeCondition], -) -> Result, String> { - if conditions.is_empty() { - return Ok(Some(ResolvedConditionContext::default())); - } - - let mut company_matches: Option> = None; - let mut player_matches: Option> = None; - let mut chairman_matches: Option> = None; - - for condition in conditions { - match condition { - RuntimeCondition::WorldVariableThreshold { - index, - comparator, - value, - } => { - let actual = state - .world_runtime_variables - .get(index) - .copied() - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::CompanyNumericThreshold { - target, - metric, - comparator, - value, - } => { - let resolved = resolve_company_target_ids( - state, - target, - &ResolvedConditionContext::default(), - )?; - let matching = resolved - .into_iter() - .filter(|company_id| { - state - .companies - .iter() - .find(|company| company.company_id == *company_id) - .is_some_and(|company| { - compare_condition_value( - company_metric_value(state, company, *metric), - *comparator, - *value, - ) - }) - }) - .collect::>(); - if matching.is_empty() { - return Ok(None); - } - intersect_company_matches(&mut company_matches, matching); - if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { - return Ok(None); - } - } - RuntimeCondition::CompanyVariableThreshold { - target, - index, - comparator, - value, - } => { - let resolved = resolve_company_target_ids( - state, - target, - &ResolvedConditionContext::default(), - )?; - let matching = resolved - .into_iter() - .filter(|company_id| { - let actual = state - .company_runtime_variables - .get(company_id) - .and_then(|vars| vars.get(index)) - .copied() - .unwrap_or(0); - compare_condition_value(actual, *comparator, *value) - }) - .collect::>(); - if matching.is_empty() { - return Ok(None); - } - intersect_company_matches(&mut company_matches, matching); - if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { - return Ok(None); - } - } - RuntimeCondition::TerritoryNumericThreshold { - target, - metric, - comparator, - value, - } => { - let territory_ids = resolve_territory_target_ids(state, target)?; - let actual = territory_metric_value(state, &territory_ids, *metric); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::TerritoryVariableThreshold { - target, - index, - comparator, - value, - } => { - let territory_ids = resolve_territory_target_ids(state, target)?; - let actual = territory_ids - .iter() - .map(|territory_id| { - state - .territory_runtime_variables - .get(territory_id) - .and_then(|vars| vars.get(index)) - .copied() - .unwrap_or(0) - }) - .sum::(); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::PlayerVariableThreshold { - target, - index, - comparator, - value, - } => { - let resolved = - resolve_player_target_ids(state, target, &ResolvedConditionContext::default())?; - let matching = resolved - .into_iter() - .filter(|player_id| { - let actual = state - .player_runtime_variables - .get(player_id) - .and_then(|vars| vars.get(index)) - .copied() - .unwrap_or(0); - compare_condition_value(actual, *comparator, *value) - }) - .collect::>(); - if matching.is_empty() { - return Ok(None); - } - intersect_player_matches(&mut player_matches, matching); - if player_matches.as_ref().is_some_and(BTreeSet::is_empty) { - return Ok(None); - } - } - RuntimeCondition::ChairmanNumericThreshold { - target, - metric, - comparator, - value, - } => { - let resolved = resolve_chairman_target_ids( - state, - target, - &ResolvedConditionContext::default(), - )?; - let matching = resolved - .into_iter() - .filter(|profile_id| { - state - .chairman_profiles - .iter() - .find(|profile| profile.profile_id == *profile_id) - .is_some_and(|profile| { - compare_condition_value( - chairman_metric_value(profile, *metric), - *comparator, - *value, - ) - }) - }) - .collect::>(); - if matching.is_empty() { - return Ok(None); - } - intersect_chairman_matches(&mut chairman_matches, matching); - if chairman_matches.as_ref().is_some_and(BTreeSet::is_empty) { - return Ok(None); - } - } - RuntimeCondition::CompanyTerritoryNumericThreshold { - target, - territory, - metric, - comparator, - value, - } => { - let territory_ids = resolve_territory_target_ids(state, territory)?; - let resolved = resolve_company_target_ids( - state, - target, - &ResolvedConditionContext::default(), - )?; - let matching = resolved - .into_iter() - .filter(|company_id| { - compare_condition_value( - company_territory_metric_value( - state, - *company_id, - &territory_ids, - *metric, - ), - *comparator, - *value, - ) - }) - .collect::>(); - if matching.is_empty() { - return Ok(None); - } - intersect_company_matches(&mut company_matches, matching); - if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { - return Ok(None); - } - } - RuntimeCondition::SpecialConditionThreshold { - label, - comparator, - value, - } => { - let actual = state - .special_conditions - .get(label) - .copied() - .map(i64::from) - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::CandidateAvailabilityThreshold { - name, - comparator, - value, - } => { - let actual = state - .candidate_availability - .get(name) - .copied() - .map(i64::from) - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name, - comparator, - value, - } => { - let actual = state - .named_locomotive_availability - .get(name) - .copied() - .map(i64::from) - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::NamedLocomotiveCostThreshold { - name, - comparator, - value, - } => { - let actual = state - .named_locomotive_cost - .get(name) - .copied() - .map(i64::from) - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::CargoProductionSlotThreshold { - slot, - comparator, - value, - .. - } => { - let actual = state - .cargo_production_overrides - .get(slot) - .copied() - .map(i64::from) - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => { - let actual = state - .cargo_production_overrides - .values() - .copied() - .map(i64::from) - .sum::(); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::FactoryProductionTotalThreshold { comparator, value } => { - let actual = cargo_production_total_for_class(state, RuntimeCargoClass::Factory); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value } => { - let actual = cargo_production_total_for_class(state, RuntimeCargoClass::FarmMine); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value } => { - let actual = cargo_production_total_for_class(state, RuntimeCargoClass::Other); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => { - let actual = state - .world_restore - .limited_track_building_amount - .map(i64::from) - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => { - let actual = state - .world_restore - .territory_access_cost - .map(i64::from) - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => { - let actual = state - .world_restore - .economic_status_code - .map(i64::from) - .unwrap_or(0); - if !compare_condition_value(actual, *comparator, *value) { - return Ok(None); - } - } - RuntimeCondition::WorldFlagEquals { key, value } => { - let actual = state.world_flags.get(key).copied().unwrap_or(false); - if actual != *value { - return Ok(None); - } - } - } - } - - Ok(Some(ResolvedConditionContext { - matching_company_ids: company_matches.unwrap_or_default(), - matching_player_ids: player_matches.unwrap_or_default(), - matching_chairman_profile_ids: chairman_matches.unwrap_or_default(), - })) -} - -fn intersect_company_matches(company_matches: &mut Option>, next: BTreeSet) { - match company_matches { - Some(existing) => { - existing.retain(|company_id| next.contains(company_id)); - } - None => { - *company_matches = Some(next); - } - } -} - -fn intersect_player_matches(player_matches: &mut Option>, next: BTreeSet) { - match player_matches { - Some(existing) => { - existing.retain(|player_id| next.contains(player_id)); - } - None => { - *player_matches = Some(next); - } - } -} - -fn intersect_chairman_matches(chairman_matches: &mut Option>, next: BTreeSet) { - match chairman_matches { - Some(existing) => { - existing.retain(|profile_id| next.contains(profile_id)); - } - None => { - *chairman_matches = Some(next); - } - } -} - -fn resolve_company_target_ids( - state: &RuntimeState, - target: &RuntimeCompanyTarget, - condition_context: &ResolvedConditionContext, -) -> Result, String> { - match target { - RuntimeCompanyTarget::AllActive => Ok(state - .companies - .iter() - .filter(|company| company.active) - .map(|company| company.company_id) - .collect()), - RuntimeCompanyTarget::Ids { ids } => { - let known_ids = state - .companies - .iter() - .map(|company| company.company_id) - .collect::>(); - for company_id in ids { - if !known_ids.contains(company_id) { - return Err(format!("target references unknown company_id {company_id}")); - } - } - Ok(ids.clone()) - } - RuntimeCompanyTarget::HumanCompanies => { - if state - .companies - .iter() - .any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown) - { - return Err( - "target requires company role context but at least one company has unknown controller_kind" - .to_string(), - ); - } - Ok(state - .companies - .iter() - .filter(|company| { - company.active && company.controller_kind == RuntimeCompanyControllerKind::Human - }) - .map(|company| company.company_id) - .collect()) - } - RuntimeCompanyTarget::AiCompanies => { - if state - .companies - .iter() - .any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown) - { - return Err( - "target requires company role context but at least one company has unknown controller_kind" - .to_string(), - ); - } - Ok(state - .companies - .iter() - .filter(|company| { - company.active && company.controller_kind == RuntimeCompanyControllerKind::Ai - }) - .map(|company| company.company_id) - .collect()) - } - RuntimeCompanyTarget::SelectedCompany => { - let selected_company_id = state - .selected_company_id - .ok_or_else(|| "target requires selected_company_id context".to_string())?; - if state - .companies - .iter() - .any(|company| company.company_id == selected_company_id && company.active) - { - Ok(vec![selected_company_id]) - } else { - Err( - "target requires selected_company_id to reference an active company" - .to_string(), - ) - } - } - RuntimeCompanyTarget::ConditionTrueCompany => { - if condition_context.matching_company_ids.is_empty() { - Err("target requires condition-evaluation context".to_string()) - } else { - Ok(condition_context - .matching_company_ids - .iter() - .copied() - .collect()) - } - } - } -} - -fn resolve_player_target_ids( - state: &RuntimeState, - target: &RuntimePlayerTarget, - condition_context: &ResolvedConditionContext, -) -> Result, String> { - match target { - RuntimePlayerTarget::AllActive => Ok(state - .players - .iter() - .filter(|player| player.active) - .map(|player| player.player_id) - .collect()), - RuntimePlayerTarget::Ids { ids } => { - let known_ids = state - .players - .iter() - .map(|player| player.player_id) - .collect::>(); - for player_id in ids { - if !known_ids.contains(player_id) { - return Err(format!("target references unknown player_id {player_id}")); - } - } - Ok(ids.clone()) - } - RuntimePlayerTarget::HumanPlayers => { - if state - .players - .iter() - .any(|player| player.controller_kind == RuntimeCompanyControllerKind::Unknown) - { - return Err( - "target requires player role context but at least one player has unknown controller_kind" - .to_string(), - ); - } - Ok(state - .players - .iter() - .filter(|player| { - player.active && player.controller_kind == RuntimeCompanyControllerKind::Human - }) - .map(|player| player.player_id) - .collect()) - } - RuntimePlayerTarget::AiPlayers => { - if state - .players - .iter() - .any(|player| player.controller_kind == RuntimeCompanyControllerKind::Unknown) - { - return Err( - "target requires player role context but at least one player has unknown controller_kind" - .to_string(), - ); - } - Ok(state - .players - .iter() - .filter(|player| { - player.active && player.controller_kind == RuntimeCompanyControllerKind::Ai - }) - .map(|player| player.player_id) - .collect()) - } - RuntimePlayerTarget::SelectedPlayer => { - let selected_player_id = state - .selected_player_id - .ok_or_else(|| "target requires selected_player_id context".to_string())?; - if state - .players - .iter() - .any(|player| player.player_id == selected_player_id && player.active) - { - Ok(vec![selected_player_id]) - } else { - Err("target requires selected_player_id to reference an active player".to_string()) - } - } - RuntimePlayerTarget::ConditionTruePlayer => { - if condition_context.matching_player_ids.is_empty() { - Err("target requires player condition-evaluation context".to_string()) - } else { - Ok(condition_context - .matching_player_ids - .iter() - .copied() - .collect()) - } - } - } -} - -fn resolve_chairman_target_ids( - state: &RuntimeState, - target: &RuntimeChairmanTarget, - condition_context: &ResolvedConditionContext, -) -> Result, String> { - match target { - RuntimeChairmanTarget::AllActive => Ok(state - .chairman_profiles - .iter() - .filter(|profile| profile.active) - .map(|profile| profile.profile_id) - .collect()), - RuntimeChairmanTarget::HumanChairmen => Ok(state - .chairman_profiles - .iter() - .filter(|profile| { - chairman_profile_matches_company_controller_kind( - state, - profile, - RuntimeCompanyControllerKind::Human, - ) - }) - .map(|profile| profile.profile_id) - .collect()), - RuntimeChairmanTarget::AiChairmen => Ok(state - .chairman_profiles - .iter() - .filter(|profile| { - chairman_profile_matches_company_controller_kind( - state, - profile, - RuntimeCompanyControllerKind::Ai, - ) - }) - .map(|profile| profile.profile_id) - .collect()), - RuntimeChairmanTarget::Ids { ids } => { - let known_ids = state - .chairman_profiles - .iter() - .map(|profile| profile.profile_id) - .collect::>(); - for profile_id in ids { - if !known_ids.contains(profile_id) { - return Err(format!( - "target references unknown chairman profile_id {profile_id}" - )); - } - } - Ok(ids.clone()) - } - RuntimeChairmanTarget::SelectedChairman => { - let selected_profile_id = state.selected_chairman_profile_id.ok_or_else(|| { - "target requires selected_chairman_profile_id context".to_string() - })?; - if state - .chairman_profiles - .iter() - .any(|profile| profile.profile_id == selected_profile_id && profile.active) - { - Ok(vec![selected_profile_id]) - } else { - Err( - "target requires selected_chairman_profile_id to reference an active chairman profile" - .to_string(), - ) - } - } - RuntimeChairmanTarget::ConditionTrueChairman => { - if condition_context.matching_chairman_profile_ids.is_empty() { - Err("target requires chairman condition-evaluation context".to_string()) - } else { - Ok(condition_context - .matching_chairman_profile_ids - .iter() - .copied() - .collect()) - } - } - } -} - -fn chairman_profile_matches_company_controller_kind( - state: &RuntimeState, - profile: &crate::RuntimeChairmanProfile, - controller_kind: RuntimeCompanyControllerKind, -) -> bool { - profile.active - && profile - .linked_company_id - .and_then(|company_id| { - state - .companies - .iter() - .find(|company| company.company_id == company_id) - }) - .is_some_and(|company| company.controller_kind == controller_kind) -} - -fn resolve_territory_target_ids( - state: &RuntimeState, - target: &RuntimeTerritoryTarget, -) -> Result, String> { - match target { - RuntimeTerritoryTarget::AllTerritories => Ok(state - .territories - .iter() - .map(|territory| territory.territory_id) - .collect()), - RuntimeTerritoryTarget::Ids { ids } => { - let known_ids = state - .territories - .iter() - .map(|territory| territory.territory_id) - .collect::>(); - for territory_id in ids { - if !known_ids.contains(territory_id) { - return Err(format!( - "territory target references unknown territory_id {territory_id}" - )); - } - } - Ok(ids.clone()) - } - } -} - -fn company_metric_value( - state: &RuntimeState, - company: &crate::RuntimeCompany, - metric: RuntimeCompanyMetric, -) -> i64 { - match metric { - RuntimeCompanyMetric::CurrentCash => company.current_cash, - RuntimeCompanyMetric::TotalDebt => company.debt as i64, - RuntimeCompanyMetric::CreditRating => { - runtime_company_credit_rating(state, company.company_id).unwrap_or(0) - } - RuntimeCompanyMetric::PrimeRate => { - runtime_company_prime_rate(state, company.company_id).unwrap_or(0) - } - RuntimeCompanyMetric::BookValuePerShare => { - runtime_company_book_value_per_share(state, company.company_id).unwrap_or(0) - } - RuntimeCompanyMetric::InvestorConfidence => { - runtime_company_investor_confidence(state, company.company_id).unwrap_or(0) - } - RuntimeCompanyMetric::ManagementAttitude => { - runtime_company_management_attitude(state, company.company_id).unwrap_or(0) - } - RuntimeCompanyMetric::TrackPiecesTotal => i64::from(company.track_piece_counts.total), - RuntimeCompanyMetric::TrackPiecesSingle => i64::from(company.track_piece_counts.single), - RuntimeCompanyMetric::TrackPiecesDouble => i64::from(company.track_piece_counts.double), - RuntimeCompanyMetric::TrackPiecesTransition => { - i64::from(company.track_piece_counts.transition) - } - RuntimeCompanyMetric::TrackPiecesElectric => i64::from(company.track_piece_counts.electric), - RuntimeCompanyMetric::TrackPiecesNonElectric => { - i64::from(company.track_piece_counts.non_electric) - } - } -} - -fn chairman_metric_value( - profile: &crate::RuntimeChairmanProfile, - metric: RuntimeChairmanMetric, -) -> i64 { - match metric { - RuntimeChairmanMetric::CurrentCash => profile.current_cash, - RuntimeChairmanMetric::HoldingsValueTotal => profile.holdings_value_total, - RuntimeChairmanMetric::NetWorthTotal => profile.net_worth_total, - RuntimeChairmanMetric::PurchasingPowerTotal => profile.purchasing_power_total, - } -} - -fn territory_metric_value( - state: &RuntimeState, - territory_ids: &[u32], - metric: RuntimeTerritoryMetric, -) -> i64 { - state - .territories - .iter() - .filter(|territory| territory_ids.contains(&territory.territory_id)) - .map(|territory| { - track_piece_metric_value( - territory.track_piece_counts, - territory_metric_to_track_metric(metric), - ) - }) - .sum() -} - -fn company_territory_metric_value( - state: &RuntimeState, - company_id: u32, - territory_ids: &[u32], - metric: RuntimeTrackMetric, -) -> i64 { - state - .company_territory_track_piece_counts - .iter() - .filter(|entry| { - entry.company_id == company_id && territory_ids.contains(&entry.territory_id) - }) - .map(|entry| track_piece_metric_value(entry.track_piece_counts, metric)) - .sum() -} - -fn track_piece_metric_value(counts: RuntimeTrackPieceCounts, metric: RuntimeTrackMetric) -> i64 { - match metric { - RuntimeTrackMetric::Total => i64::from(counts.total), - RuntimeTrackMetric::Single => i64::from(counts.single), - RuntimeTrackMetric::Double => i64::from(counts.double), - RuntimeTrackMetric::Transition => i64::from(counts.transition), - RuntimeTrackMetric::Electric => i64::from(counts.electric), - RuntimeTrackMetric::NonElectric => i64::from(counts.non_electric), - } -} - -fn territory_metric_to_track_metric(metric: RuntimeTerritoryMetric) -> RuntimeTrackMetric { - match metric { - RuntimeTerritoryMetric::TrackPiecesTotal => RuntimeTrackMetric::Total, - RuntimeTerritoryMetric::TrackPiecesSingle => RuntimeTrackMetric::Single, - RuntimeTerritoryMetric::TrackPiecesDouble => RuntimeTrackMetric::Double, - RuntimeTerritoryMetric::TrackPiecesTransition => RuntimeTrackMetric::Transition, - RuntimeTerritoryMetric::TrackPiecesElectric => RuntimeTrackMetric::Electric, - RuntimeTerritoryMetric::TrackPiecesNonElectric => RuntimeTrackMetric::NonElectric, - } -} - -fn compare_condition_value( - actual: i64, - comparator: RuntimeConditionComparator, - expected: i64, -) -> bool { - match comparator { - RuntimeConditionComparator::Ge => actual >= expected, - RuntimeConditionComparator::Le => actual <= expected, - RuntimeConditionComparator::Gt => actual > expected, - RuntimeConditionComparator::Lt => actual < expected, - RuntimeConditionComparator::Eq => actual == expected, - RuntimeConditionComparator::Ne => actual != expected, - } -} - -fn cargo_production_total_for_class(state: &RuntimeState, cargo_class: RuntimeCargoClass) -> i64 { - state - .cargo_catalog - .iter() - .filter(|entry| entry.cargo_class == cargo_class) - .filter_map(|entry| state.cargo_production_overrides.get(&entry.slot_id)) - .copied() - .map(i64::from) - .sum() -} - -fn apply_u64_delta(current: u64, delta: i64, company_id: u32) -> Result { - if delta >= 0 { - current - .checked_add(delta as u64) - .ok_or_else(|| format!("company_id {company_id} debt adjustment overflow")) - } else { - current - .checked_sub(delta.unsigned_abs()) - .ok_or_else(|| format!("company_id {company_id} debt adjustment underflow")) - } -} - -fn retire_matching_trains( - trains: &mut [crate::RuntimeTrain], - company_ids: Option<&Vec>, - territory_ids: Option<&Vec>, - locomotive_name: Option<&str>, -) { - for train in trains.iter_mut() { - if !train.active || train.retired { - continue; - } - if company_ids.is_some_and(|company_ids| !company_ids.contains(&train.owner_company_id)) { - continue; - } - if territory_ids.is_some_and(|territory_ids| { - !train - .territory_id - .is_some_and(|territory_id| territory_ids.contains(&territory_id)) - }) { - continue; - } - if locomotive_name.is_some_and(|name| train.locomotive_name.as_deref() != Some(name)) { - continue; - } - train.active = false; - train.retired = true; - } -} - -fn set_company_territory_access_pairs( - access_entries: &mut Vec, - company_ids: &[u32], - territory_ids: &[u32], - value: bool, -) { - if value { - for company_id in company_ids { - for territory_id in territory_ids { - if !access_entries.iter().any(|entry| { - entry.company_id == *company_id && entry.territory_id == *territory_id - }) { - access_entries.push(crate::RuntimeCompanyTerritoryAccess { - company_id: *company_id, - territory_id: *territory_id, - }); - } - } - } - } else { - access_entries.retain(|entry| { - !(company_ids.contains(&entry.company_id) - && territory_ids.contains(&entry.territory_id)) - }); - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use super::*; - use crate::{ - CalendarPoint, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, - RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, - RuntimeCompanyControllerKind, RuntimeCompanyPeriodicSideLatchState, RuntimeCompanyTarget, - RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, - RuntimeEventRecordTemplate, RuntimePlayer, RuntimePlayerTarget, RuntimeSaveProfileState, - RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts, - RuntimeTrain, RuntimeWorldRestoreState, - }; - - fn state() -> RuntimeState { - RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Unknown, - current_cash: 10, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - } - } - - #[test] - fn advances_to_target() { - let mut state = state(); - let result = execute_step_command( - &mut state, - &StepCommand::AdvanceTo { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 5, - }, - }, - ) - .expect("advance_to should succeed"); - - assert_eq!(result.steps_executed, 5); - assert_eq!(state.calendar.tick_slot, 5); - assert_eq!(state.world_restore.packed_year_word_raw_u16, Some(1830)); - assert_eq!(state.world_restore.partial_year_progress_raw_u8, Some(1)); - assert_eq!( - state.world_restore.current_calendar_tuple_word_raw_u32, - Some(0x0101_0726) - ); - assert_eq!( - state.world_restore.current_calendar_tuple_word_2_raw_u32, - Some(0x2801_0001) - ); - assert_eq!( - state.world_restore.absolute_counter_raw_u32, - Some(885_427_240) - ); - assert_eq!( - state.world_restore.absolute_counter_mirror_raw_u32, - Some(885_427_240) - ); - assert_eq!( - state.world_restore.selected_year_gap_scalar_raw_u32, - Some((1.0f32 / 3.0).to_bits()) - ); - assert_eq!( - state - .world_restore - .selected_year_gap_scalar_value_f32_text - .as_deref(), - Some("0.333333") - ); - } - - #[test] - fn year_rollover_step_runs_periodic_boundary_services() { - let mut state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: crate::MONTH_SLOTS_PER_YEAR - 1, - phase_slot: crate::PHASE_SLOTS_PER_MONTH - 1, - tick_slot: crate::TICKS_PER_PHASE - 1, - }, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 77, - trigger_kind: 1, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world.periodic_rollover_service_fired".to_string(), - value: true, - }], - }], - ..state() - }; - - let result = execute_step_command(&mut state, &StepCommand::StepCount { steps: 1 }) - .expect("year rollover step should run periodic boundary services"); - - assert_eq!(result.steps_executed, 1); - assert_eq!( - result.boundary_events, - vec![BoundaryEvent { - kind: "year_rollover".to_string(), - calendar: CalendarPoint { - year: 1831, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - }] - ); - assert_eq!(state.service_state.periodic_boundary_calls, 1); - assert_eq!(state.service_state.total_event_record_services, 1); - assert_eq!( - state - .world_flags - .get("world.periodic_rollover_service_fired"), - Some(&true) - ); - assert_eq!(state.world_restore.packed_year_word_raw_u16, Some(1831)); - assert_eq!(state.world_restore.partial_year_progress_raw_u8, Some(1)); - assert_eq!( - state.world_restore.current_calendar_tuple_word_raw_u32, - Some(0x0101_0727) - ); - assert_eq!( - state.world_restore.current_calendar_tuple_word_2_raw_u32, - Some(0x0001_0001) - ); - assert_eq!( - state.world_restore.absolute_counter_raw_u32, - Some(885_911_040) - ); - assert_eq!( - state.world_restore.absolute_counter_mirror_raw_u32, - Some(885_911_040) - ); - assert_eq!( - state.world_restore.selected_year_gap_scalar_raw_u32, - Some((1.0f32 / 3.0).to_bits()) - ); - assert_eq!( - state - .world_restore - .selected_year_gap_scalar_value_f32_text - .as_deref(), - Some("0.333333") - ); - assert!( - result - .service_events - .iter() - .any(|event| event.trigger_kind == Some(1)) - ); - assert!( - result - .service_events - .iter() - .any(|event| event.kind == "annual_finance_policy") - ); - } - - #[test] - fn rejects_backward_target() { - let mut state = state(); - state.calendar.tick_slot = 3; - - let result = execute_step_command( - &mut state, - &StepCommand::AdvanceTo { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 2, - }, - }, - ); - - assert!(result.is_err()); - } - - #[test] - fn services_periodic_trigger_order_and_dirty_rerun() { - let mut state = RuntimeState { - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 1, - trigger_kind: 1, - active: true, - service_count: 0, - marks_collection_dirty: true, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetWorldFlag { - key: "runtime.effect_fired".to_string(), - value: true, - }], - }, - RuntimeEventRecord { - record_id: 2, - trigger_kind: 4, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::AllActive, - delta: 5, - }], - }, - RuntimeEventRecord { - record_id: 3, - trigger_kind: 0x0a, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetSpecialCondition { - label: "Dirty rerun fired".to_string(), - value: 1, - }], - }, - ], - ..state() - }; - - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary service should succeed"); - - assert_eq!(result.steps_executed, 0); - assert_eq!(state.service_state.periodic_boundary_calls, 1); - assert_eq!(state.service_state.total_event_record_services, 3); - assert_eq!(state.service_state.dirty_rerun_count, 1); - assert_eq!(state.event_runtime_records[0].service_count, 1); - assert_eq!(state.event_runtime_records[1].service_count, 1); - assert_eq!(state.event_runtime_records[2].service_count, 1); - assert_eq!(state.world_flags.get("runtime.effect_fired"), Some(&true)); - assert_eq!(state.companies[0].current_cash, 15); - assert_eq!(state.special_conditions.get("Dirty rerun fired"), Some(&1)); - assert_eq!( - state.service_state.trigger_dispatch_counts.get(&1), - Some(&1) - ); - assert_eq!( - state.service_state.trigger_dispatch_counts.get(&4), - Some(&1) - ); - assert_eq!( - state.service_state.trigger_dispatch_counts.get(&0x0a), - Some(&1) - ); - assert_eq!(result.service_events.len(), 8); - assert_eq!(result.service_events[0].applied_effect_count, 1); - assert_eq!( - result - .service_events - .iter() - .find(|event| event.trigger_kind == Some(4)) - .expect("trigger kind 4 event should be present") - .applied_effect_count, - 1 - ); - assert_eq!( - result - .service_events - .iter() - .find(|event| event.trigger_kind == Some(0x0a)) - .expect("trigger kind 0x0a event should be present") - .applied_effect_count, - 1 - ); - assert_eq!( - result - .service_events - .iter() - .find(|event| event.trigger_kind == Some(4)) - .expect("trigger kind 4 event should be present") - .mutated_company_ids, - vec![1] - ); - assert!( - result - .service_events - .iter() - .any(|event| event.kind == "annual_finance_policy") - ); - } - - #[test] - fn periodic_boundary_clears_transient_preferred_locomotive_side_latch() { - let mut state = state(); - state.service_state.company_periodic_side_latch_state = BTreeMap::from([( - 1, - RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: Some(2), - city_connection_latch: true, - linked_transit_latch: false, - }, - )]); - - execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should refresh periodic side latches"); - - assert_eq!( - state - .service_state - .company_periodic_side_latch_state - .get(&1), - Some(&RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: None, - city_connection_latch: true, - linked_transit_latch: false, - }) - ); - } - - #[test] - fn periodic_boundary_reseeds_finance_side_latches_from_market_state() { - let mut state = state(); - state.service_state.company_market_state = BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - city_connection_latch: false, - linked_transit_latch: true, - ..crate::RuntimeCompanyMarketState::default() - }, - )]); - state.service_state.company_periodic_side_latch_state = BTreeMap::from([( - 1, - RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: Some(2), - city_connection_latch: true, - linked_transit_latch: false, - }, - )]); - - execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should reseed finance latches from market state"); - - assert_eq!( - state - .service_state - .company_periodic_side_latch_state - .get(&1), - Some(&RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: None, - city_connection_latch: false, - linked_transit_latch: true, - }) - ); - } - - #[test] - fn periodic_boundary_applies_dividend_adjustment_from_annual_finance_policy() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = - (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); - - let mut state = RuntimeState { - calendar: crate::CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - dividend_policy_raw_u8: Some(0), - dividend_adjustment_allowed: Some(true), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - building_density_growth_setting_raw_u32: Some(1), - ..crate::RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 21, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: Some(7), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(21), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 7, - name: "Chairman Seven".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(21), - company_holdings: BTreeMap::from([(21, 9_500)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - company_market_state: BTreeMap::from([( - 21, - crate::RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1840, - last_dividend_year: 1844, - year_stat_family_qword_bits, - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x33f, - 0.4f32.to_bits(), - )]), - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should apply annual finance policy"); - - assert_eq!(state.service_state.annual_finance_service_calls, 1); - assert_eq!( - state.service_state.annual_dividend_adjustment_commit_count, - 1 - ); - assert_eq!( - state.service_state.annual_finance_last_actions.get(&21), - Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment) - ); - assert_eq!( - state.service_state.company_market_state[&21] - .direct_control_transfer_float_fields_raw_u32 - .get(&0x33f), - Some(&1.8f32.to_bits()) - ); - assert!( - result - .service_events - .iter() - .any(|event| event.kind == "annual_finance_policy" - && event.applied_effect_count == 1 - && event.mutated_company_ids == vec![21] - && event.finance_news_family_candidates.is_empty()) - ); - } - - #[test] - fn periodic_boundary_applies_stock_issue_from_annual_finance_policy() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = 250_000.0f64.to_bits(); - - let current_issue_calendar_word = 0x0101_0725; - let current_issue_calendar_word_2 = 0x0001_0001; - let prior_issue_calendar_word = 0x0101_0701; - let prior_issue_calendar_word_2 = 0x0001_0001; - - let mut state = RuntimeState { - calendar: crate::CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - current_calendar_tuple_word_raw_u32: Some(0x0101_0726), - current_calendar_tuple_word_2_raw_u32: Some(0x0001_0001), - absolute_counter_raw_u32: Some(885_911_040), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - issue_37_value: Some(5.0f32.to_bits()), - issue_38_value: Some(2), - ..crate::RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 22, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(22), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - company_market_state: BTreeMap::from([( - 22, - crate::RuntimeCompanyMarketState { - outstanding_shares: 20_000, - bond_count: 2, - highest_coupon_live_bond_principal: Some(300_000), - founding_year: 1840, - cached_share_price_raw_u32: 35.0f32.to_bits(), - recent_per_share_cache_absolute_counter: 885_911_040, - recent_per_share_cached_value_bits: 34.0f64.to_bits(), - current_issue_calendar_word, - current_issue_calendar_word_2, - prior_issue_calendar_word, - prior_issue_calendar_word_2, - live_bond_slots: vec![ - crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 300_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.11f32.to_bits(), - }, - crate::RuntimeCompanyBondSlot { - slot_index: 1, - principal: 200_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.07f32.to_bits(), - }, - ], - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x32f, - 30.0f32.to_bits(), - )]), - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should apply annual stock issue policy"); - - assert_eq!( - state.service_state.annual_finance_last_actions.get(&22), - Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::StockIssue) - ); - assert_eq!( - state - .service_state - .annual_finance_last_news_family_candidates - .get(&22), - Some(&"4053".to_string()) - ); - assert_eq!( - state.service_state.annual_stock_issue_last_share_count, - 4_000 - ); - assert_eq!( - state.service_state.annual_stock_repurchase_last_share_count, - 0 - ); - assert_eq!(state.companies[0].current_cash, 390_000); - assert_eq!( - state.service_state.company_market_state[&22].outstanding_shares, - 24_000 - ); - assert_eq!( - state.service_state.company_market_state[&22].prior_issue_calendar_word, - current_issue_calendar_word - ); - assert_eq!( - state.service_state.company_market_state[&22].prior_issue_calendar_word_2, - current_issue_calendar_word_2 - ); - assert_eq!( - state.service_state.company_market_state[&22].current_issue_calendar_word, - 0x0101_0726 - ); - assert_eq!( - state.service_state.company_market_state[&22].current_issue_calendar_word_2, - 0x0001_0001 - ); - assert!(result.service_events.iter().any(|event| { - event.kind == "annual_finance_policy" - && event.applied_effect_count == 1 - && event.mutated_company_ids == vec![22] - && event.finance_news_family_candidates.get(&22) == Some(&"4053".to_string()) - && event.annual_finance_news_events.iter().any(|news| { - news.company_id == 22 - && news.selector_label == "4053" - && news.action_label == "stock_issue" - && news.issued_share_count == 4_000 - }) - })); - } - - #[test] - fn periodic_boundary_applies_stock_repurchase_from_annual_finance_policy() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = 1_600_000.0f64.to_bits(); - - let base_state = RuntimeState { - calendar: crate::CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - absolute_counter_raw_u32: Some(1_000), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - building_density_growth_setting_raw_u32: Some(0), - ..crate::RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 23, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: Some(8), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(23), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 8, - name: "Chairman".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(23), - company_holdings: BTreeMap::from([(23, 9_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: Some(8), - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - company_market_state: BTreeMap::from([( - 23, - crate::RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1840, - city_connection_latch: true, - recent_per_share_cache_absolute_counter: 1_000, - recent_per_share_cached_value_bits: 50.0f64.to_bits(), - mutable_support_scalar_raw_u32: 0.0f32.to_bits(), - young_company_support_scalar_raw_u32: 0.0f32.to_bits(), - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let pressured_share_price = - crate::runtime::runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( - &base_state, - 23, - 700, - ) - .expect("repurchase share price"); - let expected_repurchase_total = - crate::runtime::runtime_round_f64_to_i64(pressured_share_price * 1_000.0) - .expect("repurchase total should round"); - - let mut state = base_state; - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should apply annual stock repurchase policy"); - - assert_eq!( - state.service_state.annual_finance_last_actions.get(&23), - Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase) - ); - assert_eq!( - state - .service_state - .annual_finance_last_news_family_candidates - .get(&23), - Some(&"2887".to_string()) - ); - assert_eq!(state.service_state.annual_stock_issue_last_share_count, 0); - assert_eq!( - state.service_state.annual_stock_repurchase_last_share_count, - 1_000 - ); - assert_eq!( - state.companies[0].current_cash, - 1_600_000 - expected_repurchase_total - ); - assert_eq!( - state.service_state.company_market_state[&23].outstanding_shares, - 9_000 - ); - assert!(result.service_events.iter().any(|event| { - event.kind == "annual_finance_policy" - && event.applied_effect_count == 1 - && event.mutated_company_ids == vec![23] - && event.finance_news_family_candidates.get(&23) == Some(&"2887".to_string()) - && event.annual_finance_news_events.iter().any(|news| { - news.company_id == 23 - && news.selector_label == "2887" - && news.action_label == "stock_repurchase" - && news.repurchased_share_count == 1_000 - }) - })); - } - - #[test] - fn periodic_boundary_applies_bond_issue_from_annual_finance_policy() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = (-400_000.0f64).to_bits(); - - let mut state = RuntimeState { - calendar: crate::CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..crate::RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 24, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - current_cash: 0, - debt: 0, - credit_rating_score: Some(6), - prime_rate: Some(5), - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(24), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - company_market_state: BTreeMap::from([( - 24, - crate::RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1840, - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should apply annual bond issue policy"); - - assert_eq!( - state.service_state.annual_finance_last_actions.get(&24), - Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) - ); - assert_eq!( - state - .service_state - .annual_finance_last_news_family_candidates - .get(&24), - Some(&"2886".to_string()) - ); - assert_eq!(state.companies[0].current_cash, 100_000); - assert_eq!(state.service_state.company_market_state[&24].bond_count, 1); - assert_eq!( - state.service_state.annual_bond_last_retired_principal_total, - 0 - ); - assert_eq!( - state.service_state.annual_bond_last_issued_principal_total, - 500_000 - ); - assert_eq!( - state.service_state.company_market_state[&24].largest_live_bond_principal, - Some(500_000) - ); - assert_eq!( - state.service_state.company_market_state[&24].highest_coupon_live_bond_principal, - Some(500_000) - ); - assert_eq!( - state.service_state.company_market_state[&24].live_bond_slots, - vec![crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 500_000, - maturity_year: 1875, - coupon_rate_raw_u32: 0.09f32.to_bits(), - }] - ); - assert!(result.service_events.iter().any(|event| { - event.kind == "annual_finance_policy" - && event.applied_effect_count == 1 - && event.mutated_company_ids == vec![24] - && event.finance_news_family_candidates.get(&24) == Some(&"2886".to_string()) - && event.annual_finance_news_events.iter().any(|news| { - news.company_id == 24 - && news.selector_label == "2886" - && news.action_label == "bond_issue" - && news.retired_principal_total == 0 - && news.issued_principal_total == 500_000 - }) - })); - } - - #[test] - fn periodic_boundary_retires_live_bonds_when_annual_bond_lane_needs_no_reissue() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - write_current_value( - &mut year_stat_family_qword_bits, - crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - 900_000.0, - ); - write_current_value(&mut year_stat_family_qword_bits, 0x12, -350_000.0); - - let mut state = crate::RuntimeState { - calendar: crate::CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..crate::RuntimeWorldRestoreState::default() - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 25, - current_cash: 900_000, - debt: 350_000, - active: true, - credit_rating_score: Some(7), - prime_rate: Some(6), - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - available_track_laying_capacity: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(25), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - company_market_state: BTreeMap::from([( - 25, - crate::RuntimeCompanyMarketState { - bond_count: 2, - live_bond_slots: vec![ - crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 200_000, - maturity_year: 1845, - coupon_rate_raw_u32: 0.09f32.to_bits(), - }, - crate::RuntimeCompanyBondSlot { - slot_index: 1, - principal: 150_000, - maturity_year: 1845, - coupon_rate_raw_u32: 0.08f32.to_bits(), - }, - ], - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should apply annual bond repayment lane"); - - assert_eq!( - state.service_state.annual_finance_last_actions.get(&25), - Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) - ); - assert_eq!( - state - .service_state - .annual_finance_last_news_family_candidates - .get(&25), - Some(&"2885".to_string()) - ); - assert_eq!(state.companies[0].current_cash, 550_000); - assert_eq!(state.companies[0].debt, 0); - assert_eq!( - state.service_state.annual_bond_last_retired_principal_total, - 350_000 - ); - assert_eq!( - state.service_state.annual_bond_last_issued_principal_total, - 0 - ); - assert_eq!(state.service_state.company_market_state[&25].bond_count, 0); - assert!( - state.service_state.company_market_state[&25] - .live_bond_slots - .is_empty() - ); - assert_eq!( - state.service_state.company_market_state[&25].largest_live_bond_principal, - None - ); - assert_eq!( - state.service_state.company_market_state[&25].highest_coupon_live_bond_principal, - None - ); - assert!(result.service_events.iter().any(|event| { - event.kind == "annual_finance_policy" - && event.applied_effect_count == 1 - && event.mutated_company_ids == vec![25] - && event.finance_news_family_candidates.get(&25) == Some(&"2885".to_string()) - && event.annual_finance_news_events.iter().any(|news| { - news.company_id == 25 - && news.selector_label == "2885" - && news.action_label == "bond_issue" - && news.retired_principal_total == 350_000 - && news.issued_principal_total == 0 - }) - })); - } - - #[test] - fn periodic_boundary_retires_then_reissues_exact_annual_bond_count() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - write_current_value( - &mut year_stat_family_qword_bits, - crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - -400_000.0, - ); - write_current_value(&mut year_stat_family_qword_bits, 0x12, -350_000.0); - - let mut state = crate::RuntimeState { - calendar: crate::CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..crate::RuntimeWorldRestoreState::default() - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 26, - current_cash: -400_000, - debt: 350_000, - active: true, - credit_rating_score: Some(7), - prime_rate: Some(6), - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - available_track_laying_capacity: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(26), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - company_market_state: BTreeMap::from([( - 26, - crate::RuntimeCompanyMarketState { - bond_count: 2, - linked_transit_latch: true, - live_bond_slots: vec![ - crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 200_000, - maturity_year: 1845, - coupon_rate_raw_u32: 0.09f32.to_bits(), - }, - crate::RuntimeCompanyBondSlot { - slot_index: 1, - principal: 150_000, - maturity_year: 1845, - coupon_rate_raw_u32: 0.08f32.to_bits(), - }, - ], - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should apply annual bond restructure lane"); - - assert_eq!( - state.service_state.annual_finance_last_actions.get(&26), - Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) - ); - assert_eq!( - state - .service_state - .annual_finance_last_news_family_candidates - .get(&26), - Some(&"2883".to_string()) - ); - assert_eq!(state.companies[0].current_cash, 250_000); - assert_eq!(state.companies[0].debt, 1_000_000); - assert_eq!( - state.service_state.annual_bond_last_retired_principal_total, - 350_000 - ); - assert_eq!( - state.service_state.annual_bond_last_issued_principal_total, - 1_000_000 - ); - assert_eq!(state.service_state.company_market_state[&26].bond_count, 2); - assert_eq!( - state.service_state.company_market_state[&26].largest_live_bond_principal, - Some(500_000) - ); - assert_eq!( - state.service_state.company_market_state[&26].highest_coupon_live_bond_principal, - Some(500_000) - ); - assert_eq!( - state.service_state.company_market_state[&26].live_bond_slots, - vec![ - crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 500_000, - maturity_year: 1875, - coupon_rate_raw_u32: 0.09f32.to_bits(), - }, - crate::RuntimeCompanyBondSlot { - slot_index: 1, - principal: 500_000, - maturity_year: 1875, - coupon_rate_raw_u32: 0.09f32.to_bits(), - }, - ] - ); - assert!(result.service_events.iter().any(|event| { - event.kind == "annual_finance_policy" - && event.applied_effect_count == 1 - && event.mutated_company_ids == vec![26] - && event.finance_news_family_candidates.get(&26) == Some(&"2883".to_string()) - && event.annual_finance_news_events.iter().any(|news| { - news.company_id == 26 - && news.selector_label == "2883" - && news.action_label == "bond_issue" - && news.retired_principal_total == 350_000 - && news.issued_principal_total == 1_000_000 - }) - })); - } - - #[test] - fn periodic_boundary_applies_creditor_pressure_bankruptcy_from_annual_finance_policy() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = - (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value( - &mut year_stat_family_qword_bits, - crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - -200_000.0, - ); - write_current_value(&mut year_stat_family_qword_bits, 0x12, -500_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 95_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -125_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -115_000.0); - - let mut state = RuntimeState { - calendar: crate::CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..crate::RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 31, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - current_cash: 0, - debt: 200_000, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(31), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: vec![crate::RuntimeTrain { - train_id: 88, - owner_company_id: 31, - territory_id: None, - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }], - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - company_market_state: BTreeMap::from([( - 31, - crate::RuntimeCompanyMarketState { - outstanding_shares: 10_000, - bond_count: 1, - largest_live_bond_principal: Some(500_000), - highest_coupon_live_bond_principal: Some(500_000), - cached_share_price_raw_u32: 25.0f32.to_bits(), - founding_year: 1841, - last_bankruptcy_year: 1832, - year_stat_family_qword_bits, - live_bond_slots: vec![crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 500_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.08f32.to_bits(), - }], - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should apply creditor-pressure bankruptcy"); - - assert_eq!( - state.service_state.annual_finance_last_actions.get(&31), - Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy) - ); - assert_eq!( - state - .service_state - .annual_finance_last_news_family_candidates - .get(&31), - Some(&"2881".to_string()) - ); - assert_eq!(state.companies[0].current_cash, 0); - assert_eq!(state.companies[0].debt, 250_000); - assert!(state.companies[0].active); - assert_eq!(state.selected_company_id, Some(31)); - assert!(!state.trains[0].retired); - assert_eq!( - state.service_state.company_market_state[&31].last_bankruptcy_year, - 1845 - ); - assert_eq!(state.service_state.company_market_state[&31].bond_count, 1); - assert_eq!( - state.service_state.company_market_state[&31].largest_live_bond_principal, - Some(250_000) - ); - assert_eq!( - state.service_state.company_market_state[&31].highest_coupon_live_bond_principal, - Some(250_000) - ); - assert!( - state.service_state.company_market_state[&31].live_bond_slots - == vec![crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 250_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.08f32.to_bits(), - }] - ); - assert!( - result - .service_events - .iter() - .any(|event| event.kind == "annual_finance_policy" - && event.applied_effect_count == 1 - && event.mutated_company_ids == vec![31] - && event.finance_news_family_candidates.get(&31) == Some(&"2881".to_string())) - ); - } - - #[test] - fn periodic_boundary_applies_deep_distress_bankruptcy_fallback_from_annual_finance_policy() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = - (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value( - &mut year_stat_family_qword_bits, - crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - -350_000.0, - ); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0); - - let mut state = RuntimeState { - calendar: crate::CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..crate::RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 32, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - current_cash: 0, - debt: 50_000, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(32), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: vec![crate::RuntimeTrain { - train_id: 89, - owner_company_id: 32, - territory_id: None, - locomotive_name: Some("Orca".to_string()), - active: true, - retired: false, - }], - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - company_market_state: BTreeMap::from([( - 32, - crate::RuntimeCompanyMarketState { - outstanding_shares: 8_000, - bond_count: 1, - largest_live_bond_principal: Some(250_000), - highest_coupon_live_bond_principal: Some(250_000), - founding_year: 1841, - last_bankruptcy_year: 1840, - year_stat_family_qword_bits, - live_bond_slots: vec![crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 250_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.07f32.to_bits(), - }], - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) - .expect("periodic boundary should apply deep-distress bankruptcy fallback"); - - assert_eq!( - state.service_state.annual_finance_last_actions.get(&32), - Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback) - ); - assert_eq!( - state - .service_state - .annual_finance_last_news_family_candidates - .get(&32), - Some(&"2881".to_string()) - ); - assert_eq!(state.companies[0].current_cash, 0); - assert_eq!(state.companies[0].debt, 125_000); - assert!(state.companies[0].active); - assert_eq!(state.selected_company_id, Some(32)); - assert!(!state.trains[0].retired); - assert_eq!( - state.service_state.company_market_state[&32].last_bankruptcy_year, - 1845 - ); - assert_eq!(state.service_state.company_market_state[&32].bond_count, 1); - assert_eq!( - state.service_state.company_market_state[&32].largest_live_bond_principal, - Some(125_000) - ); - assert_eq!( - state.service_state.company_market_state[&32].highest_coupon_live_bond_principal, - Some(125_000) - ); - assert!( - state.service_state.company_market_state[&32].live_bond_slots - == vec![crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 125_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.07f32.to_bits(), - }] - ); - assert!( - result - .service_events - .iter() - .any(|event| event.kind == "annual_finance_policy" - && event.applied_effect_count == 1 - && event.mutated_company_ids == vec![32] - && event.finance_news_family_candidates.get(&32) == Some(&"2881".to_string())) - ); - } - - #[test] - fn applies_company_effects_for_specific_targets() { - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Unknown, - current_cash: 10, - debt: 5, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Unknown, - current_cash: 20, - debt: 8, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 10, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::Ids { ids: vec![2] }, - delta: 4, - }, - RuntimeEffect::AdjustCompanyDebt { - target: RuntimeCompanyTarget::Ids { ids: vec![2] }, - delta: -3, - }, - ], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("targeted company effects should succeed"); - - assert_eq!(state.companies[0].current_cash, 10); - assert_eq!(state.companies[1].current_cash, 24); - assert_eq!(state.companies[1].debt, 5); - assert_eq!(result.service_events[0].applied_effect_count, 2); - assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); - } - - #[test] - fn set_company_cash_updates_owner_state_backed_current_cash() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = 100.0f64.to_bits(); - - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Unknown, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 10, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetCompanyCash { - target: RuntimeCompanyTarget::Ids { ids: vec![1] }, - value: 250, - }], - }], - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - year_stat_family_qword_bits, - special_stat_family_232a_qword_bits: vec![0u64; 0x20], - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - calendar: crate::CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("cash effect should apply through owner state"); - - assert_eq!(state.companies[0].current_cash, 250); - assert_eq!( - crate::runtime::runtime_company_stat_value( - &state, - 1, - crate::RuntimeCompanyStatSelector { - family_id: crate::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - }, - ), - Some(250) - ); - } - - #[test] - fn set_company_governance_scalar_updates_owner_state_backed_metrics() { - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Unknown, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 11, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget::Ids { ids: vec![1] }, - metric: RuntimeCompanyMetric::PrimeRate, - value: 6, - }, - RuntimeEffect::SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget::Ids { ids: vec![1] }, - metric: RuntimeCompanyMetric::BookValuePerShare, - value: 2620, - }, - RuntimeEffect::SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget::Ids { ids: vec![1] }, - metric: RuntimeCompanyMetric::InvestorConfidence, - value: 37, - }, - RuntimeEffect::SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget::Ids { ids: vec![1] }, - metric: RuntimeCompanyMetric::ManagementAttitude, - value: 58, - }, - ], - }], - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: { - let mut terms = vec![0; 0x3b]; - terms[crate::RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 0; - terms - }, - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - issue_opinion_terms_raw_i32: vec![0; 0x3b], - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - calendar: crate::CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState { - issue_37_value: Some(5.0f32.to_bits()), - ..crate::RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("governance effect should apply through owner state"); - - assert_eq!(state.companies[0].prime_rate, Some(6)); - assert_eq!(state.companies[0].book_value_per_share, 2620); - assert_eq!(state.companies[0].investor_confidence, 37); - assert_eq!(state.companies[0].management_attitude, 58); - assert_eq!( - crate::runtime::runtime_company_prime_rate(&state, 1), - Some(6) - ); - assert_eq!( - crate::runtime::runtime_company_book_value_per_share(&state, 1), - Some(2620) - ); - assert_eq!( - crate::runtime::runtime_company_investor_confidence(&state, 1), - Some(37) - ); - assert_eq!( - crate::runtime::runtime_company_management_attitude(&state, 1), - Some(58) - ); - assert_eq!( - state.service_state.company_market_state[&1] - .direct_control_transfer_float_fields_raw_u32 - .get(&0x32f) - .copied(), - Some(2620.0f32.to_bits()) - ); - assert_eq!( - state.service_state.company_market_state[&1].cached_share_price_raw_u32, - 37.0f32.to_bits() - ); - assert_eq!( - state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32 - [crate::RUNTIME_WORLD_ISSUE_PRIME_RATE as usize], - 100 - ); - assert_eq!( - state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32 - [crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize], - 58 - ); - } - - #[test] - fn set_company_credit_rating_governance_scalar_updates_issue38_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits - [(0x12 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 20.0f64.to_bits(); - year_stat_family_qword_bits - [(0x01 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = - 100.0f64.to_bits(); - year_stat_family_qword_bits - [(0x09 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = 0.0f64.to_bits(); - - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - world_restore: RuntimeWorldRestoreState { - issue_37_value: Some(5.0f32.to_bits()), - issue_38_value: Some(2), - packed_year_word_raw_u16: Some(1835), - ..RuntimeWorldRestoreState::default() - }, - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1830, - last_bankruptcy_year: 1800, - year_stat_family_qword_bits, - issue_opinion_terms_raw_i32: vec![0; 0x3b], - live_bond_slots: vec![crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 100_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.05f32.to_bits(), - }], - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 13, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetCompanyGovernanceScalar { - target: RuntimeCompanyTarget::Ids { ids: vec![1] }, - metric: RuntimeCompanyMetric::CreditRating, - value: 7, - }], - }], - calendar: crate::CalendarPoint { - year: 1835, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - metadata: BTreeMap::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("credit-rating governance effect should apply through owner state"); - - assert_eq!(state.companies[0].credit_rating_score, Some(7)); - assert_eq!( - crate::runtime::runtime_company_credit_rating(&state, 1), - Some(7) - ); - assert_eq!( - state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32 - [crate::RUNTIME_WORLD_ISSUE_CREDIT_MARKET as usize], - -3 - ); - } - - #[test] - fn adjust_company_cash_updates_owner_state_backed_current_cash() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = 100.0f64.to_bits(); - - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Unknown, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 12, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::Ids { ids: vec![1] }, - delta: 25, - }], - }], - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - year_stat_family_qword_bits, - special_stat_family_232a_qword_bits: vec![0u64; 0x20], - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - calendar: crate::CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("cash adjustment should apply through owner state"); - - assert_eq!(state.companies[0].current_cash, 125); - assert_eq!( - crate::runtime::runtime_company_stat_value( - &state, - 1, - crate::RuntimeCompanyStatSelector { - family_id: crate::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - }, - ), - Some(125) - ); - } - - #[test] - fn applies_named_locomotive_availability_effects() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 10, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::SetNamedLocomotiveAvailability { - name: "Big Boy".to_string(), - value: false, - }, - RuntimeEffect::SetNamedLocomotiveAvailability { - name: "GP7".to_string(), - value: true, - }, - ], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("named locomotive availability effects should succeed"); - - assert_eq!(state.named_locomotive_availability.get("Big Boy"), Some(&0)); - assert_eq!(state.named_locomotive_availability.get("GP7"), Some(&1)); - assert_eq!(result.service_events[0].applied_effect_count, 2); - } - - #[test] - fn applies_named_locomotive_cost_effects() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 11, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::SetNamedLocomotiveCost { - name: "Big Boy".to_string(), - value: 250000, - }, - RuntimeEffect::SetNamedLocomotiveCost { - name: "GP7".to_string(), - value: 175000, - }, - ], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("named locomotive cost effects should succeed"); - - assert_eq!(state.named_locomotive_cost.get("Big Boy"), Some(&250000)); - assert_eq!(state.named_locomotive_cost.get("GP7"), Some(&175000)); - assert_eq!(result.service_events[0].applied_effect_count, 2); - } - - #[test] - fn applies_scalar_named_locomotive_availability_effects() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 12, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::SetNamedLocomotiveAvailabilityValue { - name: "Big Boy".to_string(), - value: 42, - }, - RuntimeEffect::SetNamedLocomotiveAvailabilityValue { - name: "GP7".to_string(), - value: 7, - }, - ], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("scalar named locomotive availability effects should succeed"); - - assert_eq!( - state.named_locomotive_availability.get("Big Boy"), - Some(&42) - ); - assert_eq!(state.named_locomotive_availability.get("GP7"), Some(&7)); - assert_eq!(result.service_events[0].applied_effect_count, 2); - } - - #[test] - fn applies_world_scalar_override_effects() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 13, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::SetWorldScalarOverride { - key: "world.build_stations_cost".to_string(), - value: 350000, - }, - RuntimeEffect::SetCargoPriceOverride { - target: RuntimeCargoPriceTarget::All, - value: 180, - }, - RuntimeEffect::SetCargoPriceOverride { - target: RuntimeCargoPriceTarget::Named { - name: "Coal".to_string(), - }, - value: 95, - }, - RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::Factory, - value: 225, - }, - RuntimeEffect::SetCargoProductionOverride { - target: RuntimeCargoProductionTarget::Named { - name: "Corn".to_string(), - }, - value: 140, - }, - RuntimeEffect::SetCargoProductionSlot { - slot: 1, - value: 125, - }, - RuntimeEffect::SetTerritoryAccessCost { value: 750000 }, - ], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("world scalar override effects should succeed"); - - assert_eq!( - state - .world_scalar_overrides - .get("world.build_stations_cost"), - Some(&350000) - ); - assert_eq!(state.all_cargo_price_override, Some(180)); - assert_eq!(state.named_cargo_price_overrides.get("Coal"), Some(&95)); - assert_eq!(state.factory_cargo_production_override, Some(225)); - assert_eq!( - state.named_cargo_production_overrides.get("Corn"), - Some(&140) - ); - assert_eq!(state.cargo_production_overrides.get(&1), Some(&125)); - assert_eq!(state.world_restore.territory_access_cost, Some(750000)); - assert_eq!(result.service_events[0].applied_effect_count, 7); - } - - #[test] - fn applies_locomotive_policy_world_flags_through_owner_state() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 213, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::SetWorldFlag { - key: "world.all_steam_locos_available".to_string(), - value: true, - }, - RuntimeEffect::SetWorldFlag { - key: "world.all_diesel_locos_available".to_string(), - value: false, - }, - RuntimeEffect::SetWorldFlag { - key: "world.all_electric_locos_available".to_string(), - value: true, - }, - ], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("locomotive policy world flags should succeed"); - - assert_eq!( - state.world_flags.get("world.all_steam_locos_available"), - Some(&true) - ); - assert_eq!( - state.world_restore.all_steam_locomotives_available_raw_u8, - Some(1) - ); - assert_eq!( - state.world_restore.all_diesel_locomotives_available_raw_u8, - Some(0) - ); - assert_eq!( - state - .world_restore - .all_electric_locomotives_available_raw_u8, - Some(1) - ); - assert_eq!(result.service_events[0].applied_effect_count, 3); - } - - #[test] - fn applies_runtime_variable_effects() { - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 10, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 20, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - players: vec![RuntimePlayer { - player_id: 9, - current_cash: 0, - active: true, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - territories: vec![RuntimeTerritory { - territory_id: 7, - name: Some("North".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 14, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::SetWorldVariable { - index: 1, - value: -5, - }, - RuntimeEffect::SetCompanyVariable { - target: RuntimeCompanyTarget::AllActive, - index: 2, - value: 17, - }, - RuntimeEffect::SetPlayerVariable { - target: RuntimePlayerTarget::Ids { ids: vec![9] }, - index: 3, - value: 99, - }, - RuntimeEffect::SetTerritoryVariable { - target: RuntimeTerritoryTarget::Ids { ids: vec![7] }, - index: 4, - value: 1234, - }, - ], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("runtime variable effects should succeed"); - - assert_eq!(state.world_runtime_variables.get(&1), Some(&-5)); - assert_eq!( - state - .company_runtime_variables - .get(&1) - .and_then(|vars| vars.get(&2)), - Some(&17) - ); - assert_eq!( - state - .company_runtime_variables - .get(&2) - .and_then(|vars| vars.get(&2)), - Some(&17) - ); - assert_eq!( - state - .player_runtime_variables - .get(&9) - .and_then(|vars| vars.get(&3)), - Some(&99) - ); - assert_eq!( - state - .territory_runtime_variables - .get(&7) - .and_then(|vars| vars.get(&4)), - Some(&1234) - ); - assert_eq!(result.service_events[0].applied_effect_count, 4); - } - - #[test] - fn resolves_symbolic_company_targets() { - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 10, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 20, - debt: 2, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - selected_company_id: Some(1), - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 11, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::HumanCompanies, - delta: 5, - }], - }, - RuntimeEventRecord { - record_id: 12, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyDebt { - target: RuntimeCompanyTarget::AiCompanies, - delta: 3, - }], - }, - RuntimeEventRecord { - record_id: 13, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::SelectedCompany, - delta: 7, - }], - }, - ], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("symbolic target effects should succeed"); - - assert_eq!(state.companies[0].current_cash, 22); - assert_eq!(state.companies[0].debt, 0); - assert_eq!(state.companies[1].current_cash, 20); - assert_eq!(state.companies[1].debt, 5); - assert_eq!(result.service_events[0].mutated_company_ids, vec![1, 2]); - } - - #[test] - fn rejects_selected_company_target_without_selection_context() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 14, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::SelectedCompany, - delta: 1, - }], - }], - ..state() - }; - - let error = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect_err("selected company target should require selection context"); - - assert!(error.contains("selected_company_id")); - } - - #[test] - fn rejects_human_or_ai_targets_without_role_context() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 15, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::HumanCompanies, - delta: 1, - }], - }], - ..state() - }; - - let error = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect_err("human target should require controller metadata"); - - assert!(error.contains("controller_kind")); - } - - #[test] - fn all_active_and_role_targets_exclude_inactive_companies() { - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 10, - debt: 1, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 20, - debt: 2, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: false, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 3, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 30, - debt: 3, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 16, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::AllActive, - delta: 5, - }], - }, - RuntimeEventRecord { - record_id: 17, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyDebt { - target: RuntimeCompanyTarget::HumanCompanies, - delta: 4, - }], - }, - RuntimeEventRecord { - record_id: 18, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyDebt { - target: RuntimeCompanyTarget::AiCompanies, - delta: 6, - }], - }, - ], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("active-company filtering should succeed"); - - assert_eq!(state.companies[0].current_cash, 15); - assert_eq!(state.companies[1].current_cash, 20); - assert_eq!(state.companies[2].current_cash, 35); - assert_eq!(state.companies[0].debt, 5); - assert_eq!(state.companies[1].debt, 2); - assert_eq!(state.companies[2].debt, 9); - } - - #[test] - fn deactivating_selected_company_clears_selection() { - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 10, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: Some(8), - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(1), - event_runtime_records: vec![RuntimeEventRecord { - record_id: 19, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::DeactivateCompany { - target: RuntimeCompanyTarget::SelectedCompany, - }], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("deactivate company effect should succeed"); - - assert!(!state.companies[0].active); - assert_eq!(state.selected_company_id, None); - assert_eq!(result.service_events[0].mutated_company_ids, vec![1]); - } - - #[test] - fn deactivating_selected_player_clears_selection() { - let mut state = RuntimeState { - players: vec![RuntimePlayer { - player_id: 1, - current_cash: 500, - active: true, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_player_id: Some(1), - event_runtime_records: vec![RuntimeEventRecord { - record_id: 19, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::DeactivatePlayer { - target: crate::RuntimePlayerTarget::SelectedPlayer, - }], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("deactivate player effect should succeed"); - - assert!(!state.players[0].active); - assert_eq!(state.selected_player_id, None); - assert_eq!(result.service_events[0].mutated_player_ids, vec![1]); - } - - #[test] - fn sets_track_laying_capacity_for_resolved_targets() { - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 10, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 20, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 20, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { - target: RuntimeCompanyTarget::Ids { ids: vec![2] }, - value: Some(14), - }], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("track capacity effect should succeed"); - - assert_eq!(state.companies[0].available_track_laying_capacity, None); - assert_eq!(state.companies[1].available_track_laying_capacity, Some(14)); - assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); - } - - #[test] - fn sets_and_clears_company_territory_access_for_resolved_targets() { - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 10, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 20, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - territories: vec![ - RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - RuntimeTerritory { - territory_id: 8, - name: Some("Great Plains".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - ], - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 21, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: true, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetCompanyTerritoryAccess { - target: RuntimeCompanyTarget::SelectedCompany, - territory: RuntimeTerritoryTarget::Ids { ids: vec![7, 8] }, - value: true, - }], - }, - RuntimeEventRecord { - record_id: 22, - trigger_kind: 8, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: true, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetCompanyTerritoryAccess { - target: RuntimeCompanyTarget::SelectedCompany, - territory: RuntimeTerritoryTarget::Ids { ids: vec![8] }, - value: false, - }], - }, - ], - selected_company_id: Some(1), - ..state() - }; - - let first = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("territory access grant should succeed"); - - assert_eq!( - state.company_territory_access, - vec![ - crate::RuntimeCompanyTerritoryAccess { - company_id: 1, - territory_id: 7, - }, - crate::RuntimeCompanyTerritoryAccess { - company_id: 1, - territory_id: 8, - }, - ] - ); - assert_eq!(first.service_events[0].mutated_company_ids, vec![1]); - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 8 }, - ) - .expect("territory access clear should succeed"); - - assert_eq!( - state.company_territory_access, - vec![crate::RuntimeCompanyTerritoryAccess { - company_id: 1, - territory_id: 7, - }] - ); - } - - #[test] - fn rejects_condition_true_company_target_without_condition_context() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 16, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyCash { - target: RuntimeCompanyTarget::ConditionTrueCompany, - delta: 1, - }], - }], - ..state() - }; - - let error = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect_err("condition-relative target should remain blocked"); - - assert!(error.contains("condition-evaluation context")); - } - - #[test] - fn evaluates_world_state_conditions_before_effects_run() { - let mut state = RuntimeState { - world_restore: RuntimeWorldRestoreState { - economic_status_code: Some(3), - ..RuntimeWorldRestoreState::default() - }, - world_flags: BTreeMap::from([( - String::from("world.disable_stock_buying_and_selling"), - true, - )]), - candidate_availability: BTreeMap::from([(String::from("Mogul"), 2)]), - special_conditions: BTreeMap::from([(String::from("Use Wartime Cargos"), 1)]), - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 23, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![ - RuntimeCondition::SpecialConditionThreshold { - label: "Use Wartime Cargos".to_string(), - comparator: RuntimeConditionComparator::Ge, - value: 1, - }, - RuntimeCondition::CandidateAvailabilityThreshold { - name: "Mogul".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 2, - }, - RuntimeCondition::EconomicStatusCodeThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 3, - }, - RuntimeCondition::WorldFlagEquals { - key: "world.disable_stock_buying_and_selling".to_string(), - value: true, - }, - ], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world_condition_passed".to_string(), - value: true, - }], - }, - RuntimeEventRecord { - record_id: 24, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::SpecialConditionThreshold { - label: "Disable Cargo Economy".to_string(), - comparator: RuntimeConditionComparator::Gt, - value: 0, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world_condition_failed".to_string(), - value: true, - }], - }, - ], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("world-state conditions should evaluate successfully"); - - assert_eq!(result.service_events[0].serviced_record_ids, vec![23]); - assert_eq!(state.world_flags.get("world_condition_passed"), Some(&true)); - assert_eq!(state.world_flags.get("world_condition_failed"), None); - } - - #[test] - fn evaluates_world_scalar_conditions_before_effects_run() { - let mut state = RuntimeState { - world_restore: RuntimeWorldRestoreState { - limited_track_building_amount: Some(18), - territory_access_cost: Some(750000), - ..RuntimeWorldRestoreState::default() - }, - cargo_catalog: vec![ - crate::RuntimeCargoCatalogEntry { - slot_id: 1, - label: "Cargo Production Slot 1".to_string(), - cargo_class: RuntimeCargoClass::Factory, - supplied_token_stem: None, - demanded_token_stem: None, - }, - crate::RuntimeCargoCatalogEntry { - slot_id: 5, - label: "Cargo Production Slot 5".to_string(), - cargo_class: RuntimeCargoClass::FarmMine, - supplied_token_stem: None, - demanded_token_stem: None, - }, - crate::RuntimeCargoCatalogEntry { - slot_id: 9, - label: "Cargo Production Slot 9".to_string(), - cargo_class: RuntimeCargoClass::Other, - supplied_token_stem: None, - demanded_token_stem: None, - }, - ], - named_locomotive_availability: BTreeMap::from([(String::from("Big Boy"), 42)]), - named_locomotive_cost: BTreeMap::from([(String::from("GP7"), 175000)]), - cargo_production_overrides: BTreeMap::from([(1, 125), (5, 75), (9, 30)]), - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 25, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![ - RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name: "Big Boy".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 42, - }, - RuntimeCondition::NamedLocomotiveCostThreshold { - name: "GP7".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 175000, - }, - RuntimeCondition::CargoProductionSlotThreshold { - slot: 1, - label: "Cargo Production Slot 1".to_string(), - comparator: RuntimeConditionComparator::Eq, - value: 125, - }, - RuntimeCondition::CargoProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 230, - }, - RuntimeCondition::FactoryProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 125, - }, - RuntimeCondition::FarmMineProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 75, - }, - RuntimeCondition::OtherCargoProductionTotalThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 30, - }, - RuntimeCondition::LimitedTrackBuildingAmountThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 18, - }, - RuntimeCondition::TerritoryAccessCostThreshold { - comparator: RuntimeConditionComparator::Eq, - value: 750000, - }, - ], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world_scalar_condition_passed".to_string(), - value: true, - }], - }, - RuntimeEventRecord { - record_id: 26, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold { - name: "Missing Loco".to_string(), - comparator: RuntimeConditionComparator::Gt, - value: 0, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world_scalar_condition_failed".to_string(), - value: true, - }], - }, - ], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("world-scalar conditions should evaluate successfully"); - - assert_eq!(result.service_events[0].serviced_record_ids, vec![25]); - assert_eq!( - state.world_flags.get("world_scalar_condition_passed"), - Some(&true) - ); - assert_eq!(state.world_flags.get("world_scalar_condition_failed"), None); - } - - #[test] - fn evaluates_runtime_variable_conditions_before_effects_run() { - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 10, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - selected_company_id: Some(1), - players: vec![RuntimePlayer { - player_id: 1, - current_cash: 50, - active: true, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_player_id: Some(1), - territories: vec![RuntimeTerritory { - territory_id: 7, - name: Some("North".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - world_runtime_variables: BTreeMap::from([(1, 111)]), - company_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(2, 222)]))]), - player_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(3, 333)]))]), - territory_runtime_variables: BTreeMap::from([(7, BTreeMap::from([(4, 444)]))]), - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 27, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![ - RuntimeCondition::WorldVariableThreshold { - index: 1, - comparator: RuntimeConditionComparator::Eq, - value: 111, - }, - RuntimeCondition::CompanyVariableThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - index: 2, - comparator: RuntimeConditionComparator::Eq, - value: 222, - }, - RuntimeCondition::PlayerVariableThreshold { - target: RuntimePlayerTarget::SelectedPlayer, - index: 3, - comparator: RuntimeConditionComparator::Eq, - value: 333, - }, - RuntimeCondition::TerritoryVariableThreshold { - target: RuntimeTerritoryTarget::Ids { ids: vec![7] }, - index: 4, - comparator: RuntimeConditionComparator::Eq, - value: 444, - }, - ], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "runtime_variable_condition_passed".to_string(), - value: true, - }], - }, - RuntimeEventRecord { - record_id: 28, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::PlayerVariableThreshold { - target: RuntimePlayerTarget::SelectedPlayer, - index: 4, - comparator: RuntimeConditionComparator::Gt, - value: 0, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "runtime_variable_condition_failed".to_string(), - value: true, - }], - }, - ], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("runtime-variable conditions should evaluate successfully"); - - assert_eq!(result.service_events[0].serviced_record_ids, vec![27]); - assert_eq!( - state.world_flags.get("runtime_variable_condition_passed"), - Some(&true) - ); - assert_eq!( - state.world_flags.get("runtime_variable_condition_failed"), - None - ); - } - - #[test] - fn one_shot_record_only_fires_once() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 20, - trigger_kind: 2, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: true, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetWorldFlag { - key: "one_shot".to_string(), - value: true, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 2 }, - ) - .expect("first one-shot service should succeed"); - let second = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 2 }, - ) - .expect("second one-shot service should succeed"); - - assert_eq!(state.event_runtime_records[0].service_count, 1); - assert!(state.event_runtime_records[0].has_fired); - assert_eq!( - second.service_events[0].serviced_record_ids, - Vec::::new() - ); - assert_eq!(second.service_events[0].applied_effect_count, 0); - } - - #[test] - fn rejects_debt_underflow() { - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Unknown, - current_cash: 10, - debt: 2, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 30, - trigger_kind: 3, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AdjustCompanyDebt { - target: RuntimeCompanyTarget::AllActive, - delta: -3, - }], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 3 }, - ); - - assert!(result.is_err()); - } - - #[test] - fn appended_record_waits_until_later_pass_without_dirty_rerun() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 40, - trigger_kind: 5, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: true, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AppendEventRecord { - record: Box::new(RuntimeEventRecordTemplate { - record_id: 41, - trigger_kind: 5, - active: true, - marks_collection_dirty: false, - one_shot: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetWorldFlag { - key: "follow_on_later_pass".to_string(), - value: true, - }], - }), - }], - }], - ..state() - }; - - let first = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 5 }, - ) - .expect("first pass should succeed"); - - assert_eq!(first.service_events.len(), 1); - assert_eq!(first.service_events[0].serviced_record_ids, vec![40]); - assert_eq!(first.service_events[0].appended_record_ids, vec![41]); - assert_eq!(state.world_flags.get("follow_on_later_pass"), None); - assert_eq!(state.event_runtime_records.len(), 2); - assert_eq!(state.event_runtime_records[1].service_count, 0); - - let second = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 5 }, - ) - .expect("second pass should succeed"); - - assert_eq!(second.service_events[0].serviced_record_ids, vec![41]); - assert_eq!(state.world_flags.get("follow_on_later_pass"), Some(&true)); - assert!(state.event_runtime_records[0].has_fired); - assert_eq!(state.event_runtime_records[1].service_count, 1); - } - - #[test] - fn appended_record_runs_in_dirty_rerun_after_commit() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 50, - trigger_kind: 1, - active: true, - service_count: 0, - marks_collection_dirty: true, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AppendEventRecord { - record: Box::new(RuntimeEventRecordTemplate { - record_id: 51, - trigger_kind: 0x0a, - active: true, - marks_collection_dirty: false, - one_shot: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetWorldFlag { - key: "dirty_rerun_follow_on".to_string(), - value: true, - }], - }), - }], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 1 }, - ) - .expect("dirty rerun with follow-on should succeed"); - - assert_eq!(result.service_events.len(), 2); - assert_eq!(result.service_events[0].serviced_record_ids, vec![50]); - assert_eq!(result.service_events[0].appended_record_ids, vec![51]); - assert_eq!(result.service_events[1].trigger_kind, Some(0x0a)); - assert_eq!(result.service_events[1].serviced_record_ids, vec![51]); - assert_eq!(state.service_state.dirty_rerun_count, 1); - assert_eq!(state.event_runtime_records.len(), 2); - assert_eq!(state.event_runtime_records[1].service_count, 1); - assert_eq!(state.world_flags.get("dirty_rerun_follow_on"), Some(&true)); - } - - #[test] - fn lifecycle_mutations_commit_between_passes() { - let mut state = RuntimeState { - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 60, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: true, - has_fired: false, - conditions: Vec::new(), - effects: vec![ - RuntimeEffect::AppendEventRecord { - record: Box::new(RuntimeEventRecordTemplate { - record_id: 64, - trigger_kind: 7, - active: true, - marks_collection_dirty: false, - one_shot: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetCandidateAvailability { - name: "Appended Industry".to_string(), - value: 1, - }], - }), - }, - RuntimeEffect::DeactivateEventRecord { record_id: 61 }, - RuntimeEffect::ActivateEventRecord { record_id: 62 }, - RuntimeEffect::RemoveEventRecord { record_id: 63 }, - ], - }, - RuntimeEventRecord { - record_id: 61, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetWorldFlag { - key: "deactivated_after_first_pass".to_string(), - value: true, - }], - }, - RuntimeEventRecord { - record_id: 62, - trigger_kind: 7, - active: false, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetSpecialCondition { - label: "Activated On Second Pass".to_string(), - value: 1, - }], - }, - RuntimeEventRecord { - record_id: 63, - trigger_kind: 7, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetWorldFlag { - key: "removed_after_first_pass".to_string(), - value: true, - }], - }, - ], - ..state() - }; - - let first = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("first lifecycle pass should succeed"); - - assert_eq!( - first.service_events[0].serviced_record_ids, - vec![60, 61, 63] - ); - assert_eq!(first.service_events[0].appended_record_ids, vec![64]); - assert_eq!(first.service_events[0].activated_record_ids, vec![62]); - assert_eq!(first.service_events[0].deactivated_record_ids, vec![61]); - assert_eq!(first.service_events[0].removed_record_ids, vec![63]); - assert_eq!( - state - .event_runtime_records - .iter() - .map(|record| (record.record_id, record.active)) - .collect::>(), - vec![(60, true), (61, false), (62, true), (64, true)] - ); - - let second = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, - ) - .expect("second lifecycle pass should succeed"); - - assert_eq!(second.service_events[0].serviced_record_ids, vec![62, 64]); - assert_eq!( - state.special_conditions.get("Activated On Second Pass"), - Some(&1) - ); - assert_eq!( - state.candidate_availability.get("Appended Industry"), - Some(&1) - ); - assert_eq!( - state.world_flags.get("deactivated_after_first_pass"), - Some(&true) - ); - assert_eq!( - state.world_flags.get("removed_after_first_pass"), - Some(&true) - ); - } - - #[test] - fn rejects_duplicate_appended_record_id() { - let mut state = RuntimeState { - event_runtime_records: vec![ - RuntimeEventRecord { - record_id: 70, - trigger_kind: 4, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::AppendEventRecord { - record: Box::new(RuntimeEventRecordTemplate { - record_id: 71, - trigger_kind: 4, - active: true, - marks_collection_dirty: false, - one_shot: false, - conditions: Vec::new(), - effects: Vec::new(), - }), - }], - }, - RuntimeEventRecord { - record_id: 71, - trigger_kind: 4, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: Vec::new(), - }, - ], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 4 }, - ); - - assert!(result.is_err()); - } - - #[test] - fn rejects_missing_lifecycle_mutation_target() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 80, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::ActivateEventRecord { record_id: 999 }], - }], - ..state() - }; - - let result = execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ); - - assert!(result.is_err()); - } - - #[test] - fn applies_economic_status_code_effect() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 90, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetEconomicStatusCode { value: 3 }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("economic-status effect should succeed"); - - assert_eq!(state.world_restore.economic_status_code, Some(3)); - } - - #[test] - fn applies_limited_track_building_amount_effect() { - let mut state = RuntimeState { - event_runtime_records: vec![RuntimeEventRecord { - record_id: 91, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("limited-track-building-amount effect should succeed"); - - assert_eq!(state.world_restore.limited_track_building_amount, Some(18)); - } - - #[test] - fn confiscate_company_assets_zeros_company_and_retires_owned_trains() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = 50.0f64.to_bits(); - - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 50, - debt: 7, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 80, - debt: 9, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - selected_company_id: Some(1), - trains: vec![ - RuntimeTrain { - train_id: 10, - owner_company_id: 1, - territory_id: None, - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 11, - owner_company_id: 2, - territory_id: None, - locomotive_name: Some("Orca".to_string()), - active: true, - retired: false, - }, - ], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 91, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::ConfiscateCompanyAssets { - target: RuntimeCompanyTarget::SelectedCompany, - }], - }], - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - year_stat_family_qword_bits, - special_stat_family_232a_qword_bits: vec![0u64; 0x20], - live_bond_slots: vec![crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 20_000, - maturity_year: 1845, - coupon_rate_raw_u32: 0.05f32.to_bits(), - }], - bond_count: 1, - largest_live_bond_principal: Some(20_000), - highest_coupon_live_bond_principal: Some(20_000), - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("confiscation effect should succeed"); - - assert_eq!(state.companies[0].current_cash, 0); - assert_eq!(state.companies[0].debt, 0); - assert!(!state.companies[0].active); - assert_eq!(state.selected_company_id, None); - assert!(state.trains[0].retired); - assert!(!state.trains[1].retired); - assert_eq!( - crate::runtime::runtime_company_stat_value( - &state, - 1, - crate::RuntimeCompanyStatSelector { - family_id: crate::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, - slot_id: crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - }, - ), - Some(0) - ); - assert!( - state.service_state.company_market_state[&1] - .live_bond_slots - .is_empty() - ); - assert_eq!(state.service_state.company_market_state[&1].bond_count, 0); - assert_eq!( - state.service_state.company_market_state[&1].largest_live_bond_principal, - None - ); - assert_eq!( - state.service_state.company_market_state[&1].highest_coupon_live_bond_principal, - None - ); - } - - #[test] - fn retire_trains_respects_company_territory_and_locomotive_filters() { - let mut state = RuntimeState { - territories: vec![ - RuntimeTerritory { - territory_id: 7, - name: Some("Appalachia".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - RuntimeTerritory { - territory_id: 8, - name: Some("Great Plains".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - ], - trains: vec![ - RuntimeTrain { - train_id: 10, - owner_company_id: 1, - territory_id: Some(7), - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 11, - owner_company_id: 1, - territory_id: Some(7), - locomotive_name: Some("Orca".to_string()), - active: true, - retired: false, - }, - RuntimeTrain { - train_id: 12, - owner_company_id: 1, - territory_id: Some(8), - locomotive_name: Some("Mikado".to_string()), - active: true, - retired: false, - }, - ], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 92, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::RetireTrains { - company_target: Some(RuntimeCompanyTarget::SelectedCompany), - territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }), - locomotive_name: Some("Mikado".to_string()), - }], - }], - selected_company_id: Some(1), - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("retire-trains effect should succeed"); - - assert!(state.trains[0].retired); - assert!(!state.trains[1].retired); - assert!(!state.trains[2].retired); - } - - #[test] - fn set_chairman_cash_supports_all_active_target() { - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Human, - linked_chairman_profile_id: Some(1), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - RuntimeCompany { - company_id: 2, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Human, - linked_chairman_profile_id: Some(2), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }, - ], - chairman_profiles: vec![ - RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 10, - linked_company_id: Some(1), - company_holdings: BTreeMap::from([(1, 2)]), - holdings_value_total: 20, - net_worth_total: 30, - purchasing_power_total: 70, - }, - RuntimeChairmanProfile { - profile_id: 2, - name: "Chairman Two".to_string(), - active: true, - current_cash: 20, - linked_company_id: Some(2), - company_holdings: BTreeMap::from([(2, 3)]), - holdings_value_total: 60, - net_worth_total: 80, - purchasing_power_total: 110, - }, - RuntimeChairmanProfile { - profile_id: 3, - name: "Chairman Three".to_string(), - active: false, - current_cash: 30, - linked_company_id: None, - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 30, - purchasing_power_total: 30, - }, - ], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 93, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::SetChairmanCash { - target: RuntimeChairmanTarget::AllActive, - value: 77, - }], - }], - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([ - ( - 1, - crate::RuntimeCompanyMarketState { - cached_share_price_raw_u32: 0x41200000, - ..crate::RuntimeCompanyMarketState::default() - }, - ), - ( - 2, - crate::RuntimeCompanyMarketState { - cached_share_price_raw_u32: 0x41a00000, - ..crate::RuntimeCompanyMarketState::default() - }, - ), - ]), - ..RuntimeServiceState::default() - }, - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("all-active chairman cash effect should succeed"); - - assert_eq!(state.chairman_profiles[0].current_cash, 77); - assert_eq!(state.chairman_profiles[1].current_cash, 77); - assert_eq!(state.chairman_profiles[2].current_cash, 30); - assert_eq!(state.chairman_profiles[0].net_worth_total, 97); - assert_eq!(state.chairman_profiles[0].purchasing_power_total, 137); - assert_eq!(state.chairman_profiles[1].net_worth_total, 137); - assert_eq!(state.chairman_profiles[1].purchasing_power_total, 167); - } - - #[test] - fn deactivate_chairman_clears_selected_and_company_links_for_ids_target() { - let mut state = RuntimeState { - companies: vec![ - RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: Some(1), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - RuntimeCompany { - company_id: 2, - controller_kind: RuntimeCompanyControllerKind::Ai, - current_cash: 80, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: Some(2), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - }, - ], - chairman_profiles: vec![ - RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 10, - linked_company_id: Some(1), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }, - RuntimeChairmanProfile { - profile_id: 2, - name: "Chairman Two".to_string(), - active: true, - current_cash: 20, - linked_company_id: Some(2), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }, - ], - selected_chairman_profile_id: Some(2), - event_runtime_records: vec![RuntimeEventRecord { - record_id: 94, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: Vec::new(), - effects: vec![RuntimeEffect::DeactivateChairman { - target: RuntimeChairmanTarget::Ids { ids: vec![2] }, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("ids-target chairman deactivation should succeed"); - - assert!(state.chairman_profiles[0].active); - assert!(!state.chairman_profiles[1].active); - assert_eq!(state.chairman_profiles[1].linked_company_id, None); - assert_eq!(state.selected_chairman_profile_id, None); - assert_eq!(state.companies[0].linked_chairman_profile_id, Some(1)); - assert_eq!(state.companies[1].linked_chairman_profile_id, None); - } - - #[test] - fn company_governance_metric_conditions_gate_execution() { - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 2620, - investor_confidence: 37, - management_attitude: 58, - takeover_cooldown_year: Some(1844), - merger_cooldown_year: Some(1845), - }], - selected_company_id: Some(1), - event_runtime_records: vec![RuntimeEventRecord { - record_id: 95, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::BookValuePerShare, - comparator: RuntimeConditionComparator::Eq, - value: 2620, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world.book_value_gate_passed".to_string(), - value: true, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("book-value company condition should gate execution"); - - assert_eq!( - state.world_flags.get("world.book_value_gate_passed"), - Some(&true) - ); - } - - #[test] - fn derived_prime_rate_condition_reads_rehosted_issue_owner_state() { - let mut issue_terms = vec![0; 0x3b]; - issue_terms[crate::RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100; - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 2620, - investor_confidence: 37, - management_attitude: 58, - takeover_cooldown_year: Some(1844), - merger_cooldown_year: Some(1845), - }], - selected_company_id: Some(1), - world_restore: RuntimeWorldRestoreState { - issue_37_value: Some(5.0f32.to_bits()), - ..RuntimeWorldRestoreState::default() - }, - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: issue_terms, - ..RuntimeServiceState::default() - }, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 195, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::PrimeRate, - comparator: RuntimeConditionComparator::Eq, - value: 6, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world.derived_prime_rate_gate_passed".to_string(), - value: true, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("derived prime-rate company condition should gate execution"); - - assert_eq!( - state - .world_flags - .get("world.derived_prime_rate_gate_passed"), - Some(&true) - ); - } - - #[test] - fn derived_investor_confidence_condition_reads_rehosted_share_price_cache() { - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 2620, - investor_confidence: 0, - management_attitude: 58, - takeover_cooldown_year: Some(1844), - merger_cooldown_year: Some(1845), - }], - selected_company_id: Some(1), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - recent_per_share_subscore_raw_u32: 12.0f32.to_bits(), - cached_share_price_raw_u32: 37.0f32.to_bits(), - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 196, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::InvestorConfidence, - comparator: RuntimeConditionComparator::Eq, - value: 37, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world.derived_investor_confidence_gate_passed".to_string(), - value: true, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("derived investor-confidence company condition should gate execution"); - - assert_eq!( - state - .world_flags - .get("world.derived_investor_confidence_gate_passed"), - Some(&true) - ); - } - - #[test] - fn derived_management_attitude_condition_reads_issue3a_owner_state() { - let mut issue_terms = vec![0; 0x3b]; - issue_terms[crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 40; - let mut company_terms = vec![0; 0x3b]; - company_terms[crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 12; - let mut chairman_terms = vec![0; 0x3b]; - chairman_terms[crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 6; - - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: Some(3), - book_value_per_share: 2620, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: Some(1844), - merger_cooldown_year: Some(1845), - }], - selected_company_id: Some(1), - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 3, - name: "Chairman".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(1), - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: issue_terms, - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - issue_opinion_terms_raw_i32: company_terms, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(3, chairman_terms)]), - ..RuntimeServiceState::default() - }, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 196, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::ManagementAttitude, - comparator: RuntimeConditionComparator::Eq, - value: 58, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world.derived_management_attitude_gate_passed".to_string(), - value: true, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("derived management-attitude company condition should gate execution"); - - assert_eq!( - state - .world_flags - .get("world.derived_management_attitude_gate_passed"), - Some(&true) - ); - } - - #[test] - fn book_value_condition_reads_rehosted_direct_company_field_band() { - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 37, - management_attitude: 58, - takeover_cooldown_year: Some(1844), - merger_cooldown_year: Some(1845), - }], - selected_company_id: Some(1), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x32f, - 2620.0f32.to_bits(), - )]), - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 197, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::BookValuePerShare, - comparator: RuntimeConditionComparator::Eq, - value: 2620, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world.rehosted_book_value_gate_passed".to_string(), - value: true, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("rehosted direct-field book-value condition should gate execution"); - - assert_eq!( - state - .world_flags - .get("world.rehosted_book_value_gate_passed"), - Some(&true) - ); - } - - #[test] - fn derived_credit_rating_condition_reads_rehosted_finance_owner_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits - [(0x12 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 20.0f64.to_bits(); - year_stat_family_qword_bits - [(0x01 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = - 100.0f64.to_bits(); - year_stat_family_qword_bits - [(0x09 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = 0.0f64.to_bits(); - - let mut state = RuntimeState { - companies: vec![RuntimeCompany { - company_id: 1, - controller_kind: RuntimeCompanyControllerKind::Human, - current_cash: 100, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 37, - management_attitude: 58, - takeover_cooldown_year: Some(1844), - merger_cooldown_year: Some(1845), - }], - selected_company_id: Some(1), - world_restore: RuntimeWorldRestoreState { - issue_37_value: Some(5.0f32.to_bits()), - issue_38_value: Some(2), - packed_year_word_raw_u16: Some(1835), - ..RuntimeWorldRestoreState::default() - }, - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1830, - last_bankruptcy_year: 1800, - year_stat_family_qword_bits, - live_bond_slots: vec![crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 100_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.05f32.to_bits(), - }], - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - event_runtime_records: vec![RuntimeEventRecord { - record_id: 196, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::CompanyNumericThreshold { - target: RuntimeCompanyTarget::SelectedCompany, - metric: crate::RuntimeCompanyMetric::CreditRating, - comparator: RuntimeConditionComparator::Eq, - value: 10, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world.derived_credit_rating_gate_passed".to_string(), - value: true, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("derived credit-rating company condition should gate execution"); - - assert_eq!( - state - .world_flags - .get("world.derived_credit_rating_gate_passed"), - Some(&true) - ); - } - - #[test] - fn chairman_metric_conditions_support_all_active_target() { - let mut state = RuntimeState { - chairman_profiles: vec![ - RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 20, - linked_company_id: None, - company_holdings: BTreeMap::new(), - holdings_value_total: 500, - net_worth_total: 700, - purchasing_power_total: 900, - }, - RuntimeChairmanProfile { - profile_id: 2, - name: "Chairman Two".to_string(), - active: true, - current_cash: 30, - linked_company_id: None, - company_holdings: BTreeMap::new(), - holdings_value_total: 600, - net_worth_total: 800, - purchasing_power_total: 1000, - }, - ], - event_runtime_records: vec![RuntimeEventRecord { - record_id: 96, - trigger_kind: 6, - active: true, - service_count: 0, - marks_collection_dirty: false, - one_shot: false, - has_fired: false, - conditions: vec![RuntimeCondition::ChairmanNumericThreshold { - target: RuntimeChairmanTarget::AllActive, - metric: RuntimeChairmanMetric::PurchasingPowerTotal, - comparator: RuntimeConditionComparator::Ge, - value: 900, - }], - effects: vec![RuntimeEffect::SetWorldFlag { - key: "world.chairman_gate_passed".to_string(), - value: true, - }], - }], - ..state() - }; - - execute_step_command( - &mut state, - &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, - ) - .expect("all-active chairman condition should gate execution"); - - assert_eq!( - state.world_flags.get("world.chairman_gate_passed"), - Some(&true) - ); - } -} diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs deleted file mode 100644 index 05e5ed1..0000000 --- a/crates/rrt-runtime/src/summary.rs +++ /dev/null @@ -1,4167 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{ - CalendarPoint, RuntimeState, runtime_annual_bond_principal_flow_relation_label, - runtime_company_annual_bond_policy_state, runtime_company_annual_creditor_pressure_state, - runtime_company_annual_deep_distress_state, runtime_company_annual_dividend_policy_state, - runtime_company_annual_finance_policy_action_label, - runtime_company_annual_finance_policy_state, runtime_company_annual_finance_state, - runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state, - runtime_company_periodic_service_state, runtime_company_unassigned_share_pool, -}; - -fn raw_u32_to_f32_text(raw: u32) -> String { - format!("{:.6}", f32::from_bits(raw)) -} - -fn raw_u64_to_f64_text(raw: u64) -> String { - format!("{:.6}", f64::from_bits(raw)) -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RuntimeSummary { - pub calendar: CalendarPoint, - pub calendar_projection_source: Option, - pub calendar_projection_is_placeholder: bool, - pub world_flag_count: usize, - pub world_restore_selected_year_profile_lane: Option, - pub world_restore_campaign_scenario_enabled: Option, - pub world_restore_sandbox_enabled: Option, - pub world_restore_seed_tuple_written_from_raw_lane: Option, - pub world_restore_absolute_counter_requires_shell_context: Option, - pub world_restore_absolute_counter_reconstructible_from_save: Option, - pub world_restore_packed_year_word_raw_u16: Option, - pub world_restore_partial_year_progress_raw_u8: Option, - pub world_restore_current_calendar_tuple_word_raw_u32: Option, - pub world_restore_current_calendar_tuple_word_2_raw_u32: Option, - pub world_restore_absolute_counter_raw_u32: Option, - pub world_restore_absolute_counter_mirror_raw_u32: Option, - pub world_restore_disable_cargo_economy_special_condition_slot: Option, - pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: - Option, - pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option, - pub world_restore_disable_cargo_economy_special_condition_enabled: Option, - pub world_restore_use_bio_accelerator_cars_enabled: Option, - pub world_restore_use_wartime_cargos_enabled: Option, - pub world_restore_disable_train_crashes_enabled: Option, - pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option, - pub world_restore_ai_ignore_territories_at_startup_enabled: Option, - pub world_restore_limited_track_building_amount: Option, - pub world_restore_economic_status_code: Option, - pub world_restore_territory_access_cost: Option, - pub world_restore_linked_site_removal_follow_on_gate_raw_u8: Option, - pub world_restore_linked_site_removal_follow_on_gate_enabled: Option, - pub world_restore_auto_show_grade_during_track_lay_raw_u8: Option, - pub world_restore_starting_building_density_level_raw_u8: Option, - pub world_restore_post_text_building_density_growth_raw_u8: Option, - pub world_restore_leftover_simulation_time_accumulator_raw_u32: Option, - pub world_restore_leftover_simulation_time_accumulator_value_f32_text: Option, - pub world_restore_selected_year_lane_snapshot_raw_u8: Option, - pub world_restore_all_steam_locomotives_available_raw_u8: Option, - pub world_restore_all_steam_locomotives_available_enabled: Option, - pub world_restore_all_diesel_locomotives_available_raw_u8: Option, - pub world_restore_all_diesel_locomotives_available_enabled: Option, - pub world_restore_all_electric_locomotives_available_raw_u8: Option, - pub world_restore_all_electric_locomotives_available_enabled: Option, - pub world_restore_issue_37_value: Option, - pub world_restore_issue_38_value: Option, - pub world_restore_issue_39_value: Option, - pub world_restore_issue_3a_value: Option, - pub world_restore_issue_37_multiplier_raw_u32: Option, - pub world_restore_issue_37_multiplier_value_f32_text: Option, - pub world_restore_stock_issue_and_buyback_policy_raw_u8: Option, - pub world_restore_bond_issue_and_repayment_policy_raw_u8: Option, - pub world_restore_bankruptcy_policy_raw_u8: Option, - pub world_restore_dividend_policy_raw_u8: Option, - pub world_restore_building_density_growth_setting_raw_u32: Option, - pub world_restore_stock_issue_and_buyback_allowed: Option, - pub world_restore_bond_issue_and_repayment_allowed: Option, - pub world_restore_bankruptcy_allowed: Option, - pub world_restore_dividend_adjustment_allowed: Option, - pub world_restore_finance_neighborhood_count: usize, - pub world_restore_finance_neighborhood_labels: Vec, - pub world_restore_economic_tuning_mirror_raw_u32: Option, - pub world_restore_economic_tuning_mirror_value_f32_text: Option, - pub world_restore_economic_tuning_lane_count: usize, - pub world_restore_economic_tuning_lane_value_f32_text: Vec, - pub world_restore_cached_available_locomotive_rating_raw_u32: Option, - pub world_restore_cached_available_locomotive_rating_value_f32_text: Option, - pub world_restore_selected_year_bucket_scalar_raw_u32: Option, - pub world_restore_selected_year_bucket_scalar_value_f32_text: Option, - pub world_restore_selected_year_bucket_direct_lane_count: usize, - pub world_restore_selected_year_bucket_direct_lane_value_f32_text: Vec, - pub world_restore_selected_year_bucket_complement_lane_count: usize, - pub world_restore_selected_year_bucket_complement_lane_value_f32_text: Vec, - pub world_restore_selected_year_bucket_scaled_companion_lane_count: usize, - pub world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text: Vec, - pub world_restore_selected_year_gap_scalar_raw_u32: Option, - pub world_restore_selected_year_gap_scalar_value_f32_text: Option, - pub world_restore_absolute_counter_restore_kind: Option, - pub world_restore_absolute_counter_adjustment_context: Option, - pub metadata_count: usize, - pub company_count: usize, - pub active_company_count: usize, - pub company_market_state_owner_count: usize, - pub selected_company_outstanding_shares: Option, - pub selected_company_bond_count: Option, - pub selected_company_largest_live_bond_principal: Option, - pub selected_company_highest_coupon_live_bond_principal: Option, - pub selected_company_assigned_share_pool: Option, - pub selected_company_unassigned_share_pool: Option, - pub selected_company_cached_share_price: Option, - pub selected_company_cached_share_price_value_f32_text: Option, - pub selected_company_recent_per_share_cache_absolute_counter: Option, - pub selected_company_recent_per_share_cached_value_f64_text: Option, - pub selected_company_recent_per_share_subscore_value_f32_text: Option, - pub selected_company_mutable_support_scalar_value_f32_text: Option, - pub selected_company_stat_band_root_0cfb_count: usize, - pub selected_company_stat_band_root_0d7f_count: usize, - pub selected_company_stat_band_root_1c47_count: usize, - pub selected_company_last_dividend_year: Option, - pub selected_company_years_since_founding: Option, - pub selected_company_years_since_last_bankruptcy: Option, - pub selected_company_years_since_last_dividend: Option, - pub selected_company_current_partial_year_weight_numerator: Option, - pub selected_company_current_issue_absolute_counter: Option, - pub selected_company_prior_issue_absolute_counter: Option, - pub selected_company_current_issue_age_absolute_counter_delta: Option, - pub selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8: Option, - pub selected_company_periodic_side_latch_city_connection_latch: Option, - pub selected_company_periodic_side_latch_linked_transit_latch: Option, - #[serde(default)] - pub selected_company_linked_transit_route_anchor_entry_id: Option, - #[serde(default)] - pub selected_company_linked_transit_route_anchor_fallback_counts: Vec, - pub selected_company_periodic_service_base_route_preference_raw_u8: Option, - pub selected_company_periodic_service_effective_route_preference_raw_u8: Option, - pub selected_company_periodic_service_electric_route_preference_override_active: Option, - pub selected_company_periodic_service_route_quality_multiplier_basis_points: Option, - pub active_periodic_route_preference_override_company_id: Option, - pub active_periodic_route_preference_override_effective_raw_u8: Option, - pub last_periodic_route_preference_override_company_id: Option, - pub last_periodic_route_preference_override_effective_raw_u8: Option, - pub selected_company_chairman_bonus_year: Option, - pub selected_company_chairman_bonus_amount: Option, - pub selected_company_creditor_pressure_recent_bad_net_profit_year_count: Option, - pub selected_company_creditor_pressure_recent_peak_revenue: Option, - pub selected_company_creditor_pressure_recent_three_year_net_profit_total: Option, - pub selected_company_creditor_pressure_cash_floor: Option, - pub selected_company_creditor_pressure_cash_plus_slot_12_total: Option, - pub selected_company_creditor_pressure_share_price_floor: Option, - pub selected_company_creditor_pressure_share_price_scalar: Option, - pub selected_company_creditor_pressure_current_fuel_cost: Option, - pub selected_company_creditor_pressure_current_fuel_cost_floor: Option, - pub selected_company_creditor_pressure_eligible_for_bankruptcy_branch: Option, - pub selected_company_deep_distress_current_cash: Option, - pub selected_company_deep_distress_recent_first_three_net_profit_years: Vec, - pub selected_company_deep_distress_cash_floor: Option, - pub selected_company_deep_distress_net_profit_floor: Option, - pub selected_company_deep_distress_eligible_for_bankruptcy_fallback: Option, - pub selected_company_annual_bond_linked_transit_latch: Option, - pub selected_company_annual_bond_live_bond_count: Option, - pub selected_company_annual_bond_live_bond_principal_total: Option, - pub selected_company_annual_bond_matured_live_bond_count: Option, - pub selected_company_annual_bond_matured_live_bond_principal_total: Option, - pub selected_company_annual_bond_next_live_bond_maturity_year: Option, - pub selected_company_annual_bond_live_bond_coupon_burden_total: Option, - pub selected_company_annual_bond_current_cash: Option, - pub selected_company_annual_bond_cash_after_full_repayment: Option, - pub selected_company_annual_bond_issue_cash_floor: Option, - pub selected_company_annual_bond_issue_principal_step: Option, - pub selected_company_annual_bond_proposed_issue_bond_count: Option, - pub selected_company_annual_bond_proposed_issue_total_principal: Option, - pub selected_company_annual_bond_proposed_issue_years_to_maturity: Option, - pub selected_company_annual_bond_eligible_for_issue_branch: Option, - pub selected_company_stock_repurchase_city_connection_latch: Option, - pub selected_company_stock_repurchase_building_density_growth_setting: Option, - pub selected_company_stock_repurchase_linked_chairman_profile_id: Option, - pub selected_company_stock_repurchase_linked_chairman_personality_raw_u8: Option, - pub selected_company_stock_repurchase_batch_size: Option, - pub selected_company_stock_repurchase_factor_basis_points: Option, - pub selected_company_stock_repurchase_current_cash: Option, - pub selected_company_stock_repurchase_stock_value_gate_cash_floor: Option, - pub selected_company_stock_repurchase_support_adjusted_share_price_scalar: Option, - pub selected_company_stock_repurchase_affordability_cash_floor: Option, - pub selected_company_stock_repurchase_unassigned_share_pool: Option, - pub selected_company_stock_repurchase_eligible_for_single_batch: Option, - pub selected_company_stock_issue_live_bond_count: Option, - pub selected_company_stock_issue_initial_batch_size: Option, - pub selected_company_stock_issue_trimmed_batch_size: Option, - pub selected_company_stock_issue_share_pressure_basis_points: Option, - pub selected_company_stock_issue_pressured_share_price_scalar: Option, - pub selected_company_stock_issue_pressured_proceeds: Option, - pub selected_company_stock_issue_book_value_per_share_floor_applied: Option, - pub selected_company_stock_issue_price_to_book_ratio_basis_points: Option, - pub selected_company_stock_issue_current_cash: Option, - pub selected_company_stock_issue_highest_coupon_live_bond_principal: Option, - pub selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points: Option, - pub selected_company_stock_issue_current_issue_age_absolute_counter_delta: Option, - pub selected_company_stock_issue_current_issue_cooldown_floor: Option, - pub selected_company_stock_issue_minimum_price_to_book_ratio_basis_points: Option, - pub selected_company_stock_issue_passes_share_price_floor: Option, - pub selected_company_stock_issue_passes_proceeds_floor: Option, - pub selected_company_stock_issue_passes_cash_gate: Option, - pub selected_company_stock_issue_passes_issue_cooldown_gate: Option, - pub selected_company_stock_issue_passes_coupon_price_to_book_gate: Option, - pub selected_company_stock_issue_eligible_for_double_tranche: Option, - pub selected_company_dividend_weighted_recent_net_profit_total: Option, - pub selected_company_dividend_weighted_recent_net_profit_average: Option, - pub selected_company_dividend_current_cash: Option, - pub selected_company_dividend_tiny_unassigned_share_cash_supplement_branch: Option, - pub selected_company_dividend_tentative_target_per_share_tenths: Option, - pub selected_company_dividend_current_per_share_tenths: Option, - pub selected_company_dividend_growth_adjusted_current_per_share_tenths: Option, - pub selected_company_dividend_board_approved_ceiling_tenths: Option, - pub selected_company_dividend_proposed_per_share_tenths: Option, - pub selected_company_dividend_eligible_for_adjustment_branch: Option, - pub selected_company_annual_finance_policy_action: Option, - pub selected_company_annual_finance_news_family_candidate: Option, - pub selected_company_annual_finance_last_news_selector: Option, - pub annual_finance_last_news_event_count: usize, - pub selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible: Option, - pub selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible: - Option, - pub selected_company_annual_finance_policy_bond_issue_eligible: Option, - pub selected_company_annual_finance_policy_stock_repurchase_eligible: Option, - pub selected_company_annual_finance_policy_stock_issue_eligible: Option, - pub selected_company_annual_finance_policy_dividend_adjustment_eligible: Option, - pub player_count: usize, - pub chairman_profile_count: usize, - pub active_chairman_profile_count: usize, - pub selected_chairman_profile_id: Option, - pub linked_chairman_company_count: usize, - pub company_takeover_cooldown_count: usize, - pub company_merger_cooldown_count: usize, - pub train_count: usize, - pub active_train_count: usize, - pub retired_train_count: usize, - pub locomotive_catalog_count: usize, - pub cargo_catalog_count: usize, - pub territory_count: usize, - pub company_territory_track_count: usize, - pub packed_event_collection_present: bool, - pub packed_event_record_count: usize, - pub packed_event_decoded_record_count: usize, - pub packed_event_imported_runtime_record_count: usize, - pub packed_event_parity_only_record_count: usize, - pub packed_event_unsupported_record_count: usize, - pub packed_event_blocked_missing_company_context_count: usize, - pub packed_event_blocked_missing_selection_context_count: usize, - pub packed_event_blocked_missing_company_role_context_count: usize, - pub packed_event_blocked_missing_player_context_count: usize, - pub packed_event_blocked_missing_player_selection_context_count: usize, - pub packed_event_blocked_missing_player_role_context_count: usize, - pub packed_event_blocked_missing_chairman_context_count: usize, - pub packed_event_blocked_chairman_target_scope_count: usize, - pub packed_event_blocked_missing_condition_context_count: usize, - pub packed_event_blocked_missing_player_condition_context_count: usize, - pub packed_event_blocked_company_condition_scope_disabled_count: usize, - pub packed_event_blocked_player_condition_scope_count: usize, - pub packed_event_blocked_territory_condition_scope_count: usize, - pub packed_event_blocked_missing_territory_context_count: usize, - pub packed_event_blocked_named_territory_binding_count: usize, - pub packed_event_blocked_unmapped_ordinary_condition_count: usize, - pub packed_event_blocked_unmapped_world_condition_count: usize, - pub packed_event_blocked_missing_compact_control_count: usize, - pub packed_event_blocked_shell_owned_descriptor_count: usize, - pub packed_event_blocked_evidence_blocked_descriptor_count: usize, - pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: usize, - pub packed_event_blocked_unmapped_real_descriptor_count: usize, - pub packed_event_blocked_unmapped_world_descriptor_count: usize, - pub packed_event_blocked_territory_access_variant_count: usize, - pub packed_event_blocked_territory_access_scope_count: usize, - pub packed_event_blocked_missing_train_context_count: usize, - pub packed_event_blocked_missing_train_territory_context_count: usize, - pub packed_event_blocked_missing_locomotive_catalog_context_count: usize, - pub packed_event_blocked_confiscation_variant_count: usize, - pub packed_event_blocked_retire_train_variant_count: usize, - pub packed_event_blocked_retire_train_scope_count: usize, - pub packed_event_blocked_structural_only_count: usize, - pub event_runtime_record_count: usize, - pub candidate_availability_count: usize, - pub zero_candidate_availability_count: usize, - pub named_locomotive_availability_count: usize, - pub zero_named_locomotive_availability_count: usize, - pub named_locomotive_cost_count: usize, - pub cargo_production_override_count: usize, - pub world_runtime_variable_count: usize, - pub company_runtime_variable_owner_count: usize, - pub player_runtime_variable_owner_count: usize, - pub territory_runtime_variable_owner_count: usize, - pub world_scalar_override_count: usize, - pub special_condition_count: usize, - pub enabled_special_condition_count: usize, - pub save_profile_kind: Option, - pub save_profile_family: Option, - pub save_profile_map_path: Option, - pub save_profile_display_name: Option, - pub save_profile_selected_year_profile_lane: Option, - pub save_profile_sandbox_enabled: Option, - pub save_profile_campaign_scenario_enabled: Option, - pub save_profile_staged_profile_copy_on_restore: Option, - pub total_event_record_service_count: u64, - pub periodic_boundary_call_count: u64, - pub annual_finance_service_call_count: u64, - pub periodic_route_preference_override_apply_count: u64, - pub periodic_route_preference_override_restore_count: u64, - pub annual_dividend_adjustment_commit_count: u64, - pub annual_bond_last_retired_principal_total: u64, - pub annual_bond_last_issued_principal_total: u64, - pub annual_bond_last_principal_flow_relation: Option, - pub annual_stock_repurchase_last_share_count: u64, - pub annual_stock_issue_last_share_count: u64, - pub total_trigger_dispatch_count: u64, - pub dirty_rerun_count: u64, - pub total_company_cash: i64, -} - -impl RuntimeSummary { - pub fn from_state(state: &RuntimeState) -> Self { - let selected_company_market_state = state - .selected_company_id - .and_then(|company_id| state.service_state.company_market_state.get(&company_id)); - let selected_company_periodic_side_latch_state = - state.selected_company_id.and_then(|company_id| { - state - .service_state - .company_periodic_side_latch_state - .get(&company_id) - }); - let selected_company_periodic_service_state = state - .selected_company_id - .and_then(|company_id| runtime_company_periodic_service_state(state, company_id)); - let selected_company_annual_finance_state = state - .selected_company_id - .and_then(|company_id| runtime_company_annual_finance_state(state, company_id)); - let selected_company_creditor_pressure_state = - state.selected_company_id.and_then(|company_id| { - runtime_company_annual_creditor_pressure_state(state, company_id) - }); - let selected_company_deep_distress_state = state - .selected_company_id - .and_then(|company_id| runtime_company_annual_deep_distress_state(state, company_id)); - let selected_company_annual_bond_state = state - .selected_company_id - .and_then(|company_id| runtime_company_annual_bond_policy_state(state, company_id)); - let selected_company_stock_repurchase_state = - state.selected_company_id.and_then(|company_id| { - runtime_company_annual_stock_repurchase_state(state, company_id) - }); - let selected_company_stock_issue_state = state - .selected_company_id - .and_then(|company_id| runtime_company_annual_stock_issue_state(state, company_id)); - let selected_company_dividend_state = state - .selected_company_id - .and_then(|company_id| runtime_company_annual_dividend_policy_state(state, company_id)); - let selected_company_annual_finance_policy_state = state - .selected_company_id - .and_then(|company_id| runtime_company_annual_finance_policy_state(state, company_id)); - Self { - calendar: state.calendar, - calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(), - calendar_projection_is_placeholder: state - .metadata - .get("save_slice.calendar_source") - .is_some_and(|value| value == "default-1830-placeholder"), - world_flag_count: state.world_flags.len(), - world_restore_selected_year_profile_lane: state - .world_restore - .selected_year_profile_lane, - world_restore_campaign_scenario_enabled: state.world_restore.campaign_scenario_enabled, - world_restore_sandbox_enabled: state.world_restore.sandbox_enabled, - world_restore_seed_tuple_written_from_raw_lane: state - .world_restore - .seed_tuple_written_from_raw_lane, - world_restore_absolute_counter_requires_shell_context: state - .world_restore - .absolute_counter_requires_shell_context, - world_restore_absolute_counter_reconstructible_from_save: state - .world_restore - .absolute_counter_reconstructible_from_save, - world_restore_packed_year_word_raw_u16: state.world_restore.packed_year_word_raw_u16, - world_restore_partial_year_progress_raw_u8: state - .world_restore - .partial_year_progress_raw_u8, - world_restore_current_calendar_tuple_word_raw_u32: state - .world_restore - .current_calendar_tuple_word_raw_u32, - world_restore_current_calendar_tuple_word_2_raw_u32: state - .world_restore - .current_calendar_tuple_word_2_raw_u32, - world_restore_absolute_counter_raw_u32: state.world_restore.absolute_counter_raw_u32, - world_restore_absolute_counter_mirror_raw_u32: state - .world_restore - .absolute_counter_mirror_raw_u32, - world_restore_disable_cargo_economy_special_condition_slot: state - .world_restore - .disable_cargo_economy_special_condition_slot, - world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: state - .world_restore - .disable_cargo_economy_special_condition_reconstructible_from_save, - world_restore_disable_cargo_economy_special_condition_write_side_grounded: state - .world_restore - .disable_cargo_economy_special_condition_write_side_grounded, - world_restore_disable_cargo_economy_special_condition_enabled: state - .world_restore - .disable_cargo_economy_special_condition_enabled, - world_restore_use_bio_accelerator_cars_enabled: state - .world_restore - .use_bio_accelerator_cars_enabled, - world_restore_use_wartime_cargos_enabled: state - .world_restore - .use_wartime_cargos_enabled, - world_restore_disable_train_crashes_enabled: state - .world_restore - .disable_train_crashes_enabled, - world_restore_disable_train_crashes_and_breakdowns_enabled: state - .world_restore - .disable_train_crashes_and_breakdowns_enabled, - world_restore_ai_ignore_territories_at_startup_enabled: state - .world_restore - .ai_ignore_territories_at_startup_enabled, - world_restore_limited_track_building_amount: state - .world_restore - .limited_track_building_amount, - world_restore_economic_status_code: state.world_restore.economic_status_code, - world_restore_territory_access_cost: state.world_restore.territory_access_cost, - world_restore_issue_37_value: state.world_restore.issue_37_value, - world_restore_issue_38_value: state.world_restore.issue_38_value, - world_restore_issue_39_value: state.world_restore.issue_39_value, - world_restore_issue_3a_value: state.world_restore.issue_3a_value, - world_restore_issue_37_multiplier_raw_u32: state - .world_restore - .issue_37_multiplier_raw_u32, - world_restore_issue_37_multiplier_value_f32_text: state - .world_restore - .issue_37_multiplier_value_f32_text - .clone(), - world_restore_stock_issue_and_buyback_policy_raw_u8: state - .world_restore - .stock_issue_and_buyback_policy_raw_u8, - world_restore_bond_issue_and_repayment_policy_raw_u8: state - .world_restore - .bond_issue_and_repayment_policy_raw_u8, - world_restore_bankruptcy_policy_raw_u8: state.world_restore.bankruptcy_policy_raw_u8, - world_restore_dividend_policy_raw_u8: state.world_restore.dividend_policy_raw_u8, - world_restore_building_density_growth_setting_raw_u32: state - .world_restore - .building_density_growth_setting_raw_u32, - world_restore_stock_issue_and_buyback_allowed: state - .world_restore - .stock_issue_and_buyback_allowed, - world_restore_bond_issue_and_repayment_allowed: state - .world_restore - .bond_issue_and_repayment_allowed, - world_restore_bankruptcy_allowed: state.world_restore.bankruptcy_allowed, - world_restore_dividend_adjustment_allowed: state - .world_restore - .dividend_adjustment_allowed, - world_restore_finance_neighborhood_count: state - .world_restore - .finance_neighborhood_candidates - .len(), - world_restore_finance_neighborhood_labels: state - .world_restore - .finance_neighborhood_candidates - .iter() - .map(|candidate| candidate.label.clone()) - .collect(), - world_restore_economic_tuning_mirror_raw_u32: state - .world_restore - .economic_tuning_mirror_raw_u32, - world_restore_economic_tuning_mirror_value_f32_text: state - .world_restore - .economic_tuning_mirror_value_f32_text - .clone(), - world_restore_economic_tuning_lane_count: state - .world_restore - .economic_tuning_lane_raw_u32 - .len(), - world_restore_economic_tuning_lane_value_f32_text: state - .world_restore - .economic_tuning_lane_value_f32_text - .clone(), - world_restore_linked_site_removal_follow_on_gate_raw_u8: state - .world_restore - .linked_site_removal_follow_on_gate_raw_u8, - world_restore_linked_site_removal_follow_on_gate_enabled: state - .world_restore - .linked_site_removal_follow_on_gate_enabled, - world_restore_auto_show_grade_during_track_lay_raw_u8: state - .world_restore - .auto_show_grade_during_track_lay_raw_u8, - world_restore_starting_building_density_level_raw_u8: state - .world_restore - .starting_building_density_level_raw_u8, - world_restore_post_text_building_density_growth_raw_u8: state - .world_restore - .post_text_building_density_growth_raw_u8, - world_restore_leftover_simulation_time_accumulator_raw_u32: state - .world_restore - .leftover_simulation_time_accumulator_raw_u32, - world_restore_leftover_simulation_time_accumulator_value_f32_text: state - .world_restore - .leftover_simulation_time_accumulator_value_f32_text - .clone(), - world_restore_selected_year_lane_snapshot_raw_u8: state - .world_restore - .selected_year_lane_snapshot_raw_u8, - world_restore_all_steam_locomotives_available_raw_u8: state - .world_restore - .all_steam_locomotives_available_raw_u8, - world_restore_all_steam_locomotives_available_enabled: state - .world_restore - .all_steam_locomotives_available_enabled, - world_restore_all_diesel_locomotives_available_raw_u8: state - .world_restore - .all_diesel_locomotives_available_raw_u8, - world_restore_all_diesel_locomotives_available_enabled: state - .world_restore - .all_diesel_locomotives_available_enabled, - world_restore_all_electric_locomotives_available_raw_u8: state - .world_restore - .all_electric_locomotives_available_raw_u8, - world_restore_all_electric_locomotives_available_enabled: state - .world_restore - .all_electric_locomotives_available_enabled, - world_restore_cached_available_locomotive_rating_raw_u32: state - .world_restore - .cached_available_locomotive_rating_raw_u32, - world_restore_cached_available_locomotive_rating_value_f32_text: state - .world_restore - .cached_available_locomotive_rating_value_f32_text - .clone(), - world_restore_selected_year_bucket_scalar_raw_u32: state - .world_restore - .selected_year_bucket_scalar_raw_u32, - world_restore_selected_year_bucket_scalar_value_f32_text: state - .world_restore - .selected_year_bucket_scalar_value_f32_text - .clone(), - world_restore_selected_year_bucket_direct_lane_count: state - .world_restore - .selected_year_bucket_direct_lane_raw_u32 - .len(), - world_restore_selected_year_bucket_direct_lane_value_f32_text: state - .world_restore - .selected_year_bucket_direct_lane_value_f32_text - .clone(), - world_restore_selected_year_bucket_complement_lane_count: state - .world_restore - .selected_year_bucket_complement_lane_raw_u32 - .len(), - world_restore_selected_year_bucket_complement_lane_value_f32_text: state - .world_restore - .selected_year_bucket_complement_lane_value_f32_text - .clone(), - world_restore_selected_year_bucket_scaled_companion_lane_count: state - .world_restore - .selected_year_bucket_scaled_companion_lane_raw_u32 - .len(), - world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text: state - .world_restore - .selected_year_bucket_scaled_companion_lane_value_f32_text - .clone(), - world_restore_selected_year_gap_scalar_raw_u32: state - .world_restore - .selected_year_gap_scalar_raw_u32, - world_restore_selected_year_gap_scalar_value_f32_text: state - .world_restore - .selected_year_gap_scalar_value_f32_text - .clone(), - world_restore_absolute_counter_restore_kind: state - .world_restore - .absolute_counter_restore_kind - .clone(), - world_restore_absolute_counter_adjustment_context: state - .world_restore - .absolute_counter_adjustment_context - .clone(), - metadata_count: state.metadata.len(), - company_count: state.companies.len(), - active_company_count: state - .companies - .iter() - .filter(|company| company.active) - .count(), - company_market_state_owner_count: state.service_state.company_market_state.len(), - selected_company_outstanding_shares: selected_company_market_state - .map(|market_state| market_state.outstanding_shares), - selected_company_bond_count: selected_company_market_state - .map(|market_state| market_state.bond_count), - selected_company_largest_live_bond_principal: selected_company_market_state - .and_then(|market_state| market_state.largest_live_bond_principal), - selected_company_highest_coupon_live_bond_principal: selected_company_market_state - .and_then(|market_state| market_state.highest_coupon_live_bond_principal), - selected_company_assigned_share_pool: selected_company_annual_finance_state - .as_ref() - .map(|finance_state| finance_state.assigned_share_pool), - selected_company_unassigned_share_pool: state - .selected_company_id - .and_then(|company_id| runtime_company_unassigned_share_pool(state, company_id)), - selected_company_cached_share_price: selected_company_annual_finance_state - .as_ref() - .and_then(|finance_state| finance_state.cached_share_price), - selected_company_cached_share_price_value_f32_text: selected_company_market_state - .map(|market_state| raw_u32_to_f32_text(market_state.cached_share_price_raw_u32)), - selected_company_recent_per_share_cache_absolute_counter: selected_company_market_state - .map(|market_state| market_state.recent_per_share_cache_absolute_counter), - selected_company_recent_per_share_cached_value_f64_text: selected_company_market_state - .map(|market_state| { - raw_u64_to_f64_text(market_state.recent_per_share_cached_value_bits) - }), - selected_company_recent_per_share_subscore_value_f32_text: - selected_company_market_state.map(|market_state| { - raw_u32_to_f32_text(market_state.recent_per_share_subscore_raw_u32) - }), - selected_company_mutable_support_scalar_value_f32_text: selected_company_market_state - .map(|market_state| { - raw_u32_to_f32_text(market_state.mutable_support_scalar_raw_u32) - }), - selected_company_stat_band_root_0cfb_count: selected_company_market_state - .map(|market_state| market_state.stat_band_root_0cfb_candidates.len()) - .unwrap_or(0), - selected_company_stat_band_root_0d7f_count: selected_company_market_state - .map(|market_state| market_state.stat_band_root_0d7f_candidates.len()) - .unwrap_or(0), - selected_company_stat_band_root_1c47_count: selected_company_market_state - .map(|market_state| market_state.stat_band_root_1c47_candidates.len()) - .unwrap_or(0), - selected_company_last_dividend_year: selected_company_market_state - .map(|market_state| market_state.last_dividend_year) - .filter(|year| *year != 0), - selected_company_years_since_founding: selected_company_annual_finance_state - .as_ref() - .and_then(|finance_state| finance_state.years_since_founding), - selected_company_years_since_last_bankruptcy: selected_company_annual_finance_state - .as_ref() - .and_then(|finance_state| finance_state.years_since_last_bankruptcy), - selected_company_years_since_last_dividend: selected_company_annual_finance_state - .as_ref() - .and_then(|finance_state| finance_state.years_since_last_dividend), - selected_company_current_partial_year_weight_numerator: - selected_company_annual_finance_state - .as_ref() - .and_then(|finance_state| finance_state.current_partial_year_weight_numerator), - selected_company_current_issue_absolute_counter: selected_company_annual_finance_state - .as_ref() - .and_then(|finance_state| finance_state.current_issue_absolute_counter), - selected_company_prior_issue_absolute_counter: selected_company_annual_finance_state - .as_ref() - .and_then(|finance_state| finance_state.prior_issue_absolute_counter), - selected_company_current_issue_age_absolute_counter_delta: - selected_company_annual_finance_state - .as_ref() - .and_then(|finance_state| { - finance_state.current_issue_age_absolute_counter_delta - }), - selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8: - selected_company_periodic_side_latch_state - .and_then(|latch_state| latch_state.preferred_locomotive_engine_type_raw_u8), - selected_company_periodic_side_latch_city_connection_latch: - selected_company_periodic_side_latch_state - .map(|latch_state| latch_state.city_connection_latch), - selected_company_periodic_side_latch_linked_transit_latch: - selected_company_periodic_side_latch_state - .map(|latch_state| latch_state.linked_transit_latch), - selected_company_linked_transit_route_anchor_entry_id: selected_company_market_state - .and_then(|market_state| market_state.linked_transit_route_anchor_entry_id), - selected_company_linked_transit_route_anchor_fallback_counts: - selected_company_market_state - .map(|market_state| { - market_state - .linked_transit_route_anchor_fallback_counts - .clone() - }) - .unwrap_or_default(), - selected_company_periodic_service_base_route_preference_raw_u8: - selected_company_periodic_service_state - .as_ref() - .and_then(|service_state| service_state.base_route_preference_raw_u8), - selected_company_periodic_service_effective_route_preference_raw_u8: - selected_company_periodic_service_state - .as_ref() - .and_then(|service_state| service_state.effective_route_preference_raw_u8), - selected_company_periodic_service_electric_route_preference_override_active: - selected_company_periodic_service_state - .as_ref() - .map(|service_state| service_state.electric_route_preference_override_active), - selected_company_periodic_service_route_quality_multiplier_basis_points: - selected_company_periodic_service_state - .as_ref() - .map(|service_state| { - service_state.effective_route_quality_multiplier_basis_points - }), - active_periodic_route_preference_override_company_id: state - .service_state - .active_periodic_route_preference_override - .as_ref() - .map(|override_state| override_state.company_id), - active_periodic_route_preference_override_effective_raw_u8: state - .service_state - .active_periodic_route_preference_override - .as_ref() - .and_then(|override_state| override_state.effective_route_preference_raw_u8), - last_periodic_route_preference_override_company_id: state - .service_state - .last_periodic_route_preference_override - .as_ref() - .map(|override_state| override_state.company_id), - last_periodic_route_preference_override_effective_raw_u8: state - .service_state - .last_periodic_route_preference_override - .as_ref() - .and_then(|override_state| override_state.effective_route_preference_raw_u8), - selected_company_chairman_bonus_year: selected_company_market_state - .map(|market_state| market_state.chairman_bonus_year) - .filter(|year| *year != 0), - selected_company_chairman_bonus_amount: selected_company_market_state - .map(|market_state| market_state.chairman_bonus_amount) - .filter(|amount| *amount != 0), - selected_company_creditor_pressure_recent_bad_net_profit_year_count: - selected_company_creditor_pressure_state - .as_ref() - .map(|pressure_state| pressure_state.recent_bad_net_profit_year_count), - selected_company_creditor_pressure_recent_peak_revenue: - selected_company_creditor_pressure_state - .as_ref() - .and_then(|pressure_state| pressure_state.recent_peak_revenue), - selected_company_creditor_pressure_recent_three_year_net_profit_total: - selected_company_creditor_pressure_state - .as_ref() - .and_then(|pressure_state| pressure_state.recent_three_year_net_profit_total), - selected_company_creditor_pressure_cash_floor: selected_company_creditor_pressure_state - .as_ref() - .and_then(|pressure_state| pressure_state.pressure_ladder_cash_floor), - selected_company_creditor_pressure_cash_plus_slot_12_total: - selected_company_creditor_pressure_state - .as_ref() - .and_then(|pressure_state| pressure_state.current_cash_plus_slot_12_total), - selected_company_creditor_pressure_share_price_floor: - selected_company_creditor_pressure_state - .as_ref() - .and_then(|pressure_state| pressure_state.support_adjusted_share_price_floor), - selected_company_creditor_pressure_share_price_scalar: - selected_company_creditor_pressure_state - .as_ref() - .and_then(|pressure_state| pressure_state.support_adjusted_share_price_scalar), - selected_company_creditor_pressure_current_fuel_cost: - selected_company_creditor_pressure_state - .as_ref() - .and_then(|pressure_state| pressure_state.current_fuel_cost), - selected_company_creditor_pressure_current_fuel_cost_floor: - selected_company_creditor_pressure_state - .as_ref() - .and_then(|pressure_state| pressure_state.current_fuel_cost_floor), - selected_company_creditor_pressure_eligible_for_bankruptcy_branch: - selected_company_creditor_pressure_state - .as_ref() - .map(|pressure_state| pressure_state.eligible_for_bankruptcy_branch), - selected_company_deep_distress_current_cash: selected_company_deep_distress_state - .as_ref() - .and_then(|pressure_state| pressure_state.current_cash), - selected_company_deep_distress_recent_first_three_net_profit_years: - selected_company_deep_distress_state - .as_ref() - .map(|pressure_state| { - pressure_state.recent_first_three_net_profit_years.clone() - }) - .unwrap_or_default(), - selected_company_deep_distress_cash_floor: selected_company_deep_distress_state - .as_ref() - .and_then(|pressure_state| pressure_state.deep_distress_cash_floor), - selected_company_deep_distress_net_profit_floor: selected_company_deep_distress_state - .as_ref() - .and_then(|pressure_state| pressure_state.deep_distress_net_profit_floor), - selected_company_deep_distress_eligible_for_bankruptcy_fallback: - selected_company_deep_distress_state - .as_ref() - .map(|pressure_state| pressure_state.eligible_for_bankruptcy_fallback), - selected_company_annual_bond_linked_transit_latch: selected_company_annual_bond_state - .as_ref() - .map(|bond_state| bond_state.linked_transit_latch), - selected_company_annual_bond_live_bond_count: selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.live_bond_count), - selected_company_annual_bond_live_bond_principal_total: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.live_bond_principal_total), - selected_company_annual_bond_matured_live_bond_count: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.matured_live_bond_count), - selected_company_annual_bond_matured_live_bond_principal_total: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.matured_live_bond_principal_total), - selected_company_annual_bond_next_live_bond_maturity_year: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.next_live_bond_maturity_year), - selected_company_annual_bond_live_bond_coupon_burden_total: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.live_bond_coupon_burden_total), - selected_company_annual_bond_current_cash: selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.current_cash), - selected_company_annual_bond_cash_after_full_repayment: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.cash_after_full_repayment), - selected_company_annual_bond_issue_cash_floor: selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.issue_cash_floor), - selected_company_annual_bond_issue_principal_step: selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.issue_principal_step), - selected_company_annual_bond_proposed_issue_bond_count: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.proposed_issue_bond_count), - selected_company_annual_bond_proposed_issue_total_principal: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.proposed_issue_total_principal), - selected_company_annual_bond_proposed_issue_years_to_maturity: - selected_company_annual_bond_state - .as_ref() - .and_then(|bond_state| bond_state.proposed_issue_years_to_maturity), - selected_company_annual_bond_eligible_for_issue_branch: - selected_company_annual_bond_state - .as_ref() - .map(|bond_state| bond_state.eligible_for_bond_issue_branch), - selected_company_stock_repurchase_city_connection_latch: - selected_company_stock_repurchase_state - .as_ref() - .map(|repurchase_state| repurchase_state.city_connection_latch), - selected_company_stock_repurchase_building_density_growth_setting: - selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| repurchase_state.building_density_growth_setting), - selected_company_stock_repurchase_linked_chairman_profile_id: - selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| repurchase_state.linked_chairman_profile_id), - selected_company_stock_repurchase_linked_chairman_personality_raw_u8: - selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| { - repurchase_state.linked_chairman_personality_raw_u8 - }), - selected_company_stock_repurchase_batch_size: selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| repurchase_state.repurchase_batch_size), - selected_company_stock_repurchase_factor_basis_points: - selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| repurchase_state.repurchase_factor_basis_points), - selected_company_stock_repurchase_current_cash: selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| repurchase_state.current_cash), - selected_company_stock_repurchase_stock_value_gate_cash_floor: - selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| repurchase_state.stock_value_gate_cash_floor), - selected_company_stock_repurchase_support_adjusted_share_price_scalar: - selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| { - repurchase_state.support_adjusted_share_price_scalar - }), - selected_company_stock_repurchase_affordability_cash_floor: - selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| repurchase_state.affordability_cash_floor), - selected_company_stock_repurchase_unassigned_share_pool: - selected_company_stock_repurchase_state - .as_ref() - .and_then(|repurchase_state| repurchase_state.unassigned_share_pool), - selected_company_stock_repurchase_eligible_for_single_batch: - selected_company_stock_repurchase_state - .as_ref() - .map(|repurchase_state| repurchase_state.eligible_for_single_batch_repurchase), - selected_company_stock_issue_live_bond_count: selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.live_bond_count), - selected_company_stock_issue_initial_batch_size: selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.initial_issue_batch_size), - selected_company_stock_issue_trimmed_batch_size: selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.trimmed_issue_batch_size), - selected_company_stock_issue_share_pressure_basis_points: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.share_pressure_basis_points), - selected_company_stock_issue_pressured_share_price_scalar: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| { - issue_state.pressured_support_adjusted_share_price_scalar - }), - selected_company_stock_issue_pressured_proceeds: selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.pressured_proceeds), - selected_company_stock_issue_book_value_per_share_floor_applied: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.book_value_per_share_floor_applied), - selected_company_stock_issue_price_to_book_ratio_basis_points: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.price_to_book_ratio_basis_points), - selected_company_stock_issue_current_cash: selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.current_cash), - selected_company_stock_issue_highest_coupon_live_bond_principal: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.highest_coupon_live_bond_principal), - selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.highest_coupon_live_bond_rate_basis_points), - selected_company_stock_issue_current_issue_age_absolute_counter_delta: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.current_issue_age_absolute_counter_delta), - selected_company_stock_issue_current_issue_cooldown_floor: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.current_issue_cooldown_floor), - selected_company_stock_issue_minimum_price_to_book_ratio_basis_points: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.minimum_price_to_book_ratio_basis_points), - selected_company_stock_issue_passes_share_price_floor: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.passes_share_price_floor), - selected_company_stock_issue_passes_proceeds_floor: selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.passes_proceeds_floor), - selected_company_stock_issue_passes_cash_gate: selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.passes_cash_gate), - selected_company_stock_issue_passes_issue_cooldown_gate: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.passes_issue_cooldown_gate), - selected_company_stock_issue_passes_coupon_price_to_book_gate: - selected_company_stock_issue_state - .as_ref() - .and_then(|issue_state| issue_state.passes_coupon_price_to_book_gate), - selected_company_stock_issue_eligible_for_double_tranche: - selected_company_stock_issue_state - .as_ref() - .map(|issue_state| issue_state.eligible_for_double_tranche_issue), - selected_company_dividend_weighted_recent_net_profit_total: - selected_company_dividend_state - .as_ref() - .and_then(|dividend_state| dividend_state.weighted_recent_net_profit_total), - selected_company_dividend_weighted_recent_net_profit_average: - selected_company_dividend_state - .as_ref() - .and_then(|dividend_state| dividend_state.weighted_recent_net_profit_average), - selected_company_dividend_current_cash: selected_company_dividend_state - .as_ref() - .and_then(|dividend_state| dividend_state.current_cash), - selected_company_dividend_tiny_unassigned_share_cash_supplement_branch: - selected_company_dividend_state - .as_ref() - .map(|dividend_state| { - dividend_state.tiny_unassigned_share_cash_supplement_branch - }), - selected_company_dividend_tentative_target_per_share_tenths: - selected_company_dividend_state - .as_ref() - .and_then(|dividend_state| { - dividend_state.tentative_target_dividend_per_share_tenths - }), - selected_company_dividend_current_per_share_tenths: selected_company_dividend_state - .as_ref() - .and_then(|dividend_state| dividend_state.current_dividend_per_share_tenths), - selected_company_dividend_growth_adjusted_current_per_share_tenths: - selected_company_dividend_state - .as_ref() - .and_then(|dividend_state| { - dividend_state.growth_adjusted_current_dividend_per_share_tenths - }), - selected_company_dividend_board_approved_ceiling_tenths: - selected_company_dividend_state - .as_ref() - .and_then(|dividend_state| { - dividend_state.board_approved_dividend_rate_ceiling_tenths - }), - selected_company_dividend_proposed_per_share_tenths: selected_company_dividend_state - .as_ref() - .and_then(|dividend_state| dividend_state.proposed_dividend_per_share_tenths), - selected_company_dividend_eligible_for_adjustment_branch: - selected_company_dividend_state - .as_ref() - .map(|dividend_state| dividend_state.eligible_for_dividend_adjustment_branch), - selected_company_annual_finance_policy_action: - selected_company_annual_finance_policy_state - .as_ref() - .map(|policy_state| { - runtime_company_annual_finance_policy_action_label(policy_state.action) - .to_string() - }), - selected_company_annual_finance_news_family_candidate: state - .selected_company_id - .and_then(|company_id| { - state - .service_state - .annual_finance_last_news_family_candidates - .get(&company_id) - }) - .cloned(), - selected_company_annual_finance_last_news_selector: state - .selected_company_id - .and_then(|company_id| { - state - .service_state - .annual_finance_last_news_events - .iter() - .rev() - .find(|news| news.company_id == company_id) - }) - .map(|news| news.selector_label.clone()), - annual_finance_last_news_event_count: state - .service_state - .annual_finance_last_news_events - .len(), - selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible: - selected_company_annual_finance_policy_state - .as_ref() - .map(|policy_state| policy_state.creditor_pressure_bankruptcy_eligible), - selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible: - selected_company_annual_finance_policy_state - .as_ref() - .map(|policy_state| policy_state.deep_distress_bankruptcy_fallback_eligible), - selected_company_annual_finance_policy_bond_issue_eligible: - selected_company_annual_finance_policy_state - .as_ref() - .map(|policy_state| policy_state.bond_issue_eligible), - selected_company_annual_finance_policy_stock_repurchase_eligible: - selected_company_annual_finance_policy_state - .as_ref() - .map(|policy_state| policy_state.stock_repurchase_eligible), - selected_company_annual_finance_policy_stock_issue_eligible: - selected_company_annual_finance_policy_state - .as_ref() - .map(|policy_state| policy_state.stock_issue_eligible), - selected_company_annual_finance_policy_dividend_adjustment_eligible: - selected_company_annual_finance_policy_state - .as_ref() - .map(|policy_state| policy_state.dividend_adjustment_eligible), - player_count: state.players.len(), - chairman_profile_count: state.chairman_profiles.len(), - active_chairman_profile_count: state - .chairman_profiles - .iter() - .filter(|profile| profile.active) - .count(), - selected_chairman_profile_id: state.selected_chairman_profile_id, - linked_chairman_company_count: state - .companies - .iter() - .filter(|company| company.linked_chairman_profile_id.is_some()) - .count(), - company_takeover_cooldown_count: state - .companies - .iter() - .filter(|company| company.takeover_cooldown_year.is_some()) - .count(), - company_merger_cooldown_count: state - .companies - .iter() - .filter(|company| company.merger_cooldown_year.is_some()) - .count(), - train_count: state.trains.len(), - active_train_count: state.trains.iter().filter(|train| train.active).count(), - retired_train_count: state.trains.iter().filter(|train| train.retired).count(), - locomotive_catalog_count: state.locomotive_catalog.len(), - cargo_catalog_count: state.cargo_catalog.len(), - territory_count: state.territories.len(), - company_territory_track_count: state.company_territory_track_piece_counts.len(), - packed_event_collection_present: state.packed_event_collection.is_some(), - packed_event_record_count: state - .packed_event_collection - .as_ref() - .map(|summary| summary.live_record_count) - .unwrap_or(0), - packed_event_decoded_record_count: state - .packed_event_collection - .as_ref() - .map(|summary| summary.decoded_record_count) - .unwrap_or(0), - packed_event_imported_runtime_record_count: state - .packed_event_collection - .as_ref() - .map(|summary| summary.imported_runtime_record_count) - .unwrap_or(0), - packed_event_parity_only_record_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| record.decode_status == "parity_only") - .count() - }) - .unwrap_or(0), - packed_event_unsupported_record_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| record.decode_status == "unsupported_framing") - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_company_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_company_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_selection_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_selection_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_company_role_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_company_role_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_player_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_player_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_player_selection_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_player_selection_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_player_role_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_player_role_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_chairman_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_chairman_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_chairman_target_scope_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_chairman_target_scope") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_condition_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_condition_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_player_condition_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_player_condition_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_company_condition_scope_disabled_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_company_condition_scope_disabled") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_player_condition_scope_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_player_condition_scope") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_territory_condition_scope_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_territory_condition_scope") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_territory_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_territory_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_named_territory_binding_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_named_territory_binding") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_unmapped_ordinary_condition_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_unmapped_ordinary_condition") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_unmapped_world_condition_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_unmapped_world_condition") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_compact_control_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_compact_control") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_shell_owned_descriptor_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_shell_owned_descriptor") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_evidence_blocked_descriptor_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_evidence_blocked_descriptor") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_variant_or_scope_blocked_descriptor_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_variant_or_scope_blocked_descriptor") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_unmapped_real_descriptor_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_unmapped_real_descriptor") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_unmapped_world_descriptor_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_unmapped_world_descriptor") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_territory_access_variant_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_territory_access_variant") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_territory_access_scope_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_territory_access_scope") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_train_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_train_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_train_territory_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_train_territory_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_missing_locomotive_catalog_context_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() - == Some("blocked_missing_locomotive_catalog_context") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_confiscation_variant_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() == Some("blocked_confiscation_variant") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_retire_train_variant_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() == Some("blocked_retire_train_variant") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_retire_train_scope_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() == Some("blocked_retire_train_scope") - }) - .count() - }) - .unwrap_or(0), - packed_event_blocked_structural_only_count: state - .packed_event_collection - .as_ref() - .map(|summary| { - summary - .records - .iter() - .filter(|record| { - record.import_outcome.as_deref() == Some("blocked_structural_only") - }) - .count() - }) - .unwrap_or(0), - event_runtime_record_count: state.event_runtime_records.len(), - candidate_availability_count: state.candidate_availability.len(), - zero_candidate_availability_count: state - .candidate_availability - .values() - .filter(|value| **value == 0) - .count(), - named_locomotive_availability_count: state.named_locomotive_availability.len(), - zero_named_locomotive_availability_count: state - .named_locomotive_availability - .values() - .filter(|value| **value == 0) - .count(), - named_locomotive_cost_count: state.named_locomotive_cost.len(), - cargo_production_override_count: state.cargo_production_overrides.len(), - world_runtime_variable_count: state.world_runtime_variables.len(), - company_runtime_variable_owner_count: state.company_runtime_variables.len(), - player_runtime_variable_owner_count: state.player_runtime_variables.len(), - territory_runtime_variable_owner_count: state.territory_runtime_variables.len(), - world_scalar_override_count: state.world_scalar_overrides.len(), - special_condition_count: state.special_conditions.len(), - enabled_special_condition_count: state - .special_conditions - .values() - .filter(|value| **value != 0) - .count(), - save_profile_kind: state.save_profile.profile_kind.clone(), - save_profile_family: state.save_profile.profile_family.clone(), - save_profile_map_path: state.save_profile.map_path.clone(), - save_profile_display_name: state.save_profile.display_name.clone(), - save_profile_selected_year_profile_lane: state.save_profile.selected_year_profile_lane, - save_profile_sandbox_enabled: state.save_profile.sandbox_enabled, - save_profile_campaign_scenario_enabled: state.save_profile.campaign_scenario_enabled, - save_profile_staged_profile_copy_on_restore: state - .save_profile - .staged_profile_copy_on_restore, - total_event_record_service_count: state.service_state.total_event_record_services, - periodic_boundary_call_count: state.service_state.periodic_boundary_calls, - annual_finance_service_call_count: state.service_state.annual_finance_service_calls, - periodic_route_preference_override_apply_count: state - .service_state - .periodic_route_preference_override_apply_count, - periodic_route_preference_override_restore_count: state - .service_state - .periodic_route_preference_override_restore_count, - annual_dividend_adjustment_commit_count: state - .service_state - .annual_dividend_adjustment_commit_count, - annual_bond_last_retired_principal_total: state - .service_state - .annual_bond_last_retired_principal_total, - annual_bond_last_issued_principal_total: state - .service_state - .annual_bond_last_issued_principal_total, - annual_bond_last_principal_flow_relation: - runtime_annual_bond_principal_flow_relation_label( - state.service_state.annual_bond_last_retired_principal_total, - state.service_state.annual_bond_last_issued_principal_total, - ) - .map(str::to_string), - annual_stock_repurchase_last_share_count: state - .service_state - .annual_stock_repurchase_last_share_count, - annual_stock_issue_last_share_count: state - .service_state - .annual_stock_issue_last_share_count, - total_trigger_dispatch_count: state - .service_state - .trigger_dispatch_counts - .values() - .sum(), - dirty_rerun_count: state.service_state.dirty_rerun_count, - total_company_cash: state - .companies - .iter() - .map(|company| company.current_cash) - .sum(), - } - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use crate::{ - CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, - RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimePlayer, - RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeTerritory, - RuntimeTrackPieceCounts, RuntimeWorldRestoreState, - }; - - use super::RuntimeSummary; - - #[test] - fn counts_structural_only_and_missing_context_frontiers() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - auto_show_grade_during_track_lay_raw_u8: Some(2), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(1), - company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: Some(1), - trains: Vec::new(), - locomotive_catalog: vec![ - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 10, - name: "Big Boy 4-8-8-4".to_string(), - }, - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 58, - name: "VL80T".to_string(), - }, - ], - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: Some(RuntimePackedEventCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 11, - live_record_count: 5, - live_entry_ids: vec![3, 7, 9, 10, 11], - decoded_record_count: 5, - imported_runtime_record_count: 0, - records: vec![ - RuntimePackedEventRecordSummary { - record_index: 0, - live_entry_id: 3, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some("blocked_missing_compact_control".to_string()), - notes: Vec::new(), - }, - RuntimePackedEventRecordSummary { - record_index: 1, - live_entry_id: 7, - payload_offset: Some(0x7262), - payload_len: Some(48), - decode_status: "parity_only".to_string(), - payload_family: "synthetic_harness".to_string(), - trigger_kind: Some(7), - active: Some(true), - marks_collection_dirty: Some(false), - one_shot: Some(false), - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some("blocked_missing_company_context".to_string()), - notes: Vec::new(), - }, - RuntimePackedEventRecordSummary { - record_index: 2, - live_entry_id: 9, - payload_offset: Some(0x7292), - payload_len: Some(48), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some( - "blocked_company_condition_scope_disabled".to_string(), - ), - notes: Vec::new(), - }, - RuntimePackedEventRecordSummary { - record_index: 3, - live_entry_id: 10, - payload_offset: Some(0x72c2), - payload_len: Some(48), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some("blocked_player_condition_scope".to_string()), - notes: Vec::new(), - }, - RuntimePackedEventRecordSummary { - record_index: 4, - live_entry_id: 11, - payload_offset: Some(0x72f2), - payload_len: Some(48), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some("blocked_territory_condition_scope".to_string()), - notes: Vec::new(), - }, - ], - }), - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.packed_event_blocked_missing_compact_control_count, - 1 - ); - assert_eq!( - summary.packed_event_blocked_unmapped_real_descriptor_count, - 0 - ); - assert_eq!(summary.packed_event_blocked_structural_only_count, 0); - assert_eq!( - summary.packed_event_blocked_missing_company_context_count, - 1 - ); - assert_eq!( - summary.packed_event_blocked_missing_selection_context_count, - 0 - ); - assert_eq!( - summary.packed_event_blocked_missing_company_role_context_count, - 0 - ); - assert_eq!( - summary.packed_event_blocked_missing_condition_context_count, - 0 - ); - assert_eq!( - summary.packed_event_blocked_company_condition_scope_disabled_count, - 1 - ); - assert_eq!(summary.packed_event_blocked_player_condition_scope_count, 1); - assert_eq!( - summary.packed_event_blocked_territory_condition_scope_count, - 1 - ); - } - - #[test] - fn summarizes_save_world_issue_and_economic_tuning_restore_state() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(0x0201), - partial_year_progress_raw_u8: Some(3), - current_calendar_tuple_word_raw_u32: Some(0x0108_0210), - current_calendar_tuple_word_2_raw_u32: Some(0x35e6_3160), - absolute_counter_raw_u32: Some(5), - absolute_counter_mirror_raw_u32: Some(5), - issue_37_value: Some(3), - issue_38_value: Some(1), - issue_39_value: Some(2), - issue_3a_value: Some(4), - issue_37_multiplier_raw_u32: Some(0x3d75c28f), - issue_37_multiplier_value_f32_text: Some("0.06".to_string()), - stock_issue_and_buyback_policy_raw_u8: Some(0), - bond_issue_and_repayment_policy_raw_u8: Some(1), - bankruptcy_policy_raw_u8: Some(0), - dividend_policy_raw_u8: Some(1), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_allowed: Some(false), - bankruptcy_allowed: Some(true), - dividend_adjustment_allowed: Some(false), - economic_tuning_mirror_raw_u32: Some(0x3f46dff5), - economic_tuning_mirror_value_f32_text: Some("0.7766201".to_string()), - economic_tuning_lane_raw_u32: vec![ - 0x3f400000, 0x3be56042, 0x3c03126f, 0x3c1374bc, 0x3c23d70a, 0x3c23d70a, - ], - economic_tuning_lane_value_f32_text: vec![ - "0.75".to_string(), - "0.007".to_string(), - "0.008".to_string(), - "0.009".to_string(), - "0.01".to_string(), - "0.01".to_string(), - ], - linked_site_removal_follow_on_gate_raw_u8: Some(1), - linked_site_removal_follow_on_gate_enabled: Some(true), - auto_show_grade_during_track_lay_raw_u8: Some(2), - starting_building_density_level_raw_u8: Some(3), - post_text_building_density_growth_raw_u8: Some(1), - leftover_simulation_time_accumulator_raw_u32: Some(0x3f000000), - leftover_simulation_time_accumulator_value_f32_text: Some("0.500000".to_string()), - selected_year_lane_snapshot_raw_u8: Some(7), - all_steam_locomotives_available_raw_u8: Some(1), - all_steam_locomotives_available_enabled: Some(true), - all_diesel_locomotives_available_raw_u8: Some(0), - all_diesel_locomotives_available_enabled: Some(false), - all_electric_locomotives_available_raw_u8: Some(1), - all_electric_locomotives_available_enabled: Some(true), - cached_available_locomotive_rating_raw_u32: Some(0x41a00000), - cached_available_locomotive_rating_value_f32_text: Some("20.000000".to_string()), - selected_year_bucket_scalar_raw_u32: Some(25.0f32.to_bits()), - selected_year_bucket_scalar_value_f32_text: Some("25.000000".to_string()), - selected_year_bucket_direct_lane_raw_u32: vec![ - 22.5f32.to_bits(), - 26.25f32.to_bits(), - 17.5f32.to_bits(), - ], - selected_year_bucket_direct_lane_value_f32_text: vec![ - "22.500000".to_string(), - "26.250000".to_string(), - "17.500000".to_string(), - ], - selected_year_bucket_complement_lane_raw_u32: vec![ - 0.999121f32.to_bits(), - 0.998998f32.to_bits(), - 0.999210f32.to_bits(), - ], - selected_year_bucket_complement_lane_value_f32_text: vec![ - "0.999121".to_string(), - "0.998998".to_string(), - "0.999210".to_string(), - ], - selected_year_bucket_scaled_companion_lane_raw_u32: vec![ - 139.16667f32.to_bits(), - 122.5f32.to_bits(), - 171.42857f32.to_bits(), - ], - selected_year_bucket_scaled_companion_lane_value_f32_text: vec![ - "139.166672".to_string(), - "122.500000".to_string(), - "171.428574".to_string(), - ], - selected_year_gap_scalar_raw_u32: Some(0x3eaaaaab), - selected_year_gap_scalar_value_f32_text: Some("0.333333".to_string()), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(1), - company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: Some(1), - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_production_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - - assert_eq!(summary.world_restore_packed_year_word_raw_u16, Some(0x0201)); - assert_eq!(summary.world_restore_partial_year_progress_raw_u8, Some(3)); - assert_eq!( - summary.world_restore_current_calendar_tuple_word_raw_u32, - Some(0x0108_0210) - ); - assert_eq!( - summary.world_restore_current_calendar_tuple_word_2_raw_u32, - Some(0x35e6_3160) - ); - assert_eq!(summary.world_restore_absolute_counter_raw_u32, Some(5)); - assert_eq!( - summary.world_restore_absolute_counter_mirror_raw_u32, - Some(5) - ); - assert_eq!(summary.world_restore_issue_37_value, Some(3)); - assert_eq!(summary.world_restore_issue_38_value, Some(1)); - assert_eq!(summary.world_restore_issue_39_value, Some(2)); - assert_eq!(summary.world_restore_issue_3a_value, Some(4)); - assert_eq!( - summary.world_restore_issue_37_multiplier_raw_u32, - Some(0x3d75c28f) - ); - assert_eq!( - summary.world_restore_stock_issue_and_buyback_policy_raw_u8, - Some(0) - ); - assert_eq!( - summary.world_restore_bond_issue_and_repayment_policy_raw_u8, - Some(1) - ); - assert_eq!(summary.world_restore_bankruptcy_policy_raw_u8, Some(0)); - assert_eq!(summary.world_restore_dividend_policy_raw_u8, Some(1)); - assert_eq!( - summary.world_restore_stock_issue_and_buyback_allowed, - Some(true) - ); - assert_eq!( - summary.world_restore_bond_issue_and_repayment_allowed, - Some(false) - ); - assert_eq!(summary.world_restore_bankruptcy_allowed, Some(true)); - assert_eq!( - summary.world_restore_dividend_adjustment_allowed, - Some(false) - ); - assert_eq!( - summary - .world_restore_issue_37_multiplier_value_f32_text - .as_deref(), - Some("0.06") - ); - assert_eq!( - summary.world_restore_economic_tuning_mirror_raw_u32, - Some(0x3f46dff5) - ); - assert_eq!( - summary - .world_restore_economic_tuning_mirror_value_f32_text - .as_deref(), - Some("0.7766201") - ); - assert_eq!( - summary.world_restore_linked_site_removal_follow_on_gate_raw_u8, - Some(1) - ); - assert_eq!( - summary.world_restore_linked_site_removal_follow_on_gate_enabled, - Some(true) - ); - assert_eq!( - summary.world_restore_auto_show_grade_during_track_lay_raw_u8, - Some(2) - ); - assert_eq!( - summary.world_restore_starting_building_density_level_raw_u8, - Some(3) - ); - assert_eq!( - summary.world_restore_post_text_building_density_growth_raw_u8, - Some(1) - ); - assert_eq!( - summary.world_restore_leftover_simulation_time_accumulator_raw_u32, - Some(0x3f000000) - ); - assert_eq!( - summary - .world_restore_leftover_simulation_time_accumulator_value_f32_text - .as_deref(), - Some("0.500000") - ); - assert_eq!( - summary.world_restore_selected_year_lane_snapshot_raw_u8, - Some(7) - ); - assert_eq!( - summary.world_restore_all_steam_locomotives_available_raw_u8, - Some(1) - ); - assert_eq!( - summary.world_restore_all_steam_locomotives_available_enabled, - Some(true) - ); - assert_eq!( - summary.world_restore_all_diesel_locomotives_available_raw_u8, - Some(0) - ); - assert_eq!( - summary.world_restore_all_diesel_locomotives_available_enabled, - Some(false) - ); - assert_eq!( - summary.world_restore_all_electric_locomotives_available_raw_u8, - Some(1) - ); - assert_eq!( - summary.world_restore_all_electric_locomotives_available_enabled, - Some(true) - ); - assert_eq!( - summary.world_restore_cached_available_locomotive_rating_raw_u32, - Some(0x41a00000) - ); - assert_eq!( - summary - .world_restore_cached_available_locomotive_rating_value_f32_text - .as_deref(), - Some("20.000000") - ); - assert_eq!( - summary.world_restore_selected_year_bucket_scalar_raw_u32, - Some(25.0f32.to_bits()) - ); - assert_eq!( - summary - .world_restore_selected_year_bucket_scalar_value_f32_text - .as_deref(), - Some("25.000000") - ); - assert_eq!( - summary.world_restore_selected_year_bucket_direct_lane_count, - 3 - ); - assert_eq!( - summary.world_restore_selected_year_bucket_direct_lane_value_f32_text, - vec!["22.500000", "26.250000", "17.500000"] - ); - assert_eq!( - summary.world_restore_selected_year_bucket_complement_lane_count, - 3 - ); - assert_eq!( - summary.world_restore_selected_year_bucket_complement_lane_value_f32_text, - vec!["0.999121", "0.998998", "0.999210"] - ); - assert_eq!( - summary.world_restore_selected_year_bucket_scaled_companion_lane_count, - 3 - ); - assert_eq!( - summary.world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text, - vec!["139.166672", "122.500000", "171.428574"] - ); - assert_eq!(summary.world_restore_economic_tuning_lane_count, 6); - assert_eq!( - summary.world_restore_economic_tuning_lane_value_f32_text, - vec!["0.75", "0.007", "0.008", "0.009", "0.01", "0.01"] - ); - assert_eq!( - summary.world_restore_selected_year_gap_scalar_raw_u32, - Some(0x3eaaaaab) - ); - assert_eq!( - summary - .world_restore_selected_year_gap_scalar_value_f32_text - .as_deref(), - Some("0.333333") - ); - } - - #[test] - fn counts_active_companies_separately_from_total_companies() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - auto_show_grade_during_track_lay_raw_u8: Some(2), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![ - RuntimeCompany { - company_id: 1, - current_cash: 10, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: true, - available_track_laying_capacity: None, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Human, - }, - RuntimeCompany { - company_id: 2, - current_cash: 20, - debt: 0, - credit_rating_score: None, - prime_rate: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - active: false, - available_track_laying_capacity: Some(7), - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - controller_kind: RuntimeCompanyControllerKind::Ai, - }, - ], - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(1), - company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: Some(1), - trains: Vec::new(), - locomotive_catalog: vec![ - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 10, - name: "Big Boy 4-8-8-4".to_string(), - }, - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 58, - name: "VL80T".to_string(), - }, - ], - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!(summary.company_count, 2); - assert_eq!(summary.active_company_count, 1); - } - - #[test] - fn counts_named_locomotive_availability_entries_and_zero_values() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - auto_show_grade_during_track_lay_raw_u8: Some(2), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: vec![ - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 10, - name: "Big Boy 4-8-8-4".to_string(), - }, - crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 58, - name: "VL80T".to_string(), - }, - ], - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::from([ - ("Big Boy".to_string(), 0), - ("GP7".to_string(), 1), - ("Mikado".to_string(), 0), - ]), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!(summary.locomotive_catalog_count, 2); - assert_eq!(summary.named_locomotive_availability_count, 3); - assert_eq!(summary.zero_named_locomotive_availability_count, 2); - assert_eq!(summary.named_locomotive_cost_count, 0); - } - - #[test] - fn counts_named_locomotive_cost_entries() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::from([ - ("Big Boy".to_string(), 250000), - ("GP7".to_string(), 175000), - ]), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - - assert_eq!(summary.named_locomotive_cost_count, 2); - } - - #[test] - fn counts_world_scalar_override_surfaces() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - territory_access_cost: Some(750000), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::from([(1, 125), (2, 250)]), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - - assert_eq!(summary.cargo_production_override_count, 2); - assert_eq!(summary.world_restore_territory_access_cost, Some(750000)); - } - - #[test] - fn counts_runtime_variable_surfaces() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: RuntimeCompanyControllerKind::Human, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: None, - players: vec![RuntimePlayer { - player_id: 2, - current_cash: 0, - active: true, - controller_kind: RuntimeCompanyControllerKind::Human, - }], - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: vec![RuntimeTerritory { - territory_id: 3, - name: Some("East".to_string()), - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::from([(1, 9)]), - company_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(2, 11)]))]), - player_runtime_variables: BTreeMap::from([(2, BTreeMap::from([(3, 13)]))]), - territory_runtime_variables: BTreeMap::from([(3, BTreeMap::from([(4, 15)]))]), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - - assert_eq!(summary.world_runtime_variable_count, 1); - assert_eq!(summary.company_runtime_variable_owner_count, 1); - assert_eq!(summary.player_runtime_variable_owner_count, 1); - assert_eq!(summary.territory_runtime_variable_owner_count, 1); - } - - #[test] - fn counts_world_frontier_buckets_separately() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: Some(RuntimePackedEventCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 2, - live_record_count: 2, - live_entry_ids: vec![21, 22], - decoded_record_count: 2, - imported_runtime_record_count: 0, - records: vec![ - RuntimePackedEventRecordSummary { - record_index: 0, - live_entry_id: 21, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some("blocked_unmapped_world_descriptor".to_string()), - notes: Vec::new(), - }, - RuntimePackedEventRecordSummary { - record_index: 1, - live_entry_id: 22, - payload_offset: Some(0x7242), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 1, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![0, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some("blocked_unmapped_world_condition".to_string()), - notes: Vec::new(), - }, - ], - }), - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.packed_event_blocked_unmapped_world_descriptor_count, - 1 - ); - assert_eq!( - summary.packed_event_blocked_unmapped_world_condition_count, - 1 - ); - } - - #[test] - fn counts_missing_locomotive_catalog_context_frontier() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: Some(RuntimePackedEventCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 1, - live_record_count: 1, - live_entry_ids: vec![1], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records: vec![RuntimePackedEventRecordSummary { - record_index: 0, - live_entry_id: 1, - payload_offset: Some(0x7202), - payload_len: Some(96), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some("blocked_missing_locomotive_catalog_context".to_string()), - notes: Vec::new(), - }], - }), - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.packed_event_blocked_missing_locomotive_catalog_context_count, - 1 - ); - } - - #[test] - fn counts_shell_owned_descriptor_frontier() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), - metadata: BTreeMap::new(), - companies: Vec::new(), - selected_company_id: None, - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: Some(RuntimePackedEventCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), - mechanism_family: "classic-save-rehydrate-v1".to_string(), - mechanism_confidence: "grounded".to_string(), - container_profile_family: Some("rt3-classic-save-container-v1".to_string()), - packed_state_version: 0x3e9, - packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 1, - live_record_count: 1, - live_entry_ids: vec![1], - decoded_record_count: 1, - imported_runtime_record_count: 0, - records: vec![RuntimePackedEventRecordSummary { - record_index: 0, - live_entry_id: 1, - payload_offset: Some(0), - payload_len: Some(0), - decode_status: "parity_only".to_string(), - payload_family: "real_packed_v1".to_string(), - trigger_kind: Some(7), - active: None, - marks_collection_dirty: None, - one_shot: None, - compact_control: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, - standalone_condition_rows: Vec::new(), - negative_sentinel_scope: None, - grouped_effect_row_counts: vec![1, 0, 0, 0], - grouped_effect_rows: Vec::new(), - grouped_company_targets: Vec::new(), - decoded_conditions: Vec::new(), - decoded_actions: Vec::new(), - executable_import_ready: false, - import_outcome: Some("blocked_shell_owned_descriptor".to_string()), - notes: Vec::new(), - }], - }), - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState::default(), - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!(summary.packed_event_blocked_shell_owned_descriptor_count, 1); - } - - #[test] - fn summarizes_selected_company_market_state() { - let state = RuntimeState { - calendar: CalendarPoint { - year: 1830, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - auto_show_grade_during_track_lay_raw_u8: Some(2), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 1, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(1), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 1, - name: "Chairman One".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(1), - company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: Some(1), - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyMarketState { - outstanding_shares: 20_000, - bond_count: 2, - live_bond_slots: Vec::new(), - highest_coupon_live_bond_principal: Some(350_000), - largest_live_bond_principal: Some(500_000), - mutable_support_scalar_raw_u32: 0x3f800000, - young_company_support_scalar_raw_u32: 0x42340000, - support_progress_word: 12, - recent_per_share_cache_absolute_counter: 42, - recent_per_share_cached_value_bits: 14.5f64.to_bits(), - recent_per_share_subscore_raw_u32: 0x420c0000, - cached_share_price_raw_u32: 0x42200000, - chairman_salary_baseline: 24, - chairman_salary_current: 30, - chairman_bonus_year: 1842, - chairman_bonus_amount: 750, - founding_year: 1831, - last_bankruptcy_year: 0, - last_dividend_year: 1841, - current_issue_calendar_word: 5, - current_issue_calendar_word_2: 6, - prior_issue_calendar_word: 4, - prior_issue_calendar_word_2: 5, - city_connection_latch: true, - linked_transit_latch: false, - linked_transit_route_anchor_entry_id: Some(77), - linked_transit_route_anchor_fallback_counts: vec![3, 5, 8], - stat_band_root_0cfb_candidates: vec![ - crate::RuntimeCompanyStatBandCandidate { - label: "stat_band_0cfb_word_1".to_string(), - relative_offset: 0x0cfb, - relative_offset_hex: "0xcfb".to_string(), - raw_u32: 1, - raw_u32_hex: "0x00000001".to_string(), - value_i32: 1, - value_f32_text: "0.000000".to_string(), - }, - crate::RuntimeCompanyStatBandCandidate { - label: "stat_band_0cfb_word_2".to_string(), - relative_offset: 0x0cff, - relative_offset_hex: "0xcff".to_string(), - raw_u32: 2, - raw_u32_hex: "0x00000002".to_string(), - value_i32: 2, - value_f32_text: "0.000000".to_string(), - }, - ], - stat_band_root_0d7f_candidates: vec![ - crate::RuntimeCompanyStatBandCandidate { - label: "stat_band_0d7f_word_1".to_string(), - relative_offset: 0x0d7f, - relative_offset_hex: "0xd7f".to_string(), - raw_u32: 3, - raw_u32_hex: "0x00000003".to_string(), - value_i32: 3, - value_f32_text: "0.000000".to_string(), - }, - ], - stat_band_root_1c47_candidates: vec![ - crate::RuntimeCompanyStatBandCandidate { - label: "stat_band_1c47_word_1".to_string(), - relative_offset: 0x1c47, - relative_offset_hex: "0x1c47".trim_start_matches("0x").to_string(), - raw_u32: 4, - raw_u32_hex: "0x00000004".to_string(), - value_i32: 4, - value_f32_text: "0.000000".to_string(), - }, - ], - year_stat_family_qword_bits: Vec::new(), - special_stat_family_232a_qword_bits: Vec::new(), - issue_opinion_terms_raw_i32: Vec::new(), - direct_control_transfer_float_fields_raw_u32: BTreeMap::new(), - direct_control_transfer_int_fields_raw_u32: BTreeMap::new(), - }, - )]), - company_periodic_side_latch_state: BTreeMap::from([( - 1, - crate::RuntimeCompanyPeriodicSideLatchState { - preferred_locomotive_engine_type_raw_u8: Some(2), - city_connection_latch: true, - linked_transit_latch: false, - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!(summary.company_market_state_owner_count, 1); - assert_eq!(summary.selected_company_outstanding_shares, Some(20_000)); - assert_eq!(summary.selected_company_bond_count, Some(2)); - assert_eq!( - summary.selected_company_largest_live_bond_principal, - Some(500_000) - ); - assert_eq!( - summary.selected_company_highest_coupon_live_bond_principal, - Some(350_000) - ); - assert_eq!(summary.selected_company_assigned_share_pool, Some(5_000)); - assert_eq!(summary.selected_company_unassigned_share_pool, Some(15_000)); - assert_eq!(summary.selected_company_cached_share_price, Some(40)); - assert_eq!( - summary.selected_company_cached_share_price_value_f32_text, - Some("40.000000".to_string()) - ); - assert_eq!( - summary.selected_company_recent_per_share_cache_absolute_counter, - Some(42) - ); - assert_eq!( - summary.selected_company_recent_per_share_cached_value_f64_text, - Some("14.500000".to_string()) - ); - assert_eq!( - summary.selected_company_recent_per_share_subscore_value_f32_text, - Some("35.000000".to_string()) - ); - assert_eq!( - summary.selected_company_mutable_support_scalar_value_f32_text, - Some("1.000000".to_string()) - ); - assert_eq!(summary.selected_company_stat_band_root_0cfb_count, 2); - assert_eq!(summary.selected_company_stat_band_root_0d7f_count, 1); - assert_eq!(summary.selected_company_stat_band_root_1c47_count, 1); - assert_eq!(summary.selected_company_last_dividend_year, Some(1841)); - assert_eq!(summary.selected_company_years_since_founding, None); - assert_eq!(summary.selected_company_years_since_last_bankruptcy, None); - assert_eq!(summary.selected_company_years_since_last_dividend, None); - assert_eq!( - summary.selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8, - Some(2) - ); - assert_eq!( - summary.selected_company_periodic_side_latch_city_connection_latch, - Some(true) - ); - assert_eq!( - summary.selected_company_periodic_side_latch_linked_transit_latch, - Some(false) - ); - assert_eq!( - summary.selected_company_linked_transit_route_anchor_entry_id, - Some(77) - ); - assert_eq!( - summary.selected_company_linked_transit_route_anchor_fallback_counts, - vec![3, 5, 8] - ); - assert_eq!( - summary.selected_company_periodic_service_base_route_preference_raw_u8, - Some(2) - ); - assert_eq!( - summary.selected_company_periodic_service_effective_route_preference_raw_u8, - Some(2) - ); - assert_eq!( - summary.selected_company_periodic_service_electric_route_preference_override_active, - Some(true) - ); - assert_eq!( - summary.selected_company_periodic_service_route_quality_multiplier_basis_points, - Some(180) - ); - assert_eq!( - summary.active_periodic_route_preference_override_company_id, - None - ); - assert_eq!( - summary.last_periodic_route_preference_override_company_id, - None - ); - assert_eq!(summary.periodic_route_preference_override_apply_count, 0); - assert_eq!(summary.periodic_route_preference_override_restore_count, 0); - assert_eq!(summary.selected_company_chairman_bonus_year, Some(1842)); - assert_eq!(summary.selected_company_chairman_bonus_amount, Some(750)); - } - - #[test] - fn summarizes_selected_company_creditor_pressure_branch_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = - (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x0d, -700_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x12, 0.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 80_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -115_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -110_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 7, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(7), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 7, - crate::RuntimeCompanyMarketState { - founding_year: 1841, - last_bankruptcy_year: 1832, - cached_share_price_raw_u32: 25.0f32.to_bits(), - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.selected_company_creditor_pressure_recent_bad_net_profit_year_count, - Some(3) - ); - assert_eq!( - summary.selected_company_creditor_pressure_recent_peak_revenue, - Some(100_000) - ); - assert_eq!( - summary.selected_company_creditor_pressure_recent_three_year_net_profit_total, - Some(-65_000) - ); - assert_eq!( - summary.selected_company_creditor_pressure_cash_floor, - Some(-600_000) - ); - assert_eq!( - summary.selected_company_creditor_pressure_cash_plus_slot_12_total, - Some(-700_000) - ); - assert_eq!( - summary.selected_company_creditor_pressure_share_price_floor, - Some(20) - ); - assert_eq!( - summary.selected_company_creditor_pressure_share_price_scalar, - Some(25) - ); - assert_eq!( - summary.selected_company_creditor_pressure_current_fuel_cost, - Some(-50_000) - ); - assert_eq!( - summary.selected_company_creditor_pressure_current_fuel_cost_floor, - Some(-48_000) - ); - assert_eq!( - summary.selected_company_creditor_pressure_eligible_for_bankruptcy_branch, - Some(true) - ); - } - - #[test] - fn summarizes_selected_company_deep_distress_fallback_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = - (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, -350_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 9, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(9), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 9, - crate::RuntimeCompanyMarketState { - founding_year: 1841, - last_bankruptcy_year: 1840, - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.selected_company_deep_distress_current_cash, - Some(-350_000) - ); - assert_eq!( - summary.selected_company_deep_distress_recent_first_three_net_profit_years, - vec![-25_000, -23_000, -21_000] - ); - assert_eq!( - summary.selected_company_deep_distress_cash_floor, - Some(-300_000) - ); - assert_eq!( - summary.selected_company_deep_distress_net_profit_floor, - Some(-20_000) - ); - assert_eq!( - summary.selected_company_deep_distress_eligible_for_bankruptcy_fallback, - Some(true) - ); - } - - #[test] - fn summarizes_selected_company_stock_repurchase_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, 1_600_000.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - partial_year_progress_raw_u8: Some(0x0c), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - building_density_growth_setting_raw_u32: Some(1), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 12, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: Some(3), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(12), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 3, - name: "Jay".to_string(), - active: true, - current_cash: 200, - linked_company_id: Some(12), - company_holdings: BTreeMap::from([(12, 14_500)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: Vec::new().into_iter().collect(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 12, - crate::RuntimeCompanyMarketState { - outstanding_shares: 20_000, - cached_share_price_raw_u32: 20.0f32.to_bits(), - founding_year: 1835, - city_connection_latch: true, - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - chairman_personality_raw_u8: BTreeMap::from([(3, 20)]), - ..RuntimeServiceState::default() - }, - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.world_restore_building_density_growth_setting_raw_u32, - Some(1) - ); - assert_eq!( - summary.selected_company_stock_repurchase_building_density_growth_setting, - Some(1) - ); - assert_eq!( - summary.selected_company_stock_repurchase_linked_chairman_personality_raw_u8, - Some(20) - ); - assert_eq!( - summary.selected_company_stock_repurchase_batch_size, - Some(1_000) - ); - assert_eq!( - summary.selected_company_stock_repurchase_factor_basis_points, - Some(432) - ); - assert_eq!( - summary.selected_company_stock_repurchase_current_cash, - Some(1_600_000) - ); - assert_eq!( - summary.selected_company_stock_repurchase_stock_value_gate_cash_floor, - Some(3_456_000) - ); - assert_eq!( - summary.selected_company_stock_repurchase_support_adjusted_share_price_scalar, - Some(20) - ); - assert_eq!( - summary.selected_company_stock_repurchase_affordability_cash_floor, - Some(103_680) - ); - assert_eq!( - summary.selected_company_stock_repurchase_unassigned_share_pool, - Some(5_500) - ); - assert_eq!( - summary.selected_company_stock_repurchase_eligible_for_single_batch, - Some(false) - ); - } - - #[test] - fn summarizes_selected_company_annual_bond_policy_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = (-400_000.0f64).to_bits(); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - partial_year_progress_raw_u8: Some(0x0c), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 11, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(11), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: Vec::new().into_iter().collect(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - company_market_state: BTreeMap::from([( - 11, - crate::RuntimeCompanyMarketState { - bond_count: 2, - linked_transit_latch: true, - live_bond_slots: vec![ - crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 200_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.09f32.to_bits(), - }, - crate::RuntimeCompanyBondSlot { - slot_index: 1, - principal: 150_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.08f32.to_bits(), - }, - ], - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.selected_company_annual_bond_linked_transit_latch, - Some(true) - ); - assert_eq!( - summary.selected_company_annual_bond_live_bond_count, - Some(2) - ); - assert_eq!( - summary.selected_company_annual_bond_live_bond_principal_total, - Some(350_000) - ); - assert_eq!( - summary.selected_company_annual_bond_matured_live_bond_count, - Some(0) - ); - assert_eq!( - summary.selected_company_annual_bond_matured_live_bond_principal_total, - Some(0) - ); - assert_eq!( - summary.selected_company_annual_bond_next_live_bond_maturity_year, - None - ); - assert_eq!( - summary.selected_company_annual_bond_live_bond_coupon_burden_total, - Some(30_000) - ); - assert_eq!( - summary.selected_company_annual_bond_current_cash, - Some(-400_000) - ); - assert_eq!( - summary.selected_company_annual_bond_cash_after_full_repayment, - Some(-750_000) - ); - assert_eq!( - summary.selected_company_annual_bond_issue_cash_floor, - Some(-30_000) - ); - assert_eq!( - summary.selected_company_annual_bond_issue_principal_step, - Some(500_000) - ); - assert_eq!( - summary.selected_company_annual_bond_proposed_issue_bond_count, - Some(2) - ); - assert_eq!( - summary.selected_company_annual_bond_proposed_issue_total_principal, - Some(1_000_000) - ); - assert_eq!( - summary.selected_company_annual_bond_proposed_issue_years_to_maturity, - Some(30) - ); - assert_eq!( - summary.selected_company_annual_bond_eligible_for_issue_branch, - Some(true) - ); - } - - #[test] - fn summarizes_selected_company_stock_issue_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize] = 250_000.0f64.to_bits(); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState { - partial_year_progress_raw_u8: Some(0x0c), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - issue_37_value: Some(2), - issue_37_multiplier_raw_u32: Some(1.0f32.to_bits()), - issue_37_multiplier_value_f32_text: Some("1.000000".to_string()), - absolute_counter_raw_u32: Some(885_911_040), - ..RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![RuntimeCompany { - company_id: 14, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: Some(8), - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(14), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 8, - name: "Taylor".to_string(), - active: true, - current_cash: 200, - linked_company_id: Some(14), - company_holdings: BTreeMap::from([(14, 14_000)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: Vec::new().into_iter().collect(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: RuntimeServiceState { - world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], - chairman_personality_raw_u8: BTreeMap::from([(8, 20)]), - annual_finance_last_actions: BTreeMap::from([( - 14, - crate::RuntimeCompanyAnnualFinancePolicyAction::StockIssue, - )]), - annual_finance_last_news_family_candidates: BTreeMap::from([( - 14, - "4053".to_string(), - )]), - annual_finance_last_news_events: vec![crate::RuntimeAnnualFinanceNewsEvent { - company_id: 14, - selector_label: "4053".to_string(), - action_label: "stock_issue".to_string(), - retired_principal_total: 0, - issued_principal_total: 0, - repurchased_share_count: 0, - issued_share_count: 4_000, - }], - company_market_state: BTreeMap::from([( - 14, - crate::RuntimeCompanyMarketState { - outstanding_shares: 20_000, - bond_count: 2, - highest_coupon_live_bond_principal: Some(300_000), - current_issue_calendar_word: 0x0101_0725, - current_issue_calendar_word_2: 0x0001_0001, - founding_year: 1840, - cached_share_price_raw_u32: 35.0f32.to_bits(), - recent_per_share_cache_absolute_counter: 885_911_040, - recent_per_share_cached_value_bits: 34.0f64.to_bits(), - live_bond_slots: vec![ - crate::RuntimeCompanyBondSlot { - slot_index: 0, - principal: 300_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.11f32.to_bits(), - }, - crate::RuntimeCompanyBondSlot { - slot_index: 1, - principal: 200_000, - maturity_year: 0, - coupon_rate_raw_u32: 0.07f32.to_bits(), - }, - ], - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x32f, - 30.0f32.to_bits(), - )]), - year_stat_family_qword_bits, - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..RuntimeServiceState::default() - }, - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.selected_company_stock_issue_live_bond_count, - Some(2) - ); - assert_eq!( - summary.selected_company_stock_issue_initial_batch_size, - Some(2_000) - ); - assert_eq!( - summary.selected_company_stock_issue_trimmed_batch_size, - Some(2_000) - ); - assert_eq!( - summary.selected_company_stock_issue_share_pressure_basis_points, - Some(-1_000) - ); - assert_eq!( - summary.selected_company_stock_issue_pressured_share_price_scalar, - Some(35) - ); - assert_eq!( - summary.selected_company_stock_issue_pressured_proceeds, - Some(70_000) - ); - assert_eq!( - summary.selected_company_stock_issue_book_value_per_share_floor_applied, - Some(30) - ); - assert_eq!( - summary.selected_company_stock_issue_price_to_book_ratio_basis_points, - Some(11_667) - ); - assert_eq!( - summary.selected_company_stock_issue_current_cash, - Some(250_000) - ); - assert_eq!( - summary.selected_company_stock_issue_highest_coupon_live_bond_principal, - Some(300_000) - ); - assert_eq!( - summary.selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points, - Some(1_100) - ); - assert_eq!( - summary.selected_company_stock_issue_current_issue_age_absolute_counter_delta, - Some(967_680) - ); - assert_eq!( - summary.selected_company_stock_issue_current_issue_cooldown_floor, - Some(483_840) - ); - assert_eq!( - summary.selected_company_stock_issue_minimum_price_to_book_ratio_basis_points, - Some(8_000) - ); - assert_eq!( - summary.selected_company_stock_issue_passes_share_price_floor, - Some(true) - ); - assert_eq!( - summary.selected_company_stock_issue_passes_proceeds_floor, - Some(true) - ); - assert_eq!( - summary.selected_company_stock_issue_passes_cash_gate, - Some(true) - ); - assert_eq!( - summary.selected_company_stock_issue_passes_issue_cooldown_gate, - Some(true) - ); - assert_eq!( - summary.selected_company_stock_issue_passes_coupon_price_to_book_gate, - Some(true) - ); - assert_eq!( - summary.selected_company_stock_issue_eligible_for_double_tranche, - Some(true) - ); - assert_eq!( - summary - .selected_company_annual_finance_news_family_candidate - .as_deref(), - Some("4053") - ); - assert_eq!( - summary - .selected_company_annual_finance_last_news_selector - .as_deref(), - Some("4053") - ); - assert_eq!(summary.annual_finance_last_news_event_count, 1); - } - - #[test] - fn summarizes_selected_company_annual_dividend_policy_state() { - let mut year_stat_family_qword_bits = vec![ - 0u64; - ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) - * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) - as usize - ]; - let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { - let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; - bits[index] = value.to_bits(); - }; - let write_prior_year_value = - |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { - let index = - (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; - bits[index] = value.to_bits(); - }; - write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); - write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); - write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); - - let state = RuntimeState { - calendar: CalendarPoint { - year: 1845, - month_slot: 0, - phase_slot: 0, - tick_slot: 0, - }, - world_flags: BTreeMap::new(), - save_profile: crate::RuntimeSaveProfileState::default(), - world_restore: crate::RuntimeWorldRestoreState { - packed_year_word_raw_u16: Some(1845), - partial_year_progress_raw_u8: Some(0x0c), - dividend_policy_raw_u8: Some(0), - dividend_adjustment_allowed: Some(true), - stock_issue_and_buyback_policy_raw_u8: Some(0), - stock_issue_and_buyback_allowed: Some(true), - bond_issue_and_repayment_policy_raw_u8: Some(0), - bond_issue_and_repayment_allowed: Some(true), - bankruptcy_policy_raw_u8: Some(0), - bankruptcy_allowed: Some(true), - building_density_growth_setting_raw_u32: Some(1), - ..crate::RuntimeWorldRestoreState::default() - }, - metadata: BTreeMap::new(), - companies: vec![crate::RuntimeCompany { - company_id: 15, - current_cash: 0, - debt: 0, - credit_rating_score: None, - prime_rate: None, - active: true, - available_track_laying_capacity: None, - controller_kind: crate::RuntimeCompanyControllerKind::Unknown, - linked_chairman_profile_id: None, - book_value_per_share: 0, - investor_confidence: 0, - management_attitude: 0, - takeover_cooldown_year: None, - merger_cooldown_year: None, - track_piece_counts: crate::RuntimeTrackPieceCounts::default(), - }], - selected_company_id: Some(15), - players: Vec::new(), - selected_player_id: None, - chairman_profiles: vec![crate::RuntimeChairmanProfile { - profile_id: 3, - name: "Chairman Three".to_string(), - active: true, - current_cash: 0, - linked_company_id: Some(15), - company_holdings: BTreeMap::from([(15, 9_500)]), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, - }], - selected_chairman_profile_id: None, - trains: Vec::new(), - locomotive_catalog: Vec::new(), - cargo_catalog: Vec::new(), - territories: Vec::new(), - company_territory_track_piece_counts: Vec::new(), - company_territory_access: Vec::new(), - packed_event_collection: None, - event_runtime_records: Vec::new(), - candidate_availability: BTreeMap::new(), - named_locomotive_availability: BTreeMap::new(), - named_locomotive_cost: BTreeMap::new(), - all_cargo_price_override: None, - named_cargo_price_overrides: BTreeMap::new(), - all_cargo_production_override: None, - factory_cargo_production_override: None, - farm_mine_cargo_production_override: None, - named_cargo_production_overrides: BTreeMap::new(), - cargo_production_overrides: BTreeMap::new(), - world_runtime_variables: BTreeMap::new(), - company_runtime_variables: BTreeMap::new(), - player_runtime_variables: BTreeMap::new(), - territory_runtime_variables: BTreeMap::new(), - world_scalar_overrides: BTreeMap::new(), - special_conditions: BTreeMap::new(), - service_state: crate::RuntimeServiceState { - company_market_state: BTreeMap::from([( - 15, - crate::RuntimeCompanyMarketState { - outstanding_shares: 10_000, - founding_year: 1840, - last_dividend_year: 1844, - year_stat_family_qword_bits, - direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( - 0x33f, - 0.4f32.to_bits(), - )]), - ..crate::RuntimeCompanyMarketState::default() - }, - )]), - ..crate::RuntimeServiceState::default() - }, - }; - - let summary = RuntimeSummary::from_state(&state); - assert_eq!( - summary.selected_company_dividend_weighted_recent_net_profit_total, - Some(600_000) - ); - assert_eq!( - summary.selected_company_dividend_weighted_recent_net_profit_average, - Some(100_000) - ); - assert_eq!( - summary.selected_company_dividend_current_cash, - Some(300_000) - ); - assert_eq!( - summary.selected_company_dividend_tiny_unassigned_share_cash_supplement_branch, - Some(true) - ); - assert_eq!( - summary.selected_company_dividend_tentative_target_per_share_tenths, - Some(133) - ); - assert_eq!( - summary.selected_company_dividend_current_per_share_tenths, - Some(4) - ); - assert_eq!( - summary.selected_company_dividend_growth_adjusted_current_per_share_tenths, - Some(3) - ); - assert_eq!( - summary.selected_company_dividend_board_approved_ceiling_tenths, - Some(18) - ); - assert_eq!( - summary.selected_company_dividend_proposed_per_share_tenths, - Some(18) - ); - assert_eq!( - summary.selected_company_dividend_eligible_for_adjustment_branch, - Some(true) - ); - assert_eq!( - summary - .selected_company_annual_finance_policy_action - .as_deref(), - Some("dividend_adjustment") - ); - assert_eq!( - summary.selected_company_annual_finance_policy_dividend_adjustment_eligible, - Some(true) - ); - } -} diff --git a/crates/rrt-runtime/src/summary/builders/base.rs b/crates/rrt-runtime/src/summary/builders/base.rs new file mode 100644 index 0000000..68718fd --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/base.rs @@ -0,0 +1,296 @@ +use crate::state::CalendarPoint; +use crate::summary::RuntimeSummary; + +pub(super) fn build_base_runtime_summary(calendar: CalendarPoint) -> RuntimeSummary { + RuntimeSummary { + calendar, + calendar_projection_source: None, + calendar_projection_is_placeholder: false, + world_flag_count: 0, + world_restore_selected_year_profile_lane: None, + world_restore_campaign_scenario_enabled: None, + world_restore_sandbox_enabled: None, + world_restore_seed_tuple_written_from_raw_lane: None, + world_restore_absolute_counter_requires_shell_context: None, + world_restore_absolute_counter_reconstructible_from_save: None, + world_restore_packed_year_word_raw_u16: None, + world_restore_partial_year_progress_raw_u8: None, + world_restore_current_calendar_tuple_word_raw_u32: None, + world_restore_current_calendar_tuple_word_2_raw_u32: None, + world_restore_absolute_counter_raw_u32: None, + world_restore_absolute_counter_mirror_raw_u32: None, + world_restore_disable_cargo_economy_special_condition_slot: None, + world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: None, + world_restore_disable_cargo_economy_special_condition_write_side_grounded: None, + world_restore_disable_cargo_economy_special_condition_enabled: None, + world_restore_use_bio_accelerator_cars_enabled: None, + world_restore_use_wartime_cargos_enabled: None, + world_restore_disable_train_crashes_enabled: None, + world_restore_disable_train_crashes_and_breakdowns_enabled: None, + world_restore_ai_ignore_territories_at_startup_enabled: None, + world_restore_limited_track_building_amount: None, + world_restore_economic_status_code: None, + world_restore_territory_access_cost: None, + world_restore_linked_site_removal_follow_on_gate_raw_u8: None, + world_restore_linked_site_removal_follow_on_gate_enabled: None, + world_restore_auto_show_grade_during_track_lay_raw_u8: None, + world_restore_starting_building_density_level_raw_u8: None, + world_restore_post_text_building_density_growth_raw_u8: None, + world_restore_leftover_simulation_time_accumulator_raw_u32: None, + world_restore_leftover_simulation_time_accumulator_value_f32_text: None, + world_restore_selected_year_lane_snapshot_raw_u8: None, + world_restore_all_steam_locomotives_available_raw_u8: None, + world_restore_all_steam_locomotives_available_enabled: None, + world_restore_all_diesel_locomotives_available_raw_u8: None, + world_restore_all_diesel_locomotives_available_enabled: None, + world_restore_all_electric_locomotives_available_raw_u8: None, + world_restore_all_electric_locomotives_available_enabled: None, + world_restore_issue_37_value: None, + world_restore_issue_38_value: None, + world_restore_issue_39_value: None, + world_restore_issue_3a_value: None, + world_restore_issue_37_multiplier_raw_u32: None, + world_restore_issue_37_multiplier_value_f32_text: None, + world_restore_stock_issue_and_buyback_policy_raw_u8: None, + world_restore_bond_issue_and_repayment_policy_raw_u8: None, + world_restore_bankruptcy_policy_raw_u8: None, + world_restore_dividend_policy_raw_u8: None, + world_restore_building_density_growth_setting_raw_u32: None, + world_restore_stock_issue_and_buyback_allowed: None, + world_restore_bond_issue_and_repayment_allowed: None, + world_restore_bankruptcy_allowed: None, + world_restore_dividend_adjustment_allowed: None, + world_restore_finance_neighborhood_count: 0, + world_restore_finance_neighborhood_labels: Vec::new(), + world_restore_economic_tuning_mirror_raw_u32: None, + world_restore_economic_tuning_mirror_value_f32_text: None, + world_restore_economic_tuning_lane_count: 0, + world_restore_economic_tuning_lane_value_f32_text: Vec::new(), + world_restore_cached_available_locomotive_rating_raw_u32: None, + world_restore_cached_available_locomotive_rating_value_f32_text: None, + world_restore_selected_year_bucket_scalar_raw_u32: None, + world_restore_selected_year_bucket_scalar_value_f32_text: None, + world_restore_selected_year_bucket_direct_lane_count: 0, + world_restore_selected_year_bucket_direct_lane_value_f32_text: Vec::new(), + world_restore_selected_year_bucket_complement_lane_count: 0, + world_restore_selected_year_bucket_complement_lane_value_f32_text: Vec::new(), + world_restore_selected_year_bucket_scaled_companion_lane_count: 0, + world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text: Vec::new(), + world_restore_selected_year_gap_scalar_raw_u32: None, + world_restore_selected_year_gap_scalar_value_f32_text: None, + world_restore_absolute_counter_restore_kind: None, + world_restore_absolute_counter_adjustment_context: None, + metadata_count: 0, + company_count: 0, + active_company_count: 0, + company_market_state_owner_count: 0, + selected_company_outstanding_shares: None, + selected_company_bond_count: None, + selected_company_largest_live_bond_principal: None, + selected_company_highest_coupon_live_bond_principal: None, + selected_company_assigned_share_pool: None, + selected_company_unassigned_share_pool: None, + selected_company_cached_share_price: None, + selected_company_cached_share_price_value_f32_text: None, + selected_company_recent_per_share_cache_absolute_counter: None, + selected_company_recent_per_share_cached_value_f64_text: None, + selected_company_recent_per_share_subscore_value_f32_text: None, + selected_company_mutable_support_scalar_value_f32_text: None, + selected_company_stat_band_root_0cfb_count: 0, + selected_company_stat_band_root_0d7f_count: 0, + selected_company_stat_band_root_1c47_count: 0, + selected_company_last_dividend_year: None, + selected_company_years_since_founding: None, + selected_company_years_since_last_bankruptcy: None, + selected_company_years_since_last_dividend: None, + selected_company_current_partial_year_weight_numerator: None, + selected_company_current_issue_absolute_counter: None, + selected_company_prior_issue_absolute_counter: None, + selected_company_current_issue_age_absolute_counter_delta: None, + selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8: None, + selected_company_periodic_side_latch_city_connection_latch: None, + selected_company_periodic_side_latch_linked_transit_latch: None, + selected_company_linked_transit_route_anchor_entry_id: None, + selected_company_linked_transit_route_anchor_fallback_counts: Vec::new(), + selected_company_periodic_service_base_route_preference_raw_u8: None, + selected_company_periodic_service_effective_route_preference_raw_u8: None, + selected_company_periodic_service_electric_route_preference_override_active: None, + selected_company_periodic_service_route_quality_multiplier_basis_points: None, + active_periodic_route_preference_override_company_id: None, + active_periodic_route_preference_override_effective_raw_u8: None, + last_periodic_route_preference_override_company_id: None, + last_periodic_route_preference_override_effective_raw_u8: None, + selected_company_chairman_bonus_year: None, + selected_company_chairman_bonus_amount: None, + selected_company_creditor_pressure_recent_bad_net_profit_year_count: None, + selected_company_creditor_pressure_recent_peak_revenue: None, + selected_company_creditor_pressure_recent_three_year_net_profit_total: None, + selected_company_creditor_pressure_cash_floor: None, + selected_company_creditor_pressure_cash_plus_slot_12_total: None, + selected_company_creditor_pressure_share_price_floor: None, + selected_company_creditor_pressure_share_price_scalar: None, + selected_company_creditor_pressure_current_fuel_cost: None, + selected_company_creditor_pressure_current_fuel_cost_floor: None, + selected_company_creditor_pressure_eligible_for_bankruptcy_branch: None, + selected_company_deep_distress_current_cash: None, + selected_company_deep_distress_recent_first_three_net_profit_years: Vec::new(), + selected_company_deep_distress_cash_floor: None, + selected_company_deep_distress_net_profit_floor: None, + selected_company_deep_distress_eligible_for_bankruptcy_fallback: None, + selected_company_annual_bond_linked_transit_latch: None, + selected_company_annual_bond_live_bond_count: None, + selected_company_annual_bond_live_bond_principal_total: None, + selected_company_annual_bond_matured_live_bond_count: None, + selected_company_annual_bond_matured_live_bond_principal_total: None, + selected_company_annual_bond_next_live_bond_maturity_year: None, + selected_company_annual_bond_live_bond_coupon_burden_total: None, + selected_company_annual_bond_current_cash: None, + selected_company_annual_bond_cash_after_full_repayment: None, + selected_company_annual_bond_issue_cash_floor: None, + selected_company_annual_bond_issue_principal_step: None, + selected_company_annual_bond_proposed_issue_bond_count: None, + selected_company_annual_bond_proposed_issue_total_principal: None, + selected_company_annual_bond_proposed_issue_years_to_maturity: None, + selected_company_annual_bond_eligible_for_issue_branch: None, + selected_company_stock_repurchase_city_connection_latch: None, + selected_company_stock_repurchase_building_density_growth_setting: None, + selected_company_stock_repurchase_linked_chairman_profile_id: None, + selected_company_stock_repurchase_linked_chairman_personality_raw_u8: None, + selected_company_stock_repurchase_batch_size: None, + selected_company_stock_repurchase_factor_basis_points: None, + selected_company_stock_repurchase_current_cash: None, + selected_company_stock_repurchase_stock_value_gate_cash_floor: None, + selected_company_stock_repurchase_support_adjusted_share_price_scalar: None, + selected_company_stock_repurchase_affordability_cash_floor: None, + selected_company_stock_repurchase_unassigned_share_pool: None, + selected_company_stock_repurchase_eligible_for_single_batch: None, + selected_company_stock_issue_live_bond_count: None, + selected_company_stock_issue_initial_batch_size: None, + selected_company_stock_issue_trimmed_batch_size: None, + selected_company_stock_issue_share_pressure_basis_points: None, + selected_company_stock_issue_pressured_share_price_scalar: None, + selected_company_stock_issue_pressured_proceeds: None, + selected_company_stock_issue_book_value_per_share_floor_applied: None, + selected_company_stock_issue_price_to_book_ratio_basis_points: None, + selected_company_stock_issue_current_cash: None, + selected_company_stock_issue_highest_coupon_live_bond_principal: None, + selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points: None, + selected_company_stock_issue_current_issue_age_absolute_counter_delta: None, + selected_company_stock_issue_current_issue_cooldown_floor: None, + selected_company_stock_issue_minimum_price_to_book_ratio_basis_points: None, + selected_company_stock_issue_passes_share_price_floor: None, + selected_company_stock_issue_passes_proceeds_floor: None, + selected_company_stock_issue_passes_cash_gate: None, + selected_company_stock_issue_passes_issue_cooldown_gate: None, + selected_company_stock_issue_passes_coupon_price_to_book_gate: None, + selected_company_stock_issue_eligible_for_double_tranche: None, + selected_company_dividend_weighted_recent_net_profit_total: None, + selected_company_dividend_weighted_recent_net_profit_average: None, + selected_company_dividend_current_cash: None, + selected_company_dividend_tiny_unassigned_share_cash_supplement_branch: None, + selected_company_dividend_tentative_target_per_share_tenths: None, + selected_company_dividend_current_per_share_tenths: None, + selected_company_dividend_growth_adjusted_current_per_share_tenths: None, + selected_company_dividend_board_approved_ceiling_tenths: None, + selected_company_dividend_proposed_per_share_tenths: None, + selected_company_dividend_eligible_for_adjustment_branch: None, + selected_company_annual_finance_policy_action: None, + selected_company_annual_finance_news_family_candidate: None, + selected_company_annual_finance_last_news_selector: None, + annual_finance_last_news_event_count: 0, + selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible: None, + selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible: None, + selected_company_annual_finance_policy_bond_issue_eligible: None, + selected_company_annual_finance_policy_stock_repurchase_eligible: None, + selected_company_annual_finance_policy_stock_issue_eligible: None, + selected_company_annual_finance_policy_dividend_adjustment_eligible: None, + player_count: 0, + chairman_profile_count: 0, + active_chairman_profile_count: 0, + selected_chairman_profile_id: None, + linked_chairman_company_count: 0, + company_takeover_cooldown_count: 0, + company_merger_cooldown_count: 0, + train_count: 0, + active_train_count: 0, + retired_train_count: 0, + locomotive_catalog_count: 0, + cargo_catalog_count: 0, + territory_count: 0, + company_territory_track_count: 0, + packed_event_collection_present: false, + packed_event_record_count: 0, + packed_event_decoded_record_count: 0, + packed_event_imported_runtime_record_count: 0, + packed_event_parity_only_record_count: 0, + packed_event_unsupported_record_count: 0, + packed_event_blocked_missing_company_context_count: 0, + packed_event_blocked_missing_selection_context_count: 0, + packed_event_blocked_missing_company_role_context_count: 0, + packed_event_blocked_missing_player_context_count: 0, + packed_event_blocked_missing_player_selection_context_count: 0, + packed_event_blocked_missing_player_role_context_count: 0, + packed_event_blocked_missing_chairman_context_count: 0, + packed_event_blocked_chairman_target_scope_count: 0, + packed_event_blocked_missing_condition_context_count: 0, + packed_event_blocked_missing_player_condition_context_count: 0, + packed_event_blocked_company_condition_scope_disabled_count: 0, + packed_event_blocked_player_condition_scope_count: 0, + packed_event_blocked_territory_condition_scope_count: 0, + packed_event_blocked_missing_territory_context_count: 0, + packed_event_blocked_named_territory_binding_count: 0, + packed_event_blocked_unmapped_ordinary_condition_count: 0, + packed_event_blocked_unmapped_world_condition_count: 0, + packed_event_blocked_missing_compact_control_count: 0, + packed_event_blocked_shell_owned_descriptor_count: 0, + packed_event_blocked_evidence_blocked_descriptor_count: 0, + packed_event_blocked_variant_or_scope_blocked_descriptor_count: 0, + packed_event_blocked_unmapped_real_descriptor_count: 0, + packed_event_blocked_unmapped_world_descriptor_count: 0, + packed_event_blocked_territory_access_variant_count: 0, + packed_event_blocked_territory_access_scope_count: 0, + packed_event_blocked_missing_train_context_count: 0, + packed_event_blocked_missing_train_territory_context_count: 0, + packed_event_blocked_missing_locomotive_catalog_context_count: 0, + packed_event_blocked_confiscation_variant_count: 0, + packed_event_blocked_retire_train_variant_count: 0, + packed_event_blocked_retire_train_scope_count: 0, + packed_event_blocked_structural_only_count: 0, + event_runtime_record_count: 0, + candidate_availability_count: 0, + zero_candidate_availability_count: 0, + named_locomotive_availability_count: 0, + zero_named_locomotive_availability_count: 0, + named_locomotive_cost_count: 0, + cargo_production_override_count: 0, + world_runtime_variable_count: 0, + company_runtime_variable_owner_count: 0, + player_runtime_variable_owner_count: 0, + territory_runtime_variable_owner_count: 0, + world_scalar_override_count: 0, + special_condition_count: 0, + enabled_special_condition_count: 0, + save_profile_kind: None, + save_profile_family: None, + save_profile_map_path: None, + save_profile_display_name: None, + save_profile_selected_year_profile_lane: None, + save_profile_sandbox_enabled: None, + save_profile_campaign_scenario_enabled: None, + save_profile_staged_profile_copy_on_restore: None, + total_event_record_service_count: 0, + periodic_boundary_call_count: 0, + annual_finance_service_call_count: 0, + periodic_route_preference_override_apply_count: 0, + periodic_route_preference_override_restore_count: 0, + annual_dividend_adjustment_commit_count: 0, + annual_bond_last_retired_principal_total: 0, + annual_bond_last_issued_principal_total: 0, + annual_bond_last_principal_flow_relation: None, + annual_stock_repurchase_last_share_count: 0, + annual_stock_issue_last_share_count: 0, + total_trigger_dispatch_count: 0, + dirty_rerun_count: 0, + total_company_cash: 0, + } +} diff --git a/crates/rrt-runtime/src/summary/builders/collections.rs b/crates/rrt-runtime/src/summary/builders/collections.rs new file mode 100644 index 0000000..dd33baf --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/collections.rs @@ -0,0 +1,276 @@ +use crate::derived::runtime_annual_bond_principal_flow_relation_label; +use crate::state::RuntimeState; +use crate::summary::RuntimeSummary; + +pub(super) struct CollectionsSummaryFields { + pub player_count: usize, + pub chairman_profile_count: usize, + pub active_chairman_profile_count: usize, + pub selected_chairman_profile_id: Option, + pub linked_chairman_company_count: usize, + pub company_takeover_cooldown_count: usize, + pub company_merger_cooldown_count: usize, + pub train_count: usize, + pub active_train_count: usize, + pub retired_train_count: usize, + pub locomotive_catalog_count: usize, + pub cargo_catalog_count: usize, + pub territory_count: usize, + pub company_territory_track_count: usize, + pub event_runtime_record_count: usize, + pub candidate_availability_count: usize, + pub zero_candidate_availability_count: usize, + pub named_locomotive_availability_count: usize, + pub zero_named_locomotive_availability_count: usize, + pub named_locomotive_cost_count: usize, + pub cargo_production_override_count: usize, + pub world_runtime_variable_count: usize, + pub company_runtime_variable_owner_count: usize, + pub player_runtime_variable_owner_count: usize, + pub territory_runtime_variable_owner_count: usize, + pub world_scalar_override_count: usize, + pub special_condition_count: usize, + pub enabled_special_condition_count: usize, + pub save_profile_kind: Option, + pub save_profile_family: Option, + pub save_profile_map_path: Option, + pub save_profile_display_name: Option, + pub save_profile_selected_year_profile_lane: Option, + pub save_profile_sandbox_enabled: Option, + pub save_profile_campaign_scenario_enabled: Option, + pub save_profile_staged_profile_copy_on_restore: Option, + pub total_event_record_service_count: u64, + pub periodic_boundary_call_count: u64, + pub annual_finance_service_call_count: u64, + pub periodic_route_preference_override_apply_count: u64, + pub periodic_route_preference_override_restore_count: u64, + pub annual_dividend_adjustment_commit_count: u64, + pub annual_bond_last_retired_principal_total: u64, + pub annual_bond_last_issued_principal_total: u64, + pub annual_bond_last_principal_flow_relation: Option, + pub annual_stock_repurchase_last_share_count: u64, + pub annual_stock_issue_last_share_count: u64, + pub total_trigger_dispatch_count: u64, + pub dirty_rerun_count: u64, + pub total_company_cash: i64, +} + +pub(super) fn build_collections_summary_fields(state: &RuntimeState) -> CollectionsSummaryFields { + CollectionsSummaryFields { + player_count: state.players.len(), + chairman_profile_count: state.chairman_profiles.len(), + active_chairman_profile_count: state + .chairman_profiles + .iter() + .filter(|profile| profile.active) + .count(), + selected_chairman_profile_id: state.selected_chairman_profile_id, + linked_chairman_company_count: state + .companies + .iter() + .filter(|company| company.linked_chairman_profile_id.is_some()) + .count(), + company_takeover_cooldown_count: state + .companies + .iter() + .filter(|company| company.takeover_cooldown_year.is_some()) + .count(), + company_merger_cooldown_count: state + .companies + .iter() + .filter(|company| company.merger_cooldown_year.is_some()) + .count(), + train_count: state.trains.len(), + active_train_count: state.trains.iter().filter(|train| train.active).count(), + retired_train_count: state.trains.iter().filter(|train| train.retired).count(), + locomotive_catalog_count: state.locomotive_catalog.len(), + cargo_catalog_count: state.cargo_catalog.len(), + territory_count: state.territories.len(), + company_territory_track_count: state.company_territory_track_piece_counts.len(), + event_runtime_record_count: state.event_runtime_records.len(), + candidate_availability_count: state.candidate_availability.len(), + zero_candidate_availability_count: state + .candidate_availability + .values() + .filter(|value| **value == 0) + .count(), + named_locomotive_availability_count: state.named_locomotive_availability.len(), + zero_named_locomotive_availability_count: state + .named_locomotive_availability + .values() + .filter(|value| **value == 0) + .count(), + named_locomotive_cost_count: state.named_locomotive_cost.len(), + cargo_production_override_count: state.cargo_production_overrides.len(), + world_runtime_variable_count: state.world_runtime_variables.len(), + company_runtime_variable_owner_count: state.company_runtime_variables.len(), + player_runtime_variable_owner_count: state.player_runtime_variables.len(), + territory_runtime_variable_owner_count: state.territory_runtime_variables.len(), + world_scalar_override_count: state.world_scalar_overrides.len(), + special_condition_count: state.special_conditions.len(), + enabled_special_condition_count: state + .special_conditions + .values() + .filter(|value| **value != 0) + .count(), + save_profile_kind: state.save_profile.profile_kind.clone(), + save_profile_family: state.save_profile.profile_family.clone(), + save_profile_map_path: state.save_profile.map_path.clone(), + save_profile_display_name: state.save_profile.display_name.clone(), + save_profile_selected_year_profile_lane: state.save_profile.selected_year_profile_lane, + save_profile_sandbox_enabled: state.save_profile.sandbox_enabled, + save_profile_campaign_scenario_enabled: state.save_profile.campaign_scenario_enabled, + save_profile_staged_profile_copy_on_restore: state + .save_profile + .staged_profile_copy_on_restore, + total_event_record_service_count: state.service_state.total_event_record_services, + periodic_boundary_call_count: state.service_state.periodic_boundary_calls, + annual_finance_service_call_count: state.service_state.annual_finance_service_calls, + periodic_route_preference_override_apply_count: state + .service_state + .periodic_route_preference_override_apply_count, + periodic_route_preference_override_restore_count: state + .service_state + .periodic_route_preference_override_restore_count, + annual_dividend_adjustment_commit_count: state + .service_state + .annual_dividend_adjustment_commit_count, + annual_bond_last_retired_principal_total: state + .service_state + .annual_bond_last_retired_principal_total, + annual_bond_last_issued_principal_total: state + .service_state + .annual_bond_last_issued_principal_total, + annual_bond_last_principal_flow_relation: + runtime_annual_bond_principal_flow_relation_label( + state.service_state.annual_bond_last_retired_principal_total, + state.service_state.annual_bond_last_issued_principal_total, + ) + .map(str::to_string), + annual_stock_repurchase_last_share_count: state + .service_state + .annual_stock_repurchase_last_share_count, + annual_stock_issue_last_share_count: state + .service_state + .annual_stock_issue_last_share_count, + total_trigger_dispatch_count: state.service_state.trigger_dispatch_counts.values().sum(), + dirty_rerun_count: state.service_state.dirty_rerun_count, + total_company_cash: state + .companies + .iter() + .map(|company| company.current_cash) + .sum(), + } +} + +pub(super) fn apply_collections_summary_fields( + summary: &mut RuntimeSummary, + fields: CollectionsSummaryFields, +) { + let CollectionsSummaryFields { + player_count, + chairman_profile_count, + active_chairman_profile_count, + selected_chairman_profile_id, + linked_chairman_company_count, + company_takeover_cooldown_count, + company_merger_cooldown_count, + train_count, + active_train_count, + retired_train_count, + locomotive_catalog_count, + cargo_catalog_count, + territory_count, + company_territory_track_count, + event_runtime_record_count, + candidate_availability_count, + zero_candidate_availability_count, + named_locomotive_availability_count, + zero_named_locomotive_availability_count, + named_locomotive_cost_count, + cargo_production_override_count, + world_runtime_variable_count, + company_runtime_variable_owner_count, + player_runtime_variable_owner_count, + territory_runtime_variable_owner_count, + world_scalar_override_count, + special_condition_count, + enabled_special_condition_count, + save_profile_kind, + save_profile_family, + save_profile_map_path, + save_profile_display_name, + save_profile_selected_year_profile_lane, + save_profile_sandbox_enabled, + save_profile_campaign_scenario_enabled, + save_profile_staged_profile_copy_on_restore, + total_event_record_service_count, + periodic_boundary_call_count, + annual_finance_service_call_count, + periodic_route_preference_override_apply_count, + periodic_route_preference_override_restore_count, + annual_dividend_adjustment_commit_count, + annual_bond_last_retired_principal_total, + annual_bond_last_issued_principal_total, + annual_bond_last_principal_flow_relation, + annual_stock_repurchase_last_share_count, + annual_stock_issue_last_share_count, + total_trigger_dispatch_count, + dirty_rerun_count, + total_company_cash, + } = fields; + + summary.player_count = player_count; + summary.chairman_profile_count = chairman_profile_count; + summary.active_chairman_profile_count = active_chairman_profile_count; + summary.selected_chairman_profile_id = selected_chairman_profile_id; + summary.linked_chairman_company_count = linked_chairman_company_count; + summary.company_takeover_cooldown_count = company_takeover_cooldown_count; + summary.company_merger_cooldown_count = company_merger_cooldown_count; + summary.train_count = train_count; + summary.active_train_count = active_train_count; + summary.retired_train_count = retired_train_count; + summary.locomotive_catalog_count = locomotive_catalog_count; + summary.cargo_catalog_count = cargo_catalog_count; + summary.territory_count = territory_count; + summary.company_territory_track_count = company_territory_track_count; + summary.event_runtime_record_count = event_runtime_record_count; + summary.candidate_availability_count = candidate_availability_count; + summary.zero_candidate_availability_count = zero_candidate_availability_count; + summary.named_locomotive_availability_count = named_locomotive_availability_count; + summary.zero_named_locomotive_availability_count = zero_named_locomotive_availability_count; + summary.named_locomotive_cost_count = named_locomotive_cost_count; + summary.cargo_production_override_count = cargo_production_override_count; + summary.world_runtime_variable_count = world_runtime_variable_count; + summary.company_runtime_variable_owner_count = company_runtime_variable_owner_count; + summary.player_runtime_variable_owner_count = player_runtime_variable_owner_count; + summary.territory_runtime_variable_owner_count = territory_runtime_variable_owner_count; + summary.world_scalar_override_count = world_scalar_override_count; + summary.special_condition_count = special_condition_count; + summary.enabled_special_condition_count = enabled_special_condition_count; + summary.save_profile_kind = save_profile_kind; + summary.save_profile_family = save_profile_family; + summary.save_profile_map_path = save_profile_map_path; + summary.save_profile_display_name = save_profile_display_name; + summary.save_profile_selected_year_profile_lane = save_profile_selected_year_profile_lane; + summary.save_profile_sandbox_enabled = save_profile_sandbox_enabled; + summary.save_profile_campaign_scenario_enabled = save_profile_campaign_scenario_enabled; + summary.save_profile_staged_profile_copy_on_restore = + save_profile_staged_profile_copy_on_restore; + summary.total_event_record_service_count = total_event_record_service_count; + summary.periodic_boundary_call_count = periodic_boundary_call_count; + summary.annual_finance_service_call_count = annual_finance_service_call_count; + summary.periodic_route_preference_override_apply_count = + periodic_route_preference_override_apply_count; + summary.periodic_route_preference_override_restore_count = + periodic_route_preference_override_restore_count; + summary.annual_dividend_adjustment_commit_count = annual_dividend_adjustment_commit_count; + summary.annual_bond_last_retired_principal_total = annual_bond_last_retired_principal_total; + summary.annual_bond_last_issued_principal_total = annual_bond_last_issued_principal_total; + summary.annual_bond_last_principal_flow_relation = annual_bond_last_principal_flow_relation; + summary.annual_stock_repurchase_last_share_count = annual_stock_repurchase_last_share_count; + summary.annual_stock_issue_last_share_count = annual_stock_issue_last_share_count; + summary.total_trigger_dispatch_count = total_trigger_dispatch_count; + summary.dirty_rerun_count = dirty_rerun_count; + summary.total_company_cash = total_company_cash; +} diff --git a/crates/rrt-runtime/src/summary/builders/mod.rs b/crates/rrt-runtime/src/summary/builders/mod.rs new file mode 100644 index 0000000..a66a658 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/mod.rs @@ -0,0 +1,28 @@ +mod base; +mod collections; +mod packed_events; +mod selected_company; +mod world; + +use crate::state::RuntimeState; +use crate::summary::RuntimeSummary; + +use base::build_base_runtime_summary; +use collections::{apply_collections_summary_fields, build_collections_summary_fields}; +use packed_events::{apply_packed_event_summary_fields, build_packed_event_summary_fields}; +use selected_company::{ + apply_selected_company_summary_fields, build_selected_company_summary_fields, +}; +use world::{apply_world_summary_fields, build_world_summary_fields}; + +pub(in crate::summary) fn build_runtime_summary(state: &RuntimeState) -> RuntimeSummary { + let mut summary = build_base_runtime_summary(state.calendar); + apply_world_summary_fields(&mut summary, build_world_summary_fields(state)); + apply_selected_company_summary_fields( + &mut summary, + build_selected_company_summary_fields(state), + ); + apply_collections_summary_fields(&mut summary, build_collections_summary_fields(state)); + apply_packed_event_summary_fields(&mut summary, build_packed_event_summary_fields(state)); + summary +} diff --git a/crates/rrt-runtime/src/summary/builders/packed_events/apply.rs b/crates/rrt-runtime/src/summary/builders/packed_events/apply.rs new file mode 100644 index 0000000..3d057ad --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/packed_events/apply.rs @@ -0,0 +1,119 @@ +use crate::summary::RuntimeSummary; + +use super::PackedEventSummaryFields; + +pub(super) fn apply_packed_event_summary_fields( + summary: &mut RuntimeSummary, + fields: PackedEventSummaryFields, +) { + let PackedEventSummaryFields { + packed_event_collection_present, + packed_event_record_count, + packed_event_decoded_record_count, + packed_event_imported_runtime_record_count, + packed_event_parity_only_record_count, + packed_event_unsupported_record_count, + packed_event_blocked_missing_company_context_count, + packed_event_blocked_missing_selection_context_count, + packed_event_blocked_missing_company_role_context_count, + packed_event_blocked_missing_player_context_count, + packed_event_blocked_missing_player_selection_context_count, + packed_event_blocked_missing_player_role_context_count, + packed_event_blocked_missing_chairman_context_count, + packed_event_blocked_chairman_target_scope_count, + packed_event_blocked_missing_condition_context_count, + packed_event_blocked_missing_player_condition_context_count, + packed_event_blocked_company_condition_scope_disabled_count, + packed_event_blocked_player_condition_scope_count, + packed_event_blocked_territory_condition_scope_count, + packed_event_blocked_missing_territory_context_count, + packed_event_blocked_named_territory_binding_count, + packed_event_blocked_unmapped_ordinary_condition_count, + packed_event_blocked_unmapped_world_condition_count, + packed_event_blocked_missing_compact_control_count, + packed_event_blocked_shell_owned_descriptor_count, + packed_event_blocked_evidence_blocked_descriptor_count, + packed_event_blocked_variant_or_scope_blocked_descriptor_count, + packed_event_blocked_unmapped_real_descriptor_count, + packed_event_blocked_unmapped_world_descriptor_count, + packed_event_blocked_territory_access_variant_count, + packed_event_blocked_territory_access_scope_count, + packed_event_blocked_missing_train_context_count, + packed_event_blocked_missing_train_territory_context_count, + packed_event_blocked_missing_locomotive_catalog_context_count, + packed_event_blocked_confiscation_variant_count, + packed_event_blocked_retire_train_variant_count, + packed_event_blocked_retire_train_scope_count, + packed_event_blocked_structural_only_count, + } = fields; + + summary.packed_event_collection_present = packed_event_collection_present; + summary.packed_event_record_count = packed_event_record_count; + summary.packed_event_decoded_record_count = packed_event_decoded_record_count; + summary.packed_event_imported_runtime_record_count = packed_event_imported_runtime_record_count; + summary.packed_event_parity_only_record_count = packed_event_parity_only_record_count; + summary.packed_event_unsupported_record_count = packed_event_unsupported_record_count; + summary.packed_event_blocked_missing_company_context_count = + packed_event_blocked_missing_company_context_count; + summary.packed_event_blocked_missing_selection_context_count = + packed_event_blocked_missing_selection_context_count; + summary.packed_event_blocked_missing_company_role_context_count = + packed_event_blocked_missing_company_role_context_count; + summary.packed_event_blocked_missing_player_context_count = + packed_event_blocked_missing_player_context_count; + summary.packed_event_blocked_missing_player_selection_context_count = + packed_event_blocked_missing_player_selection_context_count; + summary.packed_event_blocked_missing_player_role_context_count = + packed_event_blocked_missing_player_role_context_count; + summary.packed_event_blocked_missing_chairman_context_count = + packed_event_blocked_missing_chairman_context_count; + summary.packed_event_blocked_chairman_target_scope_count = + packed_event_blocked_chairman_target_scope_count; + summary.packed_event_blocked_missing_condition_context_count = + packed_event_blocked_missing_condition_context_count; + summary.packed_event_blocked_missing_player_condition_context_count = + packed_event_blocked_missing_player_condition_context_count; + summary.packed_event_blocked_company_condition_scope_disabled_count = + packed_event_blocked_company_condition_scope_disabled_count; + summary.packed_event_blocked_player_condition_scope_count = + packed_event_blocked_player_condition_scope_count; + summary.packed_event_blocked_territory_condition_scope_count = + packed_event_blocked_territory_condition_scope_count; + summary.packed_event_blocked_missing_territory_context_count = + packed_event_blocked_missing_territory_context_count; + summary.packed_event_blocked_named_territory_binding_count = + packed_event_blocked_named_territory_binding_count; + summary.packed_event_blocked_unmapped_ordinary_condition_count = + packed_event_blocked_unmapped_ordinary_condition_count; + summary.packed_event_blocked_unmapped_world_condition_count = + packed_event_blocked_unmapped_world_condition_count; + summary.packed_event_blocked_missing_compact_control_count = + packed_event_blocked_missing_compact_control_count; + summary.packed_event_blocked_shell_owned_descriptor_count = + packed_event_blocked_shell_owned_descriptor_count; + summary.packed_event_blocked_evidence_blocked_descriptor_count = + packed_event_blocked_evidence_blocked_descriptor_count; + summary.packed_event_blocked_variant_or_scope_blocked_descriptor_count = + packed_event_blocked_variant_or_scope_blocked_descriptor_count; + summary.packed_event_blocked_unmapped_real_descriptor_count = + packed_event_blocked_unmapped_real_descriptor_count; + summary.packed_event_blocked_unmapped_world_descriptor_count = + packed_event_blocked_unmapped_world_descriptor_count; + summary.packed_event_blocked_territory_access_variant_count = + packed_event_blocked_territory_access_variant_count; + summary.packed_event_blocked_territory_access_scope_count = + packed_event_blocked_territory_access_scope_count; + summary.packed_event_blocked_missing_train_context_count = + packed_event_blocked_missing_train_context_count; + summary.packed_event_blocked_missing_train_territory_context_count = + packed_event_blocked_missing_train_territory_context_count; + summary.packed_event_blocked_missing_locomotive_catalog_context_count = + packed_event_blocked_missing_locomotive_catalog_context_count; + summary.packed_event_blocked_confiscation_variant_count = + packed_event_blocked_confiscation_variant_count; + summary.packed_event_blocked_retire_train_variant_count = + packed_event_blocked_retire_train_variant_count; + summary.packed_event_blocked_retire_train_scope_count = + packed_event_blocked_retire_train_scope_count; + summary.packed_event_blocked_structural_only_count = packed_event_blocked_structural_only_count; +} diff --git a/crates/rrt-runtime/src/summary/builders/packed_events/blocked_outcomes.rs b/crates/rrt-runtime/src/summary/builders/packed_events/blocked_outcomes.rs new file mode 100644 index 0000000..562f39a --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/packed_events/blocked_outcomes.rs @@ -0,0 +1,185 @@ +use crate::state::RuntimeState; + +pub(super) struct PackedEventBlockedOutcomeCounts { + pub packed_event_blocked_missing_company_context_count: usize, + pub packed_event_blocked_missing_selection_context_count: usize, + pub packed_event_blocked_missing_company_role_context_count: usize, + pub packed_event_blocked_missing_player_context_count: usize, + pub packed_event_blocked_missing_player_selection_context_count: usize, + pub packed_event_blocked_missing_player_role_context_count: usize, + pub packed_event_blocked_missing_chairman_context_count: usize, + pub packed_event_blocked_chairman_target_scope_count: usize, + pub packed_event_blocked_missing_condition_context_count: usize, + pub packed_event_blocked_missing_player_condition_context_count: usize, + pub packed_event_blocked_company_condition_scope_disabled_count: usize, + pub packed_event_blocked_player_condition_scope_count: usize, + pub packed_event_blocked_territory_condition_scope_count: usize, + pub packed_event_blocked_missing_territory_context_count: usize, + pub packed_event_blocked_named_territory_binding_count: usize, + pub packed_event_blocked_unmapped_ordinary_condition_count: usize, + pub packed_event_blocked_unmapped_world_condition_count: usize, + pub packed_event_blocked_missing_compact_control_count: usize, + pub packed_event_blocked_shell_owned_descriptor_count: usize, + pub packed_event_blocked_evidence_blocked_descriptor_count: usize, + pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: usize, + pub packed_event_blocked_unmapped_real_descriptor_count: usize, + pub packed_event_blocked_unmapped_world_descriptor_count: usize, + pub packed_event_blocked_territory_access_variant_count: usize, + pub packed_event_blocked_territory_access_scope_count: usize, + pub packed_event_blocked_missing_train_context_count: usize, + pub packed_event_blocked_missing_train_territory_context_count: usize, + pub packed_event_blocked_missing_locomotive_catalog_context_count: usize, + pub packed_event_blocked_confiscation_variant_count: usize, + pub packed_event_blocked_retire_train_variant_count: usize, + pub packed_event_blocked_retire_train_scope_count: usize, + pub packed_event_blocked_structural_only_count: usize, +} + +pub(super) fn build_packed_event_blocked_outcome_counts( + state: &RuntimeState, +) -> PackedEventBlockedOutcomeCounts { + PackedEventBlockedOutcomeCounts { + packed_event_blocked_missing_company_context_count: count_import_outcomes( + state, + "blocked_missing_company_context", + ), + packed_event_blocked_missing_selection_context_count: count_import_outcomes( + state, + "blocked_missing_selection_context", + ), + packed_event_blocked_missing_company_role_context_count: count_import_outcomes( + state, + "blocked_missing_company_role_context", + ), + packed_event_blocked_missing_player_context_count: count_import_outcomes( + state, + "blocked_missing_player_context", + ), + packed_event_blocked_missing_player_selection_context_count: count_import_outcomes( + state, + "blocked_missing_player_selection_context", + ), + packed_event_blocked_missing_player_role_context_count: count_import_outcomes( + state, + "blocked_missing_player_role_context", + ), + packed_event_blocked_missing_chairman_context_count: count_import_outcomes( + state, + "blocked_missing_chairman_context", + ), + packed_event_blocked_chairman_target_scope_count: count_import_outcomes( + state, + "blocked_chairman_target_scope", + ), + packed_event_blocked_missing_condition_context_count: count_import_outcomes( + state, + "blocked_missing_condition_context", + ), + packed_event_blocked_missing_player_condition_context_count: count_import_outcomes( + state, + "blocked_missing_player_condition_context", + ), + packed_event_blocked_company_condition_scope_disabled_count: count_import_outcomes( + state, + "blocked_company_condition_scope_disabled", + ), + packed_event_blocked_player_condition_scope_count: count_import_outcomes( + state, + "blocked_player_condition_scope", + ), + packed_event_blocked_territory_condition_scope_count: count_import_outcomes( + state, + "blocked_territory_condition_scope", + ), + packed_event_blocked_missing_territory_context_count: count_import_outcomes( + state, + "blocked_missing_territory_context", + ), + packed_event_blocked_named_territory_binding_count: count_import_outcomes( + state, + "blocked_named_territory_binding", + ), + packed_event_blocked_unmapped_ordinary_condition_count: count_import_outcomes( + state, + "blocked_unmapped_ordinary_condition", + ), + packed_event_blocked_unmapped_world_condition_count: count_import_outcomes( + state, + "blocked_unmapped_world_condition", + ), + packed_event_blocked_missing_compact_control_count: count_import_outcomes( + state, + "blocked_missing_compact_control", + ), + packed_event_blocked_shell_owned_descriptor_count: count_import_outcomes( + state, + "blocked_shell_owned_descriptor", + ), + packed_event_blocked_evidence_blocked_descriptor_count: count_import_outcomes( + state, + "blocked_evidence_blocked_descriptor", + ), + packed_event_blocked_variant_or_scope_blocked_descriptor_count: count_import_outcomes( + state, + "blocked_variant_or_scope_blocked_descriptor", + ), + packed_event_blocked_unmapped_real_descriptor_count: count_import_outcomes( + state, + "blocked_unmapped_real_descriptor", + ), + packed_event_blocked_unmapped_world_descriptor_count: count_import_outcomes( + state, + "blocked_unmapped_world_descriptor", + ), + packed_event_blocked_territory_access_variant_count: count_import_outcomes( + state, + "blocked_territory_access_variant", + ), + packed_event_blocked_territory_access_scope_count: count_import_outcomes( + state, + "blocked_territory_access_scope", + ), + packed_event_blocked_missing_train_context_count: count_import_outcomes( + state, + "blocked_missing_train_context", + ), + packed_event_blocked_missing_train_territory_context_count: count_import_outcomes( + state, + "blocked_missing_train_territory_context", + ), + packed_event_blocked_missing_locomotive_catalog_context_count: count_import_outcomes( + state, + "blocked_missing_locomotive_catalog_context", + ), + packed_event_blocked_confiscation_variant_count: count_import_outcomes( + state, + "blocked_confiscation_variant", + ), + packed_event_blocked_retire_train_variant_count: count_import_outcomes( + state, + "blocked_retire_train_variant", + ), + packed_event_blocked_retire_train_scope_count: count_import_outcomes( + state, + "blocked_retire_train_scope", + ), + packed_event_blocked_structural_only_count: count_import_outcomes( + state, + "blocked_structural_only", + ), + } +} + +fn count_import_outcomes(state: &RuntimeState, outcome: &str) -> usize { + state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| record.import_outcome.as_deref() == Some(outcome)) + .count() + }) + .unwrap_or(0) +} diff --git a/crates/rrt-runtime/src/summary/builders/packed_events/collection.rs b/crates/rrt-runtime/src/summary/builders/packed_events/collection.rs new file mode 100644 index 0000000..f7d8436 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/packed_events/collection.rs @@ -0,0 +1,55 @@ +use crate::state::RuntimeState; + +pub(super) struct PackedEventCollectionCounts { + pub packed_event_collection_present: bool, + pub packed_event_record_count: usize, + pub packed_event_decoded_record_count: usize, + pub packed_event_imported_runtime_record_count: usize, + pub packed_event_parity_only_record_count: usize, + pub packed_event_unsupported_record_count: usize, +} + +pub(super) fn build_packed_event_collection_counts( + state: &RuntimeState, +) -> PackedEventCollectionCounts { + PackedEventCollectionCounts { + packed_event_collection_present: state.packed_event_collection.is_some(), + packed_event_record_count: state + .packed_event_collection + .as_ref() + .map(|summary| summary.live_record_count) + .unwrap_or(0), + packed_event_decoded_record_count: state + .packed_event_collection + .as_ref() + .map(|summary| summary.decoded_record_count) + .unwrap_or(0), + packed_event_imported_runtime_record_count: state + .packed_event_collection + .as_ref() + .map(|summary| summary.imported_runtime_record_count) + .unwrap_or(0), + packed_event_parity_only_record_count: state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| record.decode_status == "parity_only") + .count() + }) + .unwrap_or(0), + packed_event_unsupported_record_count: state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| record.decode_status == "unsupported_framing") + .count() + }) + .unwrap_or(0), + } +} diff --git a/crates/rrt-runtime/src/summary/builders/packed_events/mod.rs b/crates/rrt-runtime/src/summary/builders/packed_events/mod.rs new file mode 100644 index 0000000..73ecc3c --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/packed_events/mod.rs @@ -0,0 +1,132 @@ +mod apply; +mod blocked_outcomes; +mod collection; + +use crate::state::RuntimeState; +use crate::summary::RuntimeSummary; + +pub(super) struct PackedEventSummaryFields { + pub packed_event_collection_present: bool, + pub packed_event_record_count: usize, + pub packed_event_decoded_record_count: usize, + pub packed_event_imported_runtime_record_count: usize, + pub packed_event_parity_only_record_count: usize, + pub packed_event_unsupported_record_count: usize, + pub packed_event_blocked_missing_company_context_count: usize, + pub packed_event_blocked_missing_selection_context_count: usize, + pub packed_event_blocked_missing_company_role_context_count: usize, + pub packed_event_blocked_missing_player_context_count: usize, + pub packed_event_blocked_missing_player_selection_context_count: usize, + pub packed_event_blocked_missing_player_role_context_count: usize, + pub packed_event_blocked_missing_chairman_context_count: usize, + pub packed_event_blocked_chairman_target_scope_count: usize, + pub packed_event_blocked_missing_condition_context_count: usize, + pub packed_event_blocked_missing_player_condition_context_count: usize, + pub packed_event_blocked_company_condition_scope_disabled_count: usize, + pub packed_event_blocked_player_condition_scope_count: usize, + pub packed_event_blocked_territory_condition_scope_count: usize, + pub packed_event_blocked_missing_territory_context_count: usize, + pub packed_event_blocked_named_territory_binding_count: usize, + pub packed_event_blocked_unmapped_ordinary_condition_count: usize, + pub packed_event_blocked_unmapped_world_condition_count: usize, + pub packed_event_blocked_missing_compact_control_count: usize, + pub packed_event_blocked_shell_owned_descriptor_count: usize, + pub packed_event_blocked_evidence_blocked_descriptor_count: usize, + pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: usize, + pub packed_event_blocked_unmapped_real_descriptor_count: usize, + pub packed_event_blocked_unmapped_world_descriptor_count: usize, + pub packed_event_blocked_territory_access_variant_count: usize, + pub packed_event_blocked_territory_access_scope_count: usize, + pub packed_event_blocked_missing_train_context_count: usize, + pub packed_event_blocked_missing_train_territory_context_count: usize, + pub packed_event_blocked_missing_locomotive_catalog_context_count: usize, + pub packed_event_blocked_confiscation_variant_count: usize, + pub packed_event_blocked_retire_train_variant_count: usize, + pub packed_event_blocked_retire_train_scope_count: usize, + pub packed_event_blocked_structural_only_count: usize, +} + +pub(super) fn build_packed_event_summary_fields(state: &RuntimeState) -> PackedEventSummaryFields { + let collection = collection::build_packed_event_collection_counts(state); + let blocked = blocked_outcomes::build_packed_event_blocked_outcome_counts(state); + PackedEventSummaryFields { + packed_event_collection_present: collection.packed_event_collection_present, + packed_event_record_count: collection.packed_event_record_count, + packed_event_decoded_record_count: collection.packed_event_decoded_record_count, + packed_event_imported_runtime_record_count: collection + .packed_event_imported_runtime_record_count, + packed_event_parity_only_record_count: collection.packed_event_parity_only_record_count, + packed_event_unsupported_record_count: collection.packed_event_unsupported_record_count, + packed_event_blocked_missing_company_context_count: blocked + .packed_event_blocked_missing_company_context_count, + packed_event_blocked_missing_selection_context_count: blocked + .packed_event_blocked_missing_selection_context_count, + packed_event_blocked_missing_company_role_context_count: blocked + .packed_event_blocked_missing_company_role_context_count, + packed_event_blocked_missing_player_context_count: blocked + .packed_event_blocked_missing_player_context_count, + packed_event_blocked_missing_player_selection_context_count: blocked + .packed_event_blocked_missing_player_selection_context_count, + packed_event_blocked_missing_player_role_context_count: blocked + .packed_event_blocked_missing_player_role_context_count, + packed_event_blocked_missing_chairman_context_count: blocked + .packed_event_blocked_missing_chairman_context_count, + packed_event_blocked_chairman_target_scope_count: blocked + .packed_event_blocked_chairman_target_scope_count, + packed_event_blocked_missing_condition_context_count: blocked + .packed_event_blocked_missing_condition_context_count, + packed_event_blocked_missing_player_condition_context_count: blocked + .packed_event_blocked_missing_player_condition_context_count, + packed_event_blocked_company_condition_scope_disabled_count: blocked + .packed_event_blocked_company_condition_scope_disabled_count, + packed_event_blocked_player_condition_scope_count: blocked + .packed_event_blocked_player_condition_scope_count, + packed_event_blocked_territory_condition_scope_count: blocked + .packed_event_blocked_territory_condition_scope_count, + packed_event_blocked_missing_territory_context_count: blocked + .packed_event_blocked_missing_territory_context_count, + packed_event_blocked_named_territory_binding_count: blocked + .packed_event_blocked_named_territory_binding_count, + packed_event_blocked_unmapped_ordinary_condition_count: blocked + .packed_event_blocked_unmapped_ordinary_condition_count, + packed_event_blocked_unmapped_world_condition_count: blocked + .packed_event_blocked_unmapped_world_condition_count, + packed_event_blocked_missing_compact_control_count: blocked + .packed_event_blocked_missing_compact_control_count, + packed_event_blocked_shell_owned_descriptor_count: blocked + .packed_event_blocked_shell_owned_descriptor_count, + packed_event_blocked_evidence_blocked_descriptor_count: blocked + .packed_event_blocked_evidence_blocked_descriptor_count, + packed_event_blocked_variant_or_scope_blocked_descriptor_count: blocked + .packed_event_blocked_variant_or_scope_blocked_descriptor_count, + packed_event_blocked_unmapped_real_descriptor_count: blocked + .packed_event_blocked_unmapped_real_descriptor_count, + packed_event_blocked_unmapped_world_descriptor_count: blocked + .packed_event_blocked_unmapped_world_descriptor_count, + packed_event_blocked_territory_access_variant_count: blocked + .packed_event_blocked_territory_access_variant_count, + packed_event_blocked_territory_access_scope_count: blocked + .packed_event_blocked_territory_access_scope_count, + packed_event_blocked_missing_train_context_count: blocked + .packed_event_blocked_missing_train_context_count, + packed_event_blocked_missing_train_territory_context_count: blocked + .packed_event_blocked_missing_train_territory_context_count, + packed_event_blocked_missing_locomotive_catalog_context_count: blocked + .packed_event_blocked_missing_locomotive_catalog_context_count, + packed_event_blocked_confiscation_variant_count: blocked + .packed_event_blocked_confiscation_variant_count, + packed_event_blocked_retire_train_variant_count: blocked + .packed_event_blocked_retire_train_variant_count, + packed_event_blocked_retire_train_scope_count: blocked + .packed_event_blocked_retire_train_scope_count, + packed_event_blocked_structural_only_count: blocked + .packed_event_blocked_structural_only_count, + } +} + +pub(super) fn apply_packed_event_summary_fields( + summary: &mut RuntimeSummary, + fields: PackedEventSummaryFields, +) { + apply::apply_packed_event_summary_fields(summary, fields); +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/annual_finance.rs b/crates/rrt-runtime/src/summary/builders/selected_company/annual_finance.rs new file mode 100644 index 0000000..811cdeb --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/annual_finance.rs @@ -0,0 +1,68 @@ +use super::inputs::SelectedCompanyInputs; +use super::model::SelectedCompanySummaryFields; + +use crate::derived::runtime_company_annual_finance_policy_action_label; + +pub(super) fn fill_selected_company_annual_finance_fields( + fields: &mut SelectedCompanySummaryFields, + inputs: &SelectedCompanyInputs<'_>, +) { + fields.selected_company_annual_finance_policy_action = inputs + .annual_finance_policy_state + .as_ref() + .map(|policy_state| { + runtime_company_annual_finance_policy_action_label(policy_state.action).to_string() + }); + fields.selected_company_annual_finance_news_family_candidate = inputs + .selected_company_id + .and_then(|company_id| { + inputs + .state + .service_state + .annual_finance_last_news_family_candidates + .get(&company_id) + }) + .cloned(); + fields.selected_company_annual_finance_last_news_selector = inputs + .selected_company_id + .and_then(|company_id| { + inputs + .state + .service_state + .annual_finance_last_news_events + .iter() + .rev() + .find(|news| news.company_id == company_id) + }) + .map(|news| news.selector_label.clone()); + fields.annual_finance_last_news_event_count = inputs + .state + .service_state + .annual_finance_last_news_events + .len(); + fields.selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible = inputs + .annual_finance_policy_state + .as_ref() + .map(|policy_state| policy_state.creditor_pressure_bankruptcy_eligible); + fields.selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible = + inputs + .annual_finance_policy_state + .as_ref() + .map(|policy_state| policy_state.deep_distress_bankruptcy_fallback_eligible); + fields.selected_company_annual_finance_policy_bond_issue_eligible = inputs + .annual_finance_policy_state + .as_ref() + .map(|policy_state| policy_state.bond_issue_eligible); + fields.selected_company_annual_finance_policy_stock_repurchase_eligible = inputs + .annual_finance_policy_state + .as_ref() + .map(|policy_state| policy_state.stock_repurchase_eligible); + fields.selected_company_annual_finance_policy_stock_issue_eligible = inputs + .annual_finance_policy_state + .as_ref() + .map(|policy_state| policy_state.stock_issue_eligible); + fields.selected_company_annual_finance_policy_dividend_adjustment_eligible = inputs + .annual_finance_policy_state + .as_ref() + .map(|policy_state| policy_state.dividend_adjustment_eligible); +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/bond_policy.rs b/crates/rrt-runtime/src/summary/builders/selected_company/bond_policy.rs new file mode 100644 index 0000000..815324c --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/bond_policy.rs @@ -0,0 +1,68 @@ +use super::inputs::SelectedCompanyInputs; +use super::model::SelectedCompanySummaryFields; + +pub(super) fn fill_selected_company_bond_policy_fields( + fields: &mut SelectedCompanySummaryFields, + inputs: &SelectedCompanyInputs<'_>, +) { + fields.selected_company_annual_bond_linked_transit_latch = inputs + .annual_bond_state + .as_ref() + .map(|bond_state| bond_state.linked_transit_latch); + fields.selected_company_annual_bond_live_bond_count = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.live_bond_count); + fields.selected_company_annual_bond_live_bond_principal_total = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.live_bond_principal_total); + fields.selected_company_annual_bond_matured_live_bond_count = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.matured_live_bond_count); + fields.selected_company_annual_bond_matured_live_bond_principal_total = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.matured_live_bond_principal_total); + fields.selected_company_annual_bond_next_live_bond_maturity_year = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.next_live_bond_maturity_year); + fields.selected_company_annual_bond_live_bond_coupon_burden_total = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.live_bond_coupon_burden_total); + fields.selected_company_annual_bond_current_cash = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.current_cash); + fields.selected_company_annual_bond_cash_after_full_repayment = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.cash_after_full_repayment); + fields.selected_company_annual_bond_issue_cash_floor = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.issue_cash_floor); + fields.selected_company_annual_bond_issue_principal_step = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.issue_principal_step); + fields.selected_company_annual_bond_proposed_issue_bond_count = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.proposed_issue_bond_count); + fields.selected_company_annual_bond_proposed_issue_total_principal = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.proposed_issue_total_principal); + fields.selected_company_annual_bond_proposed_issue_years_to_maturity = inputs + .annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.proposed_issue_years_to_maturity); + fields.selected_company_annual_bond_eligible_for_issue_branch = inputs + .annual_bond_state + .as_ref() + .map(|bond_state| bond_state.eligible_for_bond_issue_branch); +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/distress.rs b/crates/rrt-runtime/src/summary/builders/selected_company/distress.rs new file mode 100644 index 0000000..6826253 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/distress.rs @@ -0,0 +1,69 @@ +use super::inputs::SelectedCompanyInputs; +use super::model::SelectedCompanySummaryFields; + +pub(super) fn fill_selected_company_distress_fields( + fields: &mut SelectedCompanySummaryFields, + inputs: &SelectedCompanyInputs<'_>, +) { + fields.selected_company_creditor_pressure_recent_bad_net_profit_year_count = inputs + .creditor_pressure_state + .as_ref() + .map(|pressure_state| pressure_state.recent_bad_net_profit_year_count); + fields.selected_company_creditor_pressure_recent_peak_revenue = inputs + .creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.recent_peak_revenue); + fields.selected_company_creditor_pressure_recent_three_year_net_profit_total = inputs + .creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.recent_three_year_net_profit_total); + fields.selected_company_creditor_pressure_cash_floor = inputs + .creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.pressure_ladder_cash_floor); + fields.selected_company_creditor_pressure_cash_plus_slot_12_total = inputs + .creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.current_cash_plus_slot_12_total); + fields.selected_company_creditor_pressure_share_price_floor = inputs + .creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.support_adjusted_share_price_floor); + fields.selected_company_creditor_pressure_share_price_scalar = inputs + .creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.support_adjusted_share_price_scalar); + fields.selected_company_creditor_pressure_current_fuel_cost = inputs + .creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.current_fuel_cost); + fields.selected_company_creditor_pressure_current_fuel_cost_floor = inputs + .creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.current_fuel_cost_floor); + fields.selected_company_creditor_pressure_eligible_for_bankruptcy_branch = inputs + .creditor_pressure_state + .as_ref() + .map(|pressure_state| pressure_state.eligible_for_bankruptcy_branch); + fields.selected_company_deep_distress_current_cash = inputs + .deep_distress_state + .as_ref() + .and_then(|pressure_state| pressure_state.current_cash); + fields.selected_company_deep_distress_recent_first_three_net_profit_years = inputs + .deep_distress_state + .as_ref() + .map(|pressure_state| pressure_state.recent_first_three_net_profit_years.clone()) + .unwrap_or_default(); + fields.selected_company_deep_distress_cash_floor = inputs + .deep_distress_state + .as_ref() + .and_then(|pressure_state| pressure_state.deep_distress_cash_floor); + fields.selected_company_deep_distress_net_profit_floor = inputs + .deep_distress_state + .as_ref() + .and_then(|pressure_state| pressure_state.deep_distress_net_profit_floor); + fields.selected_company_deep_distress_eligible_for_bankruptcy_fallback = inputs + .deep_distress_state + .as_ref() + .map(|pressure_state| pressure_state.eligible_for_bankruptcy_fallback); +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/dividend_policy.rs b/crates/rrt-runtime/src/summary/builders/selected_company/dividend_policy.rs new file mode 100644 index 0000000..ca00ceb --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/dividend_policy.rs @@ -0,0 +1,48 @@ +use super::inputs::SelectedCompanyInputs; +use super::model::SelectedCompanySummaryFields; + +pub(super) fn fill_selected_company_dividend_policy_fields( + fields: &mut SelectedCompanySummaryFields, + inputs: &SelectedCompanyInputs<'_>, +) { + fields.selected_company_dividend_weighted_recent_net_profit_total = inputs + .dividend_state + .as_ref() + .and_then(|dividend_state| dividend_state.weighted_recent_net_profit_total); + fields.selected_company_dividend_weighted_recent_net_profit_average = inputs + .dividend_state + .as_ref() + .and_then(|dividend_state| dividend_state.weighted_recent_net_profit_average); + fields.selected_company_dividend_current_cash = inputs + .dividend_state + .as_ref() + .and_then(|dividend_state| dividend_state.current_cash); + fields.selected_company_dividend_tiny_unassigned_share_cash_supplement_branch = inputs + .dividend_state + .as_ref() + .map(|dividend_state| dividend_state.tiny_unassigned_share_cash_supplement_branch); + fields.selected_company_dividend_tentative_target_per_share_tenths = inputs + .dividend_state + .as_ref() + .and_then(|dividend_state| dividend_state.tentative_target_dividend_per_share_tenths); + fields.selected_company_dividend_current_per_share_tenths = inputs + .dividend_state + .as_ref() + .and_then(|dividend_state| dividend_state.current_dividend_per_share_tenths); + fields.selected_company_dividend_growth_adjusted_current_per_share_tenths = + inputs.dividend_state.as_ref().and_then(|dividend_state| { + dividend_state.growth_adjusted_current_dividend_per_share_tenths + }); + fields.selected_company_dividend_board_approved_ceiling_tenths = inputs + .dividend_state + .as_ref() + .and_then(|dividend_state| dividend_state.board_approved_dividend_rate_ceiling_tenths); + fields.selected_company_dividend_proposed_per_share_tenths = inputs + .dividend_state + .as_ref() + .and_then(|dividend_state| dividend_state.proposed_dividend_per_share_tenths); + fields.selected_company_dividend_eligible_for_adjustment_branch = inputs + .dividend_state + .as_ref() + .map(|dividend_state| dividend_state.eligible_for_dividend_adjustment_branch); +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/equity_actions.rs b/crates/rrt-runtime/src/summary/builders/selected_company/equity_actions.rs new file mode 100644 index 0000000..701fd3e --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/equity_actions.rs @@ -0,0 +1,136 @@ +use super::inputs::SelectedCompanyInputs; +use super::model::SelectedCompanySummaryFields; + +pub(super) fn fill_selected_company_equity_action_fields( + fields: &mut SelectedCompanySummaryFields, + inputs: &SelectedCompanyInputs<'_>, +) { + fields.selected_company_stock_repurchase_city_connection_latch = inputs + .stock_repurchase_state + .as_ref() + .map(|repurchase_state| repurchase_state.city_connection_latch); + fields.selected_company_stock_repurchase_building_density_growth_setting = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.building_density_growth_setting); + fields.selected_company_stock_repurchase_linked_chairman_profile_id = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.linked_chairman_profile_id); + fields.selected_company_stock_repurchase_linked_chairman_personality_raw_u8 = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.linked_chairman_personality_raw_u8); + fields.selected_company_stock_repurchase_batch_size = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.repurchase_batch_size); + fields.selected_company_stock_repurchase_factor_basis_points = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.repurchase_factor_basis_points); + fields.selected_company_stock_repurchase_current_cash = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.current_cash); + fields.selected_company_stock_repurchase_stock_value_gate_cash_floor = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.stock_value_gate_cash_floor); + fields.selected_company_stock_repurchase_support_adjusted_share_price_scalar = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.support_adjusted_share_price_scalar); + fields.selected_company_stock_repurchase_affordability_cash_floor = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.affordability_cash_floor); + fields.selected_company_stock_repurchase_unassigned_share_pool = inputs + .stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.unassigned_share_pool); + fields.selected_company_stock_repurchase_eligible_for_single_batch = inputs + .stock_repurchase_state + .as_ref() + .map(|repurchase_state| repurchase_state.eligible_for_single_batch_repurchase); + fields.selected_company_stock_issue_live_bond_count = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.live_bond_count); + fields.selected_company_stock_issue_initial_batch_size = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.initial_issue_batch_size); + fields.selected_company_stock_issue_trimmed_batch_size = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.trimmed_issue_batch_size); + fields.selected_company_stock_issue_share_pressure_basis_points = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.share_pressure_basis_points); + fields.selected_company_stock_issue_pressured_share_price_scalar = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.pressured_support_adjusted_share_price_scalar); + fields.selected_company_stock_issue_pressured_proceeds = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.pressured_proceeds); + fields.selected_company_stock_issue_book_value_per_share_floor_applied = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.book_value_per_share_floor_applied); + fields.selected_company_stock_issue_price_to_book_ratio_basis_points = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.price_to_book_ratio_basis_points); + fields.selected_company_stock_issue_current_cash = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.current_cash); + fields.selected_company_stock_issue_highest_coupon_live_bond_principal = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.highest_coupon_live_bond_principal); + fields.selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.highest_coupon_live_bond_rate_basis_points); + fields.selected_company_stock_issue_current_issue_age_absolute_counter_delta = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.current_issue_age_absolute_counter_delta); + fields.selected_company_stock_issue_current_issue_cooldown_floor = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.current_issue_cooldown_floor); + fields.selected_company_stock_issue_minimum_price_to_book_ratio_basis_points = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.minimum_price_to_book_ratio_basis_points); + fields.selected_company_stock_issue_passes_share_price_floor = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_share_price_floor); + fields.selected_company_stock_issue_passes_proceeds_floor = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_proceeds_floor); + fields.selected_company_stock_issue_passes_cash_gate = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_cash_gate); + fields.selected_company_stock_issue_passes_issue_cooldown_gate = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_issue_cooldown_gate); + fields.selected_company_stock_issue_passes_coupon_price_to_book_gate = inputs + .stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_coupon_price_to_book_gate); + fields.selected_company_stock_issue_eligible_for_double_tranche = inputs + .stock_issue_state + .as_ref() + .map(|issue_state| issue_state.eligible_for_double_tranche_issue); +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/inputs.rs b/crates/rrt-runtime/src/summary/builders/selected_company/inputs.rs new file mode 100644 index 0000000..964009b --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/inputs.rs @@ -0,0 +1,73 @@ +use crate::RuntimeState; +use crate::derived::{ + runtime_company_annual_bond_policy_state, runtime_company_annual_creditor_pressure_state, + runtime_company_annual_deep_distress_state, runtime_company_annual_dividend_policy_state, + runtime_company_annual_finance_policy_state, runtime_company_annual_finance_state, + runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state, + runtime_company_periodic_service_state, +}; +use crate::state::{ + RuntimeCompanyAnnualBondPolicyState, RuntimeCompanyAnnualCreditorPressureState, + RuntimeCompanyAnnualDeepDistressState, RuntimeCompanyAnnualDividendPolicyState, + RuntimeCompanyAnnualFinancePolicyState, RuntimeCompanyAnnualFinanceState, + RuntimeCompanyAnnualStockIssueState, RuntimeCompanyAnnualStockRepurchaseState, + RuntimeCompanyMarketState, RuntimeCompanyPeriodicServiceState, + RuntimeCompanyPeriodicSideLatchState, +}; + +pub(super) struct SelectedCompanyInputs<'a> { + pub(super) state: &'a RuntimeState, + pub(super) selected_company_id: Option, + pub(super) market_state: Option<&'a RuntimeCompanyMarketState>, + pub(super) periodic_side_latch_state: Option<&'a RuntimeCompanyPeriodicSideLatchState>, + pub(super) periodic_service_state: Option, + pub(super) annual_finance_state: Option, + pub(super) creditor_pressure_state: Option, + pub(super) deep_distress_state: Option, + pub(super) annual_bond_state: Option, + pub(super) stock_repurchase_state: Option, + pub(super) stock_issue_state: Option, + pub(super) dividend_state: Option, + pub(super) annual_finance_policy_state: Option, +} + +impl<'a> SelectedCompanyInputs<'a> { + pub(super) fn from_state(state: &'a RuntimeState) -> Self { + let selected_company_id = state.selected_company_id; + Self { + state, + selected_company_id, + market_state: selected_company_id + .and_then(|company_id| state.service_state.company_market_state.get(&company_id)), + periodic_side_latch_state: selected_company_id.and_then(|company_id| { + state + .service_state + .company_periodic_side_latch_state + .get(&company_id) + }), + periodic_service_state: selected_company_id + .and_then(|company_id| runtime_company_periodic_service_state(state, company_id)), + annual_finance_state: selected_company_id + .and_then(|company_id| runtime_company_annual_finance_state(state, company_id)), + creditor_pressure_state: selected_company_id.and_then(|company_id| { + runtime_company_annual_creditor_pressure_state(state, company_id) + }), + deep_distress_state: selected_company_id.and_then(|company_id| { + runtime_company_annual_deep_distress_state(state, company_id) + }), + annual_bond_state: selected_company_id + .and_then(|company_id| runtime_company_annual_bond_policy_state(state, company_id)), + stock_repurchase_state: selected_company_id.and_then(|company_id| { + runtime_company_annual_stock_repurchase_state(state, company_id) + }), + stock_issue_state: selected_company_id + .and_then(|company_id| runtime_company_annual_stock_issue_state(state, company_id)), + dividend_state: selected_company_id.and_then(|company_id| { + runtime_company_annual_dividend_policy_state(state, company_id) + }), + annual_finance_policy_state: selected_company_id.and_then(|company_id| { + runtime_company_annual_finance_policy_state(state, company_id) + }), + } + } +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/market.rs b/crates/rrt-runtime/src/summary/builders/selected_company/market.rs new file mode 100644 index 0000000..51a0787 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/market.rs @@ -0,0 +1,101 @@ +use super::inputs::SelectedCompanyInputs; +use super::model::SelectedCompanySummaryFields; + +use crate::derived::runtime_company_unassigned_share_pool; +use crate::summary::{raw_u32_to_f32_text, raw_u64_to_f64_text}; + +pub(super) fn fill_selected_company_market_fields( + fields: &mut SelectedCompanySummaryFields, + inputs: &SelectedCompanyInputs<'_>, +) { + fields.selected_company_outstanding_shares = inputs + .market_state + .map(|market_state| market_state.outstanding_shares); + fields.selected_company_bond_count = inputs + .market_state + .map(|market_state| market_state.bond_count); + fields.selected_company_largest_live_bond_principal = inputs + .market_state + .and_then(|market_state| market_state.largest_live_bond_principal); + fields.selected_company_highest_coupon_live_bond_principal = inputs + .market_state + .and_then(|market_state| market_state.highest_coupon_live_bond_principal); + fields.selected_company_assigned_share_pool = inputs + .annual_finance_state + .as_ref() + .map(|finance_state| finance_state.assigned_share_pool); + fields.selected_company_unassigned_share_pool = inputs + .selected_company_id + .and_then(|company_id| runtime_company_unassigned_share_pool(inputs.state, company_id)); + fields.selected_company_cached_share_price = inputs + .annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.cached_share_price); + fields.selected_company_cached_share_price_value_f32_text = inputs + .market_state + .map(|market_state| raw_u32_to_f32_text(market_state.cached_share_price_raw_u32)); + fields.selected_company_recent_per_share_cache_absolute_counter = inputs + .market_state + .map(|market_state| market_state.recent_per_share_cache_absolute_counter); + fields.selected_company_recent_per_share_cached_value_f64_text = inputs + .market_state + .map(|market_state| raw_u64_to_f64_text(market_state.recent_per_share_cached_value_bits)); + fields.selected_company_recent_per_share_subscore_value_f32_text = inputs + .market_state + .map(|market_state| raw_u32_to_f32_text(market_state.recent_per_share_subscore_raw_u32)); + fields.selected_company_mutable_support_scalar_value_f32_text = inputs + .market_state + .map(|market_state| raw_u32_to_f32_text(market_state.mutable_support_scalar_raw_u32)); + fields.selected_company_stat_band_root_0cfb_count = inputs + .market_state + .map(|market_state| market_state.stat_band_root_0cfb_candidates.len()) + .unwrap_or(0); + fields.selected_company_stat_band_root_0d7f_count = inputs + .market_state + .map(|market_state| market_state.stat_band_root_0d7f_candidates.len()) + .unwrap_or(0); + fields.selected_company_stat_band_root_1c47_count = inputs + .market_state + .map(|market_state| market_state.stat_band_root_1c47_candidates.len()) + .unwrap_or(0); + fields.selected_company_last_dividend_year = inputs + .market_state + .map(|market_state| market_state.last_dividend_year) + .filter(|year| *year != 0); + fields.selected_company_years_since_founding = inputs + .annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.years_since_founding); + fields.selected_company_years_since_last_bankruptcy = inputs + .annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.years_since_last_bankruptcy); + fields.selected_company_years_since_last_dividend = inputs + .annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.years_since_last_dividend); + fields.selected_company_current_partial_year_weight_numerator = inputs + .annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.current_partial_year_weight_numerator); + fields.selected_company_current_issue_absolute_counter = inputs + .annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.current_issue_absolute_counter); + fields.selected_company_prior_issue_absolute_counter = inputs + .annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.prior_issue_absolute_counter); + fields.selected_company_current_issue_age_absolute_counter_delta = inputs + .annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.current_issue_age_absolute_counter_delta); + fields.selected_company_chairman_bonus_year = inputs + .market_state + .map(|market_state| market_state.chairman_bonus_year) + .filter(|year| *year != 0); + fields.selected_company_chairman_bonus_amount = inputs + .market_state + .map(|market_state| market_state.chairman_bonus_amount) + .filter(|amount| *amount != 0); +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/mod.rs b/crates/rrt-runtime/src/summary/builders/selected_company/mod.rs new file mode 100644 index 0000000..8c58bfe --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/mod.rs @@ -0,0 +1,388 @@ +mod annual_finance; +mod bond_policy; +mod distress; +mod dividend_policy; +mod equity_actions; +mod inputs; +mod market; +mod model; +mod periodic; + +use crate::state::RuntimeState; +use crate::summary::RuntimeSummary; + +use annual_finance::fill_selected_company_annual_finance_fields; +use bond_policy::fill_selected_company_bond_policy_fields; +use distress::fill_selected_company_distress_fields; +use dividend_policy::fill_selected_company_dividend_policy_fields; +use equity_actions::fill_selected_company_equity_action_fields; +use inputs::SelectedCompanyInputs; +use market::fill_selected_company_market_fields; +pub(super) use model::SelectedCompanySummaryFields; +use periodic::fill_selected_company_periodic_fields; + +pub(super) fn build_selected_company_summary_fields( + state: &RuntimeState, +) -> SelectedCompanySummaryFields { + let inputs = SelectedCompanyInputs::from_state(state); + let mut fields = SelectedCompanySummaryFields::default(); + fill_selected_company_market_fields(&mut fields, &inputs); + fill_selected_company_periodic_fields(&mut fields, &inputs); + fill_selected_company_distress_fields(&mut fields, &inputs); + fill_selected_company_bond_policy_fields(&mut fields, &inputs); + fill_selected_company_equity_action_fields(&mut fields, &inputs); + fill_selected_company_dividend_policy_fields(&mut fields, &inputs); + fill_selected_company_annual_finance_fields(&mut fields, &inputs); + fields +} + +pub(super) fn apply_selected_company_summary_fields( + summary: &mut RuntimeSummary, + fields: SelectedCompanySummaryFields, +) { + let SelectedCompanySummaryFields { + selected_company_outstanding_shares, + selected_company_bond_count, + selected_company_largest_live_bond_principal, + selected_company_highest_coupon_live_bond_principal, + selected_company_assigned_share_pool, + selected_company_unassigned_share_pool, + selected_company_cached_share_price, + selected_company_cached_share_price_value_f32_text, + selected_company_recent_per_share_cache_absolute_counter, + selected_company_recent_per_share_cached_value_f64_text, + selected_company_recent_per_share_subscore_value_f32_text, + selected_company_mutable_support_scalar_value_f32_text, + selected_company_stat_band_root_0cfb_count, + selected_company_stat_band_root_0d7f_count, + selected_company_stat_band_root_1c47_count, + selected_company_last_dividend_year, + selected_company_years_since_founding, + selected_company_years_since_last_bankruptcy, + selected_company_years_since_last_dividend, + selected_company_current_partial_year_weight_numerator, + selected_company_current_issue_absolute_counter, + selected_company_prior_issue_absolute_counter, + selected_company_current_issue_age_absolute_counter_delta, + selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8, + selected_company_periodic_side_latch_city_connection_latch, + selected_company_periodic_side_latch_linked_transit_latch, + selected_company_linked_transit_route_anchor_entry_id, + selected_company_linked_transit_route_anchor_fallback_counts, + selected_company_periodic_service_base_route_preference_raw_u8, + selected_company_periodic_service_effective_route_preference_raw_u8, + selected_company_periodic_service_electric_route_preference_override_active, + selected_company_periodic_service_route_quality_multiplier_basis_points, + active_periodic_route_preference_override_company_id, + active_periodic_route_preference_override_effective_raw_u8, + last_periodic_route_preference_override_company_id, + last_periodic_route_preference_override_effective_raw_u8, + selected_company_chairman_bonus_year, + selected_company_chairman_bonus_amount, + selected_company_creditor_pressure_recent_bad_net_profit_year_count, + selected_company_creditor_pressure_recent_peak_revenue, + selected_company_creditor_pressure_recent_three_year_net_profit_total, + selected_company_creditor_pressure_cash_floor, + selected_company_creditor_pressure_cash_plus_slot_12_total, + selected_company_creditor_pressure_share_price_floor, + selected_company_creditor_pressure_share_price_scalar, + selected_company_creditor_pressure_current_fuel_cost, + selected_company_creditor_pressure_current_fuel_cost_floor, + selected_company_creditor_pressure_eligible_for_bankruptcy_branch, + selected_company_deep_distress_current_cash, + selected_company_deep_distress_recent_first_three_net_profit_years, + selected_company_deep_distress_cash_floor, + selected_company_deep_distress_net_profit_floor, + selected_company_deep_distress_eligible_for_bankruptcy_fallback, + selected_company_annual_bond_linked_transit_latch, + selected_company_annual_bond_live_bond_count, + selected_company_annual_bond_live_bond_principal_total, + selected_company_annual_bond_matured_live_bond_count, + selected_company_annual_bond_matured_live_bond_principal_total, + selected_company_annual_bond_next_live_bond_maturity_year, + selected_company_annual_bond_live_bond_coupon_burden_total, + selected_company_annual_bond_current_cash, + selected_company_annual_bond_cash_after_full_repayment, + selected_company_annual_bond_issue_cash_floor, + selected_company_annual_bond_issue_principal_step, + selected_company_annual_bond_proposed_issue_bond_count, + selected_company_annual_bond_proposed_issue_total_principal, + selected_company_annual_bond_proposed_issue_years_to_maturity, + selected_company_annual_bond_eligible_for_issue_branch, + selected_company_stock_repurchase_city_connection_latch, + selected_company_stock_repurchase_building_density_growth_setting, + selected_company_stock_repurchase_linked_chairman_profile_id, + selected_company_stock_repurchase_linked_chairman_personality_raw_u8, + selected_company_stock_repurchase_batch_size, + selected_company_stock_repurchase_factor_basis_points, + selected_company_stock_repurchase_current_cash, + selected_company_stock_repurchase_stock_value_gate_cash_floor, + selected_company_stock_repurchase_support_adjusted_share_price_scalar, + selected_company_stock_repurchase_affordability_cash_floor, + selected_company_stock_repurchase_unassigned_share_pool, + selected_company_stock_repurchase_eligible_for_single_batch, + selected_company_stock_issue_live_bond_count, + selected_company_stock_issue_initial_batch_size, + selected_company_stock_issue_trimmed_batch_size, + selected_company_stock_issue_share_pressure_basis_points, + selected_company_stock_issue_pressured_share_price_scalar, + selected_company_stock_issue_pressured_proceeds, + selected_company_stock_issue_book_value_per_share_floor_applied, + selected_company_stock_issue_price_to_book_ratio_basis_points, + selected_company_stock_issue_current_cash, + selected_company_stock_issue_highest_coupon_live_bond_principal, + selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points, + selected_company_stock_issue_current_issue_age_absolute_counter_delta, + selected_company_stock_issue_current_issue_cooldown_floor, + selected_company_stock_issue_minimum_price_to_book_ratio_basis_points, + selected_company_stock_issue_passes_share_price_floor, + selected_company_stock_issue_passes_proceeds_floor, + selected_company_stock_issue_passes_cash_gate, + selected_company_stock_issue_passes_issue_cooldown_gate, + selected_company_stock_issue_passes_coupon_price_to_book_gate, + selected_company_stock_issue_eligible_for_double_tranche, + selected_company_dividend_weighted_recent_net_profit_total, + selected_company_dividend_weighted_recent_net_profit_average, + selected_company_dividend_current_cash, + selected_company_dividend_tiny_unassigned_share_cash_supplement_branch, + selected_company_dividend_tentative_target_per_share_tenths, + selected_company_dividend_current_per_share_tenths, + selected_company_dividend_growth_adjusted_current_per_share_tenths, + selected_company_dividend_board_approved_ceiling_tenths, + selected_company_dividend_proposed_per_share_tenths, + selected_company_dividend_eligible_for_adjustment_branch, + selected_company_annual_finance_policy_action, + selected_company_annual_finance_news_family_candidate, + selected_company_annual_finance_last_news_selector, + annual_finance_last_news_event_count, + selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible, + selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible, + selected_company_annual_finance_policy_bond_issue_eligible, + selected_company_annual_finance_policy_stock_repurchase_eligible, + selected_company_annual_finance_policy_stock_issue_eligible, + selected_company_annual_finance_policy_dividend_adjustment_eligible, + } = fields; + + summary.selected_company_outstanding_shares = selected_company_outstanding_shares; + summary.selected_company_bond_count = selected_company_bond_count; + summary.selected_company_largest_live_bond_principal = + selected_company_largest_live_bond_principal; + summary.selected_company_highest_coupon_live_bond_principal = + selected_company_highest_coupon_live_bond_principal; + summary.selected_company_assigned_share_pool = selected_company_assigned_share_pool; + summary.selected_company_unassigned_share_pool = selected_company_unassigned_share_pool; + summary.selected_company_cached_share_price = selected_company_cached_share_price; + summary.selected_company_cached_share_price_value_f32_text = + selected_company_cached_share_price_value_f32_text; + summary.selected_company_recent_per_share_cache_absolute_counter = + selected_company_recent_per_share_cache_absolute_counter; + summary.selected_company_recent_per_share_cached_value_f64_text = + selected_company_recent_per_share_cached_value_f64_text; + summary.selected_company_recent_per_share_subscore_value_f32_text = + selected_company_recent_per_share_subscore_value_f32_text; + summary.selected_company_mutable_support_scalar_value_f32_text = + selected_company_mutable_support_scalar_value_f32_text; + summary.selected_company_stat_band_root_0cfb_count = selected_company_stat_band_root_0cfb_count; + summary.selected_company_stat_band_root_0d7f_count = selected_company_stat_band_root_0d7f_count; + summary.selected_company_stat_band_root_1c47_count = selected_company_stat_band_root_1c47_count; + summary.selected_company_last_dividend_year = selected_company_last_dividend_year; + summary.selected_company_years_since_founding = selected_company_years_since_founding; + summary.selected_company_years_since_last_bankruptcy = + selected_company_years_since_last_bankruptcy; + summary.selected_company_years_since_last_dividend = selected_company_years_since_last_dividend; + summary.selected_company_current_partial_year_weight_numerator = + selected_company_current_partial_year_weight_numerator; + summary.selected_company_current_issue_absolute_counter = + selected_company_current_issue_absolute_counter; + summary.selected_company_prior_issue_absolute_counter = + selected_company_prior_issue_absolute_counter; + summary.selected_company_current_issue_age_absolute_counter_delta = + selected_company_current_issue_age_absolute_counter_delta; + summary.selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8 = + selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8; + summary.selected_company_periodic_side_latch_city_connection_latch = + selected_company_periodic_side_latch_city_connection_latch; + summary.selected_company_periodic_side_latch_linked_transit_latch = + selected_company_periodic_side_latch_linked_transit_latch; + summary.selected_company_linked_transit_route_anchor_entry_id = + selected_company_linked_transit_route_anchor_entry_id; + summary.selected_company_linked_transit_route_anchor_fallback_counts = + selected_company_linked_transit_route_anchor_fallback_counts; + summary.selected_company_periodic_service_base_route_preference_raw_u8 = + selected_company_periodic_service_base_route_preference_raw_u8; + summary.selected_company_periodic_service_effective_route_preference_raw_u8 = + selected_company_periodic_service_effective_route_preference_raw_u8; + summary.selected_company_periodic_service_electric_route_preference_override_active = + selected_company_periodic_service_electric_route_preference_override_active; + summary.selected_company_periodic_service_route_quality_multiplier_basis_points = + selected_company_periodic_service_route_quality_multiplier_basis_points; + summary.active_periodic_route_preference_override_company_id = + active_periodic_route_preference_override_company_id; + summary.active_periodic_route_preference_override_effective_raw_u8 = + active_periodic_route_preference_override_effective_raw_u8; + summary.last_periodic_route_preference_override_company_id = + last_periodic_route_preference_override_company_id; + summary.last_periodic_route_preference_override_effective_raw_u8 = + last_periodic_route_preference_override_effective_raw_u8; + summary.selected_company_chairman_bonus_year = selected_company_chairman_bonus_year; + summary.selected_company_chairman_bonus_amount = selected_company_chairman_bonus_amount; + summary.selected_company_creditor_pressure_recent_bad_net_profit_year_count = + selected_company_creditor_pressure_recent_bad_net_profit_year_count; + summary.selected_company_creditor_pressure_recent_peak_revenue = + selected_company_creditor_pressure_recent_peak_revenue; + summary.selected_company_creditor_pressure_recent_three_year_net_profit_total = + selected_company_creditor_pressure_recent_three_year_net_profit_total; + summary.selected_company_creditor_pressure_cash_floor = + selected_company_creditor_pressure_cash_floor; + summary.selected_company_creditor_pressure_cash_plus_slot_12_total = + selected_company_creditor_pressure_cash_plus_slot_12_total; + summary.selected_company_creditor_pressure_share_price_floor = + selected_company_creditor_pressure_share_price_floor; + summary.selected_company_creditor_pressure_share_price_scalar = + selected_company_creditor_pressure_share_price_scalar; + summary.selected_company_creditor_pressure_current_fuel_cost = + selected_company_creditor_pressure_current_fuel_cost; + summary.selected_company_creditor_pressure_current_fuel_cost_floor = + selected_company_creditor_pressure_current_fuel_cost_floor; + summary.selected_company_creditor_pressure_eligible_for_bankruptcy_branch = + selected_company_creditor_pressure_eligible_for_bankruptcy_branch; + summary.selected_company_deep_distress_current_cash = + selected_company_deep_distress_current_cash; + summary.selected_company_deep_distress_recent_first_three_net_profit_years = + selected_company_deep_distress_recent_first_three_net_profit_years; + summary.selected_company_deep_distress_cash_floor = selected_company_deep_distress_cash_floor; + summary.selected_company_deep_distress_net_profit_floor = + selected_company_deep_distress_net_profit_floor; + summary.selected_company_deep_distress_eligible_for_bankruptcy_fallback = + selected_company_deep_distress_eligible_for_bankruptcy_fallback; + summary.selected_company_annual_bond_linked_transit_latch = + selected_company_annual_bond_linked_transit_latch; + summary.selected_company_annual_bond_live_bond_count = + selected_company_annual_bond_live_bond_count; + summary.selected_company_annual_bond_live_bond_principal_total = + selected_company_annual_bond_live_bond_principal_total; + summary.selected_company_annual_bond_matured_live_bond_count = + selected_company_annual_bond_matured_live_bond_count; + summary.selected_company_annual_bond_matured_live_bond_principal_total = + selected_company_annual_bond_matured_live_bond_principal_total; + summary.selected_company_annual_bond_next_live_bond_maturity_year = + selected_company_annual_bond_next_live_bond_maturity_year; + summary.selected_company_annual_bond_live_bond_coupon_burden_total = + selected_company_annual_bond_live_bond_coupon_burden_total; + summary.selected_company_annual_bond_current_cash = selected_company_annual_bond_current_cash; + summary.selected_company_annual_bond_cash_after_full_repayment = + selected_company_annual_bond_cash_after_full_repayment; + summary.selected_company_annual_bond_issue_cash_floor = + selected_company_annual_bond_issue_cash_floor; + summary.selected_company_annual_bond_issue_principal_step = + selected_company_annual_bond_issue_principal_step; + summary.selected_company_annual_bond_proposed_issue_bond_count = + selected_company_annual_bond_proposed_issue_bond_count; + summary.selected_company_annual_bond_proposed_issue_total_principal = + selected_company_annual_bond_proposed_issue_total_principal; + summary.selected_company_annual_bond_proposed_issue_years_to_maturity = + selected_company_annual_bond_proposed_issue_years_to_maturity; + summary.selected_company_annual_bond_eligible_for_issue_branch = + selected_company_annual_bond_eligible_for_issue_branch; + summary.selected_company_stock_repurchase_city_connection_latch = + selected_company_stock_repurchase_city_connection_latch; + summary.selected_company_stock_repurchase_building_density_growth_setting = + selected_company_stock_repurchase_building_density_growth_setting; + summary.selected_company_stock_repurchase_linked_chairman_profile_id = + selected_company_stock_repurchase_linked_chairman_profile_id; + summary.selected_company_stock_repurchase_linked_chairman_personality_raw_u8 = + selected_company_stock_repurchase_linked_chairman_personality_raw_u8; + summary.selected_company_stock_repurchase_batch_size = + selected_company_stock_repurchase_batch_size; + summary.selected_company_stock_repurchase_factor_basis_points = + selected_company_stock_repurchase_factor_basis_points; + summary.selected_company_stock_repurchase_current_cash = + selected_company_stock_repurchase_current_cash; + summary.selected_company_stock_repurchase_stock_value_gate_cash_floor = + selected_company_stock_repurchase_stock_value_gate_cash_floor; + summary.selected_company_stock_repurchase_support_adjusted_share_price_scalar = + selected_company_stock_repurchase_support_adjusted_share_price_scalar; + summary.selected_company_stock_repurchase_affordability_cash_floor = + selected_company_stock_repurchase_affordability_cash_floor; + summary.selected_company_stock_repurchase_unassigned_share_pool = + selected_company_stock_repurchase_unassigned_share_pool; + summary.selected_company_stock_repurchase_eligible_for_single_batch = + selected_company_stock_repurchase_eligible_for_single_batch; + summary.selected_company_stock_issue_live_bond_count = + selected_company_stock_issue_live_bond_count; + summary.selected_company_stock_issue_initial_batch_size = + selected_company_stock_issue_initial_batch_size; + summary.selected_company_stock_issue_trimmed_batch_size = + selected_company_stock_issue_trimmed_batch_size; + summary.selected_company_stock_issue_share_pressure_basis_points = + selected_company_stock_issue_share_pressure_basis_points; + summary.selected_company_stock_issue_pressured_share_price_scalar = + selected_company_stock_issue_pressured_share_price_scalar; + summary.selected_company_stock_issue_pressured_proceeds = + selected_company_stock_issue_pressured_proceeds; + summary.selected_company_stock_issue_book_value_per_share_floor_applied = + selected_company_stock_issue_book_value_per_share_floor_applied; + summary.selected_company_stock_issue_price_to_book_ratio_basis_points = + selected_company_stock_issue_price_to_book_ratio_basis_points; + summary.selected_company_stock_issue_current_cash = selected_company_stock_issue_current_cash; + summary.selected_company_stock_issue_highest_coupon_live_bond_principal = + selected_company_stock_issue_highest_coupon_live_bond_principal; + summary.selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points = + selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points; + summary.selected_company_stock_issue_current_issue_age_absolute_counter_delta = + selected_company_stock_issue_current_issue_age_absolute_counter_delta; + summary.selected_company_stock_issue_current_issue_cooldown_floor = + selected_company_stock_issue_current_issue_cooldown_floor; + summary.selected_company_stock_issue_minimum_price_to_book_ratio_basis_points = + selected_company_stock_issue_minimum_price_to_book_ratio_basis_points; + summary.selected_company_stock_issue_passes_share_price_floor = + selected_company_stock_issue_passes_share_price_floor; + summary.selected_company_stock_issue_passes_proceeds_floor = + selected_company_stock_issue_passes_proceeds_floor; + summary.selected_company_stock_issue_passes_cash_gate = + selected_company_stock_issue_passes_cash_gate; + summary.selected_company_stock_issue_passes_issue_cooldown_gate = + selected_company_stock_issue_passes_issue_cooldown_gate; + summary.selected_company_stock_issue_passes_coupon_price_to_book_gate = + selected_company_stock_issue_passes_coupon_price_to_book_gate; + summary.selected_company_stock_issue_eligible_for_double_tranche = + selected_company_stock_issue_eligible_for_double_tranche; + summary.selected_company_dividend_weighted_recent_net_profit_total = + selected_company_dividend_weighted_recent_net_profit_total; + summary.selected_company_dividend_weighted_recent_net_profit_average = + selected_company_dividend_weighted_recent_net_profit_average; + summary.selected_company_dividend_current_cash = selected_company_dividend_current_cash; + summary.selected_company_dividend_tiny_unassigned_share_cash_supplement_branch = + selected_company_dividend_tiny_unassigned_share_cash_supplement_branch; + summary.selected_company_dividend_tentative_target_per_share_tenths = + selected_company_dividend_tentative_target_per_share_tenths; + summary.selected_company_dividend_current_per_share_tenths = + selected_company_dividend_current_per_share_tenths; + summary.selected_company_dividend_growth_adjusted_current_per_share_tenths = + selected_company_dividend_growth_adjusted_current_per_share_tenths; + summary.selected_company_dividend_board_approved_ceiling_tenths = + selected_company_dividend_board_approved_ceiling_tenths; + summary.selected_company_dividend_proposed_per_share_tenths = + selected_company_dividend_proposed_per_share_tenths; + summary.selected_company_dividend_eligible_for_adjustment_branch = + selected_company_dividend_eligible_for_adjustment_branch; + summary.selected_company_annual_finance_policy_action = + selected_company_annual_finance_policy_action; + summary.selected_company_annual_finance_news_family_candidate = + selected_company_annual_finance_news_family_candidate; + summary.selected_company_annual_finance_last_news_selector = + selected_company_annual_finance_last_news_selector; + summary.annual_finance_last_news_event_count = annual_finance_last_news_event_count; + summary.selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible = + selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible; + summary.selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible = + selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible; + summary.selected_company_annual_finance_policy_bond_issue_eligible = + selected_company_annual_finance_policy_bond_issue_eligible; + summary.selected_company_annual_finance_policy_stock_repurchase_eligible = + selected_company_annual_finance_policy_stock_repurchase_eligible; + summary.selected_company_annual_finance_policy_stock_issue_eligible = + selected_company_annual_finance_policy_stock_issue_eligible; + summary.selected_company_annual_finance_policy_dividend_adjustment_eligible = + selected_company_annual_finance_policy_dividend_adjustment_eligible; +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/model.rs b/crates/rrt-runtime/src/summary/builders/selected_company/model.rs new file mode 100644 index 0000000..2fe40b6 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/model.rs @@ -0,0 +1,124 @@ +#[derive(Default)] +pub(in crate::summary::builders) struct SelectedCompanySummaryFields { + pub selected_company_outstanding_shares: Option, + pub selected_company_bond_count: Option, + pub selected_company_largest_live_bond_principal: Option, + pub selected_company_highest_coupon_live_bond_principal: Option, + pub selected_company_assigned_share_pool: Option, + pub selected_company_unassigned_share_pool: Option, + pub selected_company_cached_share_price: Option, + pub selected_company_cached_share_price_value_f32_text: Option, + pub selected_company_recent_per_share_cache_absolute_counter: Option, + pub selected_company_recent_per_share_cached_value_f64_text: Option, + pub selected_company_recent_per_share_subscore_value_f32_text: Option, + pub selected_company_mutable_support_scalar_value_f32_text: Option, + pub selected_company_stat_band_root_0cfb_count: usize, + pub selected_company_stat_band_root_0d7f_count: usize, + pub selected_company_stat_band_root_1c47_count: usize, + pub selected_company_last_dividend_year: Option, + pub selected_company_years_since_founding: Option, + pub selected_company_years_since_last_bankruptcy: Option, + pub selected_company_years_since_last_dividend: Option, + pub selected_company_current_partial_year_weight_numerator: Option, + pub selected_company_current_issue_absolute_counter: Option, + pub selected_company_prior_issue_absolute_counter: Option, + pub selected_company_current_issue_age_absolute_counter_delta: Option, + pub selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8: Option, + pub selected_company_periodic_side_latch_city_connection_latch: Option, + pub selected_company_periodic_side_latch_linked_transit_latch: Option, + pub selected_company_linked_transit_route_anchor_entry_id: Option, + pub selected_company_linked_transit_route_anchor_fallback_counts: Vec, + pub selected_company_periodic_service_base_route_preference_raw_u8: Option, + pub selected_company_periodic_service_effective_route_preference_raw_u8: Option, + pub selected_company_periodic_service_electric_route_preference_override_active: Option, + pub selected_company_periodic_service_route_quality_multiplier_basis_points: Option, + pub active_periodic_route_preference_override_company_id: Option, + pub active_periodic_route_preference_override_effective_raw_u8: Option, + pub last_periodic_route_preference_override_company_id: Option, + pub last_periodic_route_preference_override_effective_raw_u8: Option, + pub selected_company_chairman_bonus_year: Option, + pub selected_company_chairman_bonus_amount: Option, + pub selected_company_creditor_pressure_recent_bad_net_profit_year_count: Option, + pub selected_company_creditor_pressure_recent_peak_revenue: Option, + pub selected_company_creditor_pressure_recent_three_year_net_profit_total: Option, + pub selected_company_creditor_pressure_cash_floor: Option, + pub selected_company_creditor_pressure_cash_plus_slot_12_total: Option, + pub selected_company_creditor_pressure_share_price_floor: Option, + pub selected_company_creditor_pressure_share_price_scalar: Option, + pub selected_company_creditor_pressure_current_fuel_cost: Option, + pub selected_company_creditor_pressure_current_fuel_cost_floor: Option, + pub selected_company_creditor_pressure_eligible_for_bankruptcy_branch: Option, + pub selected_company_deep_distress_current_cash: Option, + pub selected_company_deep_distress_recent_first_three_net_profit_years: Vec, + pub selected_company_deep_distress_cash_floor: Option, + pub selected_company_deep_distress_net_profit_floor: Option, + pub selected_company_deep_distress_eligible_for_bankruptcy_fallback: Option, + pub selected_company_annual_bond_linked_transit_latch: Option, + pub selected_company_annual_bond_live_bond_count: Option, + pub selected_company_annual_bond_live_bond_principal_total: Option, + pub selected_company_annual_bond_matured_live_bond_count: Option, + pub selected_company_annual_bond_matured_live_bond_principal_total: Option, + pub selected_company_annual_bond_next_live_bond_maturity_year: Option, + pub selected_company_annual_bond_live_bond_coupon_burden_total: Option, + pub selected_company_annual_bond_current_cash: Option, + pub selected_company_annual_bond_cash_after_full_repayment: Option, + pub selected_company_annual_bond_issue_cash_floor: Option, + pub selected_company_annual_bond_issue_principal_step: Option, + pub selected_company_annual_bond_proposed_issue_bond_count: Option, + pub selected_company_annual_bond_proposed_issue_total_principal: Option, + pub selected_company_annual_bond_proposed_issue_years_to_maturity: Option, + pub selected_company_annual_bond_eligible_for_issue_branch: Option, + pub selected_company_stock_repurchase_city_connection_latch: Option, + pub selected_company_stock_repurchase_building_density_growth_setting: Option, + pub selected_company_stock_repurchase_linked_chairman_profile_id: Option, + pub selected_company_stock_repurchase_linked_chairman_personality_raw_u8: Option, + pub selected_company_stock_repurchase_batch_size: Option, + pub selected_company_stock_repurchase_factor_basis_points: Option, + pub selected_company_stock_repurchase_current_cash: Option, + pub selected_company_stock_repurchase_stock_value_gate_cash_floor: Option, + pub selected_company_stock_repurchase_support_adjusted_share_price_scalar: Option, + pub selected_company_stock_repurchase_affordability_cash_floor: Option, + pub selected_company_stock_repurchase_unassigned_share_pool: Option, + pub selected_company_stock_repurchase_eligible_for_single_batch: Option, + pub selected_company_stock_issue_live_bond_count: Option, + pub selected_company_stock_issue_initial_batch_size: Option, + pub selected_company_stock_issue_trimmed_batch_size: Option, + pub selected_company_stock_issue_share_pressure_basis_points: Option, + pub selected_company_stock_issue_pressured_share_price_scalar: Option, + pub selected_company_stock_issue_pressured_proceeds: Option, + pub selected_company_stock_issue_book_value_per_share_floor_applied: Option, + pub selected_company_stock_issue_price_to_book_ratio_basis_points: Option, + pub selected_company_stock_issue_current_cash: Option, + pub selected_company_stock_issue_highest_coupon_live_bond_principal: Option, + pub selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points: Option, + pub selected_company_stock_issue_current_issue_age_absolute_counter_delta: Option, + pub selected_company_stock_issue_current_issue_cooldown_floor: Option, + pub selected_company_stock_issue_minimum_price_to_book_ratio_basis_points: Option, + pub selected_company_stock_issue_passes_share_price_floor: Option, + pub selected_company_stock_issue_passes_proceeds_floor: Option, + pub selected_company_stock_issue_passes_cash_gate: Option, + pub selected_company_stock_issue_passes_issue_cooldown_gate: Option, + pub selected_company_stock_issue_passes_coupon_price_to_book_gate: Option, + pub selected_company_stock_issue_eligible_for_double_tranche: Option, + pub selected_company_dividend_weighted_recent_net_profit_total: Option, + pub selected_company_dividend_weighted_recent_net_profit_average: Option, + pub selected_company_dividend_current_cash: Option, + pub selected_company_dividend_tiny_unassigned_share_cash_supplement_branch: Option, + pub selected_company_dividend_tentative_target_per_share_tenths: Option, + pub selected_company_dividend_current_per_share_tenths: Option, + pub selected_company_dividend_growth_adjusted_current_per_share_tenths: Option, + pub selected_company_dividend_board_approved_ceiling_tenths: Option, + pub selected_company_dividend_proposed_per_share_tenths: Option, + pub selected_company_dividend_eligible_for_adjustment_branch: Option, + pub selected_company_annual_finance_policy_action: Option, + pub selected_company_annual_finance_news_family_candidate: Option, + pub selected_company_annual_finance_last_news_selector: Option, + pub annual_finance_last_news_event_count: usize, + pub selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible: Option, + pub selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible: + Option, + pub selected_company_annual_finance_policy_bond_issue_eligible: Option, + pub selected_company_annual_finance_policy_stock_repurchase_eligible: Option, + pub selected_company_annual_finance_policy_stock_issue_eligible: Option, + pub selected_company_annual_finance_policy_dividend_adjustment_eligible: Option, +} diff --git a/crates/rrt-runtime/src/summary/builders/selected_company/periodic.rs b/crates/rrt-runtime/src/summary/builders/selected_company/periodic.rs new file mode 100644 index 0000000..05fa9ac --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/selected_company/periodic.rs @@ -0,0 +1,68 @@ +use super::inputs::SelectedCompanyInputs; +use super::model::SelectedCompanySummaryFields; + +pub(super) fn fill_selected_company_periodic_fields( + fields: &mut SelectedCompanySummaryFields, + inputs: &SelectedCompanyInputs<'_>, +) { + fields.selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8 = inputs + .periodic_side_latch_state + .and_then(|latch_state| latch_state.preferred_locomotive_engine_type_raw_u8); + fields.selected_company_periodic_side_latch_city_connection_latch = inputs + .periodic_side_latch_state + .map(|latch_state| latch_state.city_connection_latch); + fields.selected_company_periodic_side_latch_linked_transit_latch = inputs + .periodic_side_latch_state + .map(|latch_state| latch_state.linked_transit_latch); + fields.selected_company_linked_transit_route_anchor_entry_id = inputs + .market_state + .and_then(|market_state| market_state.linked_transit_route_anchor_entry_id); + fields.selected_company_linked_transit_route_anchor_fallback_counts = inputs + .market_state + .map(|market_state| { + market_state + .linked_transit_route_anchor_fallback_counts + .clone() + }) + .unwrap_or_default(); + fields.selected_company_periodic_service_base_route_preference_raw_u8 = inputs + .periodic_service_state + .as_ref() + .and_then(|service_state| service_state.base_route_preference_raw_u8); + fields.selected_company_periodic_service_effective_route_preference_raw_u8 = inputs + .periodic_service_state + .as_ref() + .and_then(|service_state| service_state.effective_route_preference_raw_u8); + fields.selected_company_periodic_service_electric_route_preference_override_active = inputs + .periodic_service_state + .as_ref() + .map(|service_state| service_state.electric_route_preference_override_active); + fields.selected_company_periodic_service_route_quality_multiplier_basis_points = inputs + .periodic_service_state + .as_ref() + .map(|service_state| service_state.effective_route_quality_multiplier_basis_points); + fields.active_periodic_route_preference_override_company_id = inputs + .state + .service_state + .active_periodic_route_preference_override + .as_ref() + .map(|override_state| override_state.company_id); + fields.active_periodic_route_preference_override_effective_raw_u8 = inputs + .state + .service_state + .active_periodic_route_preference_override + .as_ref() + .and_then(|override_state| override_state.effective_route_preference_raw_u8); + fields.last_periodic_route_preference_override_company_id = inputs + .state + .service_state + .last_periodic_route_preference_override + .as_ref() + .map(|override_state| override_state.company_id); + fields.last_periodic_route_preference_override_effective_raw_u8 = inputs + .state + .service_state + .last_periodic_route_preference_override + .as_ref() + .and_then(|override_state| override_state.effective_route_preference_raw_u8); +} diff --git a/crates/rrt-runtime/src/summary/builders/world/apply.rs b/crates/rrt-runtime/src/summary/builders/world/apply.rs new file mode 100644 index 0000000..b001daf --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/world/apply.rs @@ -0,0 +1,220 @@ +use super::model::WorldSummaryFields; +use crate::summary::RuntimeSummary; + +pub(crate) fn apply_world_summary_fields(summary: &mut RuntimeSummary, fields: WorldSummaryFields) { + let WorldSummaryFields { + calendar_projection_source, + calendar_projection_is_placeholder, + world_flag_count, + world_restore_selected_year_profile_lane, + world_restore_campaign_scenario_enabled, + world_restore_sandbox_enabled, + world_restore_seed_tuple_written_from_raw_lane, + world_restore_absolute_counter_requires_shell_context, + world_restore_absolute_counter_reconstructible_from_save, + world_restore_packed_year_word_raw_u16, + world_restore_partial_year_progress_raw_u8, + world_restore_current_calendar_tuple_word_raw_u32, + world_restore_current_calendar_tuple_word_2_raw_u32, + world_restore_absolute_counter_raw_u32, + world_restore_absolute_counter_mirror_raw_u32, + world_restore_disable_cargo_economy_special_condition_slot, + world_restore_disable_cargo_economy_special_condition_reconstructible_from_save, + world_restore_disable_cargo_economy_special_condition_write_side_grounded, + world_restore_disable_cargo_economy_special_condition_enabled, + world_restore_use_bio_accelerator_cars_enabled, + world_restore_use_wartime_cargos_enabled, + world_restore_disable_train_crashes_enabled, + world_restore_disable_train_crashes_and_breakdowns_enabled, + world_restore_ai_ignore_territories_at_startup_enabled, + world_restore_limited_track_building_amount, + world_restore_economic_status_code, + world_restore_territory_access_cost, + world_restore_linked_site_removal_follow_on_gate_raw_u8, + world_restore_linked_site_removal_follow_on_gate_enabled, + world_restore_auto_show_grade_during_track_lay_raw_u8, + world_restore_starting_building_density_level_raw_u8, + world_restore_post_text_building_density_growth_raw_u8, + world_restore_leftover_simulation_time_accumulator_raw_u32, + world_restore_leftover_simulation_time_accumulator_value_f32_text, + world_restore_selected_year_lane_snapshot_raw_u8, + world_restore_all_steam_locomotives_available_raw_u8, + world_restore_all_steam_locomotives_available_enabled, + world_restore_all_diesel_locomotives_available_raw_u8, + world_restore_all_diesel_locomotives_available_enabled, + world_restore_all_electric_locomotives_available_raw_u8, + world_restore_all_electric_locomotives_available_enabled, + world_restore_issue_37_value, + world_restore_issue_38_value, + world_restore_issue_39_value, + world_restore_issue_3a_value, + world_restore_issue_37_multiplier_raw_u32, + world_restore_issue_37_multiplier_value_f32_text, + world_restore_stock_issue_and_buyback_policy_raw_u8, + world_restore_bond_issue_and_repayment_policy_raw_u8, + world_restore_bankruptcy_policy_raw_u8, + world_restore_dividend_policy_raw_u8, + world_restore_building_density_growth_setting_raw_u32, + world_restore_stock_issue_and_buyback_allowed, + world_restore_bond_issue_and_repayment_allowed, + world_restore_bankruptcy_allowed, + world_restore_dividend_adjustment_allowed, + world_restore_finance_neighborhood_count, + world_restore_finance_neighborhood_labels, + world_restore_economic_tuning_mirror_raw_u32, + world_restore_economic_tuning_mirror_value_f32_text, + world_restore_economic_tuning_lane_count, + world_restore_economic_tuning_lane_value_f32_text, + world_restore_cached_available_locomotive_rating_raw_u32, + world_restore_cached_available_locomotive_rating_value_f32_text, + world_restore_selected_year_bucket_scalar_raw_u32, + world_restore_selected_year_bucket_scalar_value_f32_text, + world_restore_selected_year_bucket_direct_lane_count, + world_restore_selected_year_bucket_direct_lane_value_f32_text, + world_restore_selected_year_bucket_complement_lane_count, + world_restore_selected_year_bucket_complement_lane_value_f32_text, + world_restore_selected_year_bucket_scaled_companion_lane_count, + world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text, + world_restore_selected_year_gap_scalar_raw_u32, + world_restore_selected_year_gap_scalar_value_f32_text, + world_restore_absolute_counter_restore_kind, + world_restore_absolute_counter_adjustment_context, + metadata_count, + company_count, + active_company_count, + company_market_state_owner_count, + } = fields; + + summary.calendar_projection_source = calendar_projection_source; + summary.calendar_projection_is_placeholder = calendar_projection_is_placeholder; + summary.world_flag_count = world_flag_count; + summary.world_restore_selected_year_profile_lane = world_restore_selected_year_profile_lane; + summary.world_restore_campaign_scenario_enabled = world_restore_campaign_scenario_enabled; + summary.world_restore_sandbox_enabled = world_restore_sandbox_enabled; + summary.world_restore_seed_tuple_written_from_raw_lane = + world_restore_seed_tuple_written_from_raw_lane; + summary.world_restore_absolute_counter_requires_shell_context = + world_restore_absolute_counter_requires_shell_context; + summary.world_restore_absolute_counter_reconstructible_from_save = + world_restore_absolute_counter_reconstructible_from_save; + summary.world_restore_packed_year_word_raw_u16 = world_restore_packed_year_word_raw_u16; + summary.world_restore_partial_year_progress_raw_u8 = world_restore_partial_year_progress_raw_u8; + summary.world_restore_current_calendar_tuple_word_raw_u32 = + world_restore_current_calendar_tuple_word_raw_u32; + summary.world_restore_current_calendar_tuple_word_2_raw_u32 = + world_restore_current_calendar_tuple_word_2_raw_u32; + summary.world_restore_absolute_counter_raw_u32 = world_restore_absolute_counter_raw_u32; + summary.world_restore_absolute_counter_mirror_raw_u32 = + world_restore_absolute_counter_mirror_raw_u32; + summary.world_restore_disable_cargo_economy_special_condition_slot = + world_restore_disable_cargo_economy_special_condition_slot; + summary.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save = + world_restore_disable_cargo_economy_special_condition_reconstructible_from_save; + summary.world_restore_disable_cargo_economy_special_condition_write_side_grounded = + world_restore_disable_cargo_economy_special_condition_write_side_grounded; + summary.world_restore_disable_cargo_economy_special_condition_enabled = + world_restore_disable_cargo_economy_special_condition_enabled; + summary.world_restore_use_bio_accelerator_cars_enabled = + world_restore_use_bio_accelerator_cars_enabled; + summary.world_restore_use_wartime_cargos_enabled = world_restore_use_wartime_cargos_enabled; + summary.world_restore_disable_train_crashes_enabled = + world_restore_disable_train_crashes_enabled; + summary.world_restore_disable_train_crashes_and_breakdowns_enabled = + world_restore_disable_train_crashes_and_breakdowns_enabled; + summary.world_restore_ai_ignore_territories_at_startup_enabled = + world_restore_ai_ignore_territories_at_startup_enabled; + summary.world_restore_limited_track_building_amount = + world_restore_limited_track_building_amount; + summary.world_restore_economic_status_code = world_restore_economic_status_code; + summary.world_restore_territory_access_cost = world_restore_territory_access_cost; + summary.world_restore_linked_site_removal_follow_on_gate_raw_u8 = + world_restore_linked_site_removal_follow_on_gate_raw_u8; + summary.world_restore_linked_site_removal_follow_on_gate_enabled = + world_restore_linked_site_removal_follow_on_gate_enabled; + summary.world_restore_auto_show_grade_during_track_lay_raw_u8 = + world_restore_auto_show_grade_during_track_lay_raw_u8; + summary.world_restore_starting_building_density_level_raw_u8 = + world_restore_starting_building_density_level_raw_u8; + summary.world_restore_post_text_building_density_growth_raw_u8 = + world_restore_post_text_building_density_growth_raw_u8; + summary.world_restore_leftover_simulation_time_accumulator_raw_u32 = + world_restore_leftover_simulation_time_accumulator_raw_u32; + summary.world_restore_leftover_simulation_time_accumulator_value_f32_text = + world_restore_leftover_simulation_time_accumulator_value_f32_text; + summary.world_restore_selected_year_lane_snapshot_raw_u8 = + world_restore_selected_year_lane_snapshot_raw_u8; + summary.world_restore_all_steam_locomotives_available_raw_u8 = + world_restore_all_steam_locomotives_available_raw_u8; + summary.world_restore_all_steam_locomotives_available_enabled = + world_restore_all_steam_locomotives_available_enabled; + summary.world_restore_all_diesel_locomotives_available_raw_u8 = + world_restore_all_diesel_locomotives_available_raw_u8; + summary.world_restore_all_diesel_locomotives_available_enabled = + world_restore_all_diesel_locomotives_available_enabled; + summary.world_restore_all_electric_locomotives_available_raw_u8 = + world_restore_all_electric_locomotives_available_raw_u8; + summary.world_restore_all_electric_locomotives_available_enabled = + world_restore_all_electric_locomotives_available_enabled; + summary.world_restore_issue_37_value = world_restore_issue_37_value; + summary.world_restore_issue_38_value = world_restore_issue_38_value; + summary.world_restore_issue_39_value = world_restore_issue_39_value; + summary.world_restore_issue_3a_value = world_restore_issue_3a_value; + summary.world_restore_issue_37_multiplier_raw_u32 = world_restore_issue_37_multiplier_raw_u32; + summary.world_restore_issue_37_multiplier_value_f32_text = + world_restore_issue_37_multiplier_value_f32_text; + summary.world_restore_stock_issue_and_buyback_policy_raw_u8 = + world_restore_stock_issue_and_buyback_policy_raw_u8; + summary.world_restore_bond_issue_and_repayment_policy_raw_u8 = + world_restore_bond_issue_and_repayment_policy_raw_u8; + summary.world_restore_bankruptcy_policy_raw_u8 = world_restore_bankruptcy_policy_raw_u8; + summary.world_restore_dividend_policy_raw_u8 = world_restore_dividend_policy_raw_u8; + summary.world_restore_building_density_growth_setting_raw_u32 = + world_restore_building_density_growth_setting_raw_u32; + summary.world_restore_stock_issue_and_buyback_allowed = + world_restore_stock_issue_and_buyback_allowed; + summary.world_restore_bond_issue_and_repayment_allowed = + world_restore_bond_issue_and_repayment_allowed; + summary.world_restore_bankruptcy_allowed = world_restore_bankruptcy_allowed; + summary.world_restore_dividend_adjustment_allowed = world_restore_dividend_adjustment_allowed; + summary.world_restore_finance_neighborhood_count = world_restore_finance_neighborhood_count; + summary.world_restore_finance_neighborhood_labels = world_restore_finance_neighborhood_labels; + summary.world_restore_economic_tuning_mirror_raw_u32 = + world_restore_economic_tuning_mirror_raw_u32; + summary.world_restore_economic_tuning_mirror_value_f32_text = + world_restore_economic_tuning_mirror_value_f32_text; + summary.world_restore_economic_tuning_lane_count = world_restore_economic_tuning_lane_count; + summary.world_restore_economic_tuning_lane_value_f32_text = + world_restore_economic_tuning_lane_value_f32_text; + summary.world_restore_cached_available_locomotive_rating_raw_u32 = + world_restore_cached_available_locomotive_rating_raw_u32; + summary.world_restore_cached_available_locomotive_rating_value_f32_text = + world_restore_cached_available_locomotive_rating_value_f32_text; + summary.world_restore_selected_year_bucket_scalar_raw_u32 = + world_restore_selected_year_bucket_scalar_raw_u32; + summary.world_restore_selected_year_bucket_scalar_value_f32_text = + world_restore_selected_year_bucket_scalar_value_f32_text; + summary.world_restore_selected_year_bucket_direct_lane_count = + world_restore_selected_year_bucket_direct_lane_count; + summary.world_restore_selected_year_bucket_direct_lane_value_f32_text = + world_restore_selected_year_bucket_direct_lane_value_f32_text; + summary.world_restore_selected_year_bucket_complement_lane_count = + world_restore_selected_year_bucket_complement_lane_count; + summary.world_restore_selected_year_bucket_complement_lane_value_f32_text = + world_restore_selected_year_bucket_complement_lane_value_f32_text; + summary.world_restore_selected_year_bucket_scaled_companion_lane_count = + world_restore_selected_year_bucket_scaled_companion_lane_count; + summary.world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text = + world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text; + summary.world_restore_selected_year_gap_scalar_raw_u32 = + world_restore_selected_year_gap_scalar_raw_u32; + summary.world_restore_selected_year_gap_scalar_value_f32_text = + world_restore_selected_year_gap_scalar_value_f32_text; + summary.world_restore_absolute_counter_restore_kind = + world_restore_absolute_counter_restore_kind; + summary.world_restore_absolute_counter_adjustment_context = + world_restore_absolute_counter_adjustment_context; + summary.metadata_count = metadata_count; + summary.company_count = company_count; + summary.active_company_count = active_company_count; + summary.company_market_state_owner_count = company_market_state_owner_count; +} diff --git a/crates/rrt-runtime/src/summary/builders/world/economy.rs b/crates/rrt-runtime/src/summary/builders/world/economy.rs new file mode 100644 index 0000000..9f71798 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/world/economy.rs @@ -0,0 +1,35 @@ +use super::model::WorldSummaryFields; +use crate::RuntimeState; + +pub(super) fn fill_world_economy_fields(fields: &mut WorldSummaryFields, state: &RuntimeState) { + fields.world_restore_issue_37_value = state.world_restore.issue_37_value; + fields.world_restore_issue_38_value = state.world_restore.issue_38_value; + fields.world_restore_issue_39_value = state.world_restore.issue_39_value; + fields.world_restore_issue_3a_value = state.world_restore.issue_3a_value; + fields.world_restore_issue_37_multiplier_raw_u32 = + state.world_restore.issue_37_multiplier_raw_u32; + fields.world_restore_issue_37_multiplier_value_f32_text = state + .world_restore + .issue_37_multiplier_value_f32_text + .clone(); + fields.world_restore_finance_neighborhood_count = + state.world_restore.finance_neighborhood_candidates.len(); + fields.world_restore_finance_neighborhood_labels = state + .world_restore + .finance_neighborhood_candidates + .iter() + .map(|candidate| candidate.label.clone()) + .collect(); + fields.world_restore_economic_tuning_mirror_raw_u32 = + state.world_restore.economic_tuning_mirror_raw_u32; + fields.world_restore_economic_tuning_mirror_value_f32_text = state + .world_restore + .economic_tuning_mirror_value_f32_text + .clone(); + fields.world_restore_economic_tuning_lane_count = + state.world_restore.economic_tuning_lane_raw_u32.len(); + fields.world_restore_economic_tuning_lane_value_f32_text = state + .world_restore + .economic_tuning_lane_value_f32_text + .clone(); +} diff --git a/crates/rrt-runtime/src/summary/builders/world/mod.rs b/crates/rrt-runtime/src/summary/builders/world/mod.rs new file mode 100644 index 0000000..e0d8fb9 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/world/mod.rs @@ -0,0 +1,27 @@ +mod apply; +mod economy; +mod model; +mod overview; +mod policy_state; +mod restore_core; +mod selected_year; + +use crate::state::RuntimeState; + +pub(super) use apply::apply_world_summary_fields; +use economy::fill_world_economy_fields; +pub(super) use model::WorldSummaryFields; +use overview::fill_world_overview_fields; +use policy_state::fill_world_policy_state_fields; +use restore_core::fill_world_restore_core_fields; +use selected_year::fill_world_selected_year_fields; + +pub(super) fn build_world_summary_fields(state: &RuntimeState) -> WorldSummaryFields { + let mut fields = WorldSummaryFields::default(); + fill_world_overview_fields(&mut fields, state); + fill_world_restore_core_fields(&mut fields, state); + fill_world_policy_state_fields(&mut fields, state); + fill_world_economy_fields(&mut fields, state); + fill_world_selected_year_fields(&mut fields, state); + fields +} diff --git a/crates/rrt-runtime/src/summary/builders/world/model.rs b/crates/rrt-runtime/src/summary/builders/world/model.rs new file mode 100644 index 0000000..767e8b9 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/world/model.rs @@ -0,0 +1,84 @@ +#[derive(Default)] +pub(crate) struct WorldSummaryFields { + pub calendar_projection_source: Option, + pub calendar_projection_is_placeholder: bool, + pub world_flag_count: usize, + pub world_restore_selected_year_profile_lane: Option, + pub world_restore_campaign_scenario_enabled: Option, + pub world_restore_sandbox_enabled: Option, + pub world_restore_seed_tuple_written_from_raw_lane: Option, + pub world_restore_absolute_counter_requires_shell_context: Option, + pub world_restore_absolute_counter_reconstructible_from_save: Option, + pub world_restore_packed_year_word_raw_u16: Option, + pub world_restore_partial_year_progress_raw_u8: Option, + pub world_restore_current_calendar_tuple_word_raw_u32: Option, + pub world_restore_current_calendar_tuple_word_2_raw_u32: Option, + pub world_restore_absolute_counter_raw_u32: Option, + pub world_restore_absolute_counter_mirror_raw_u32: Option, + pub world_restore_disable_cargo_economy_special_condition_slot: Option, + pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: + Option, + pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option, + pub world_restore_disable_cargo_economy_special_condition_enabled: Option, + pub world_restore_use_bio_accelerator_cars_enabled: Option, + pub world_restore_use_wartime_cargos_enabled: Option, + pub world_restore_disable_train_crashes_enabled: Option, + pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option, + pub world_restore_ai_ignore_territories_at_startup_enabled: Option, + pub world_restore_limited_track_building_amount: Option, + pub world_restore_economic_status_code: Option, + pub world_restore_territory_access_cost: Option, + pub world_restore_linked_site_removal_follow_on_gate_raw_u8: Option, + pub world_restore_linked_site_removal_follow_on_gate_enabled: Option, + pub world_restore_auto_show_grade_during_track_lay_raw_u8: Option, + pub world_restore_starting_building_density_level_raw_u8: Option, + pub world_restore_post_text_building_density_growth_raw_u8: Option, + pub world_restore_leftover_simulation_time_accumulator_raw_u32: Option, + pub world_restore_leftover_simulation_time_accumulator_value_f32_text: Option, + pub world_restore_selected_year_lane_snapshot_raw_u8: Option, + pub world_restore_all_steam_locomotives_available_raw_u8: Option, + pub world_restore_all_steam_locomotives_available_enabled: Option, + pub world_restore_all_diesel_locomotives_available_raw_u8: Option, + pub world_restore_all_diesel_locomotives_available_enabled: Option, + pub world_restore_all_electric_locomotives_available_raw_u8: Option, + pub world_restore_all_electric_locomotives_available_enabled: Option, + pub world_restore_issue_37_value: Option, + pub world_restore_issue_38_value: Option, + pub world_restore_issue_39_value: Option, + pub world_restore_issue_3a_value: Option, + pub world_restore_issue_37_multiplier_raw_u32: Option, + pub world_restore_issue_37_multiplier_value_f32_text: Option, + pub world_restore_stock_issue_and_buyback_policy_raw_u8: Option, + pub world_restore_bond_issue_and_repayment_policy_raw_u8: Option, + pub world_restore_bankruptcy_policy_raw_u8: Option, + pub world_restore_dividend_policy_raw_u8: Option, + pub world_restore_building_density_growth_setting_raw_u32: Option, + pub world_restore_stock_issue_and_buyback_allowed: Option, + pub world_restore_bond_issue_and_repayment_allowed: Option, + pub world_restore_bankruptcy_allowed: Option, + pub world_restore_dividend_adjustment_allowed: Option, + pub world_restore_finance_neighborhood_count: usize, + pub world_restore_finance_neighborhood_labels: Vec, + pub world_restore_economic_tuning_mirror_raw_u32: Option, + pub world_restore_economic_tuning_mirror_value_f32_text: Option, + pub world_restore_economic_tuning_lane_count: usize, + pub world_restore_economic_tuning_lane_value_f32_text: Vec, + pub world_restore_cached_available_locomotive_rating_raw_u32: Option, + pub world_restore_cached_available_locomotive_rating_value_f32_text: Option, + pub world_restore_selected_year_bucket_scalar_raw_u32: Option, + pub world_restore_selected_year_bucket_scalar_value_f32_text: Option, + pub world_restore_selected_year_bucket_direct_lane_count: usize, + pub world_restore_selected_year_bucket_direct_lane_value_f32_text: Vec, + pub world_restore_selected_year_bucket_complement_lane_count: usize, + pub world_restore_selected_year_bucket_complement_lane_value_f32_text: Vec, + pub world_restore_selected_year_bucket_scaled_companion_lane_count: usize, + pub world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text: Vec, + pub world_restore_selected_year_gap_scalar_raw_u32: Option, + pub world_restore_selected_year_gap_scalar_value_f32_text: Option, + pub world_restore_absolute_counter_restore_kind: Option, + pub world_restore_absolute_counter_adjustment_context: Option, + pub metadata_count: usize, + pub company_count: usize, + pub active_company_count: usize, + pub company_market_state_owner_count: usize, +} diff --git a/crates/rrt-runtime/src/summary/builders/world/overview.rs b/crates/rrt-runtime/src/summary/builders/world/overview.rs new file mode 100644 index 0000000..f177f80 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/world/overview.rs @@ -0,0 +1,19 @@ +use super::model::WorldSummaryFields; +use crate::RuntimeState; + +pub(super) fn fill_world_overview_fields(fields: &mut WorldSummaryFields, state: &RuntimeState) { + fields.calendar_projection_source = state.metadata.get("save_slice.calendar_source").cloned(); + fields.calendar_projection_is_placeholder = state + .metadata + .get("save_slice.calendar_source") + .is_some_and(|value| value == "default-1830-placeholder"); + fields.world_flag_count = state.world_flags.len(); + fields.metadata_count = state.metadata.len(); + fields.company_count = state.companies.len(); + fields.active_company_count = state + .companies + .iter() + .filter(|company| company.active) + .count(); + fields.company_market_state_owner_count = state.service_state.company_market_state.len(); +} diff --git a/crates/rrt-runtime/src/summary/builders/world/policy_state.rs b/crates/rrt-runtime/src/summary/builders/world/policy_state.rs new file mode 100644 index 0000000..3e0b173 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/world/policy_state.rs @@ -0,0 +1,83 @@ +use super::model::WorldSummaryFields; +use crate::RuntimeState; + +pub(super) fn fill_world_policy_state_fields( + fields: &mut WorldSummaryFields, + state: &RuntimeState, +) { + fields.world_restore_disable_cargo_economy_special_condition_slot = state + .world_restore + .disable_cargo_economy_special_condition_slot; + fields.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save = state + .world_restore + .disable_cargo_economy_special_condition_reconstructible_from_save; + fields.world_restore_disable_cargo_economy_special_condition_write_side_grounded = state + .world_restore + .disable_cargo_economy_special_condition_write_side_grounded; + fields.world_restore_disable_cargo_economy_special_condition_enabled = state + .world_restore + .disable_cargo_economy_special_condition_enabled; + fields.world_restore_use_bio_accelerator_cars_enabled = + state.world_restore.use_bio_accelerator_cars_enabled; + fields.world_restore_use_wartime_cargos_enabled = + state.world_restore.use_wartime_cargos_enabled; + fields.world_restore_disable_train_crashes_enabled = + state.world_restore.disable_train_crashes_enabled; + fields.world_restore_disable_train_crashes_and_breakdowns_enabled = state + .world_restore + .disable_train_crashes_and_breakdowns_enabled; + fields.world_restore_ai_ignore_territories_at_startup_enabled = + state.world_restore.ai_ignore_territories_at_startup_enabled; + fields.world_restore_limited_track_building_amount = + state.world_restore.limited_track_building_amount; + fields.world_restore_economic_status_code = state.world_restore.economic_status_code; + fields.world_restore_territory_access_cost = state.world_restore.territory_access_cost; + fields.world_restore_linked_site_removal_follow_on_gate_raw_u8 = state + .world_restore + .linked_site_removal_follow_on_gate_raw_u8; + fields.world_restore_linked_site_removal_follow_on_gate_enabled = state + .world_restore + .linked_site_removal_follow_on_gate_enabled; + fields.world_restore_auto_show_grade_during_track_lay_raw_u8 = + state.world_restore.auto_show_grade_during_track_lay_raw_u8; + fields.world_restore_starting_building_density_level_raw_u8 = + state.world_restore.starting_building_density_level_raw_u8; + fields.world_restore_post_text_building_density_growth_raw_u8 = + state.world_restore.post_text_building_density_growth_raw_u8; + fields.world_restore_leftover_simulation_time_accumulator_raw_u32 = state + .world_restore + .leftover_simulation_time_accumulator_raw_u32; + fields.world_restore_leftover_simulation_time_accumulator_value_f32_text = state + .world_restore + .leftover_simulation_time_accumulator_value_f32_text + .clone(); + fields.world_restore_all_steam_locomotives_available_raw_u8 = + state.world_restore.all_steam_locomotives_available_raw_u8; + fields.world_restore_all_steam_locomotives_available_enabled = + state.world_restore.all_steam_locomotives_available_enabled; + fields.world_restore_all_diesel_locomotives_available_raw_u8 = + state.world_restore.all_diesel_locomotives_available_raw_u8; + fields.world_restore_all_diesel_locomotives_available_enabled = + state.world_restore.all_diesel_locomotives_available_enabled; + fields.world_restore_all_electric_locomotives_available_raw_u8 = state + .world_restore + .all_electric_locomotives_available_raw_u8; + fields.world_restore_all_electric_locomotives_available_enabled = state + .world_restore + .all_electric_locomotives_available_enabled; + fields.world_restore_stock_issue_and_buyback_policy_raw_u8 = + state.world_restore.stock_issue_and_buyback_policy_raw_u8; + fields.world_restore_bond_issue_and_repayment_policy_raw_u8 = + state.world_restore.bond_issue_and_repayment_policy_raw_u8; + fields.world_restore_bankruptcy_policy_raw_u8 = state.world_restore.bankruptcy_policy_raw_u8; + fields.world_restore_dividend_policy_raw_u8 = state.world_restore.dividend_policy_raw_u8; + fields.world_restore_building_density_growth_setting_raw_u32 = + state.world_restore.building_density_growth_setting_raw_u32; + fields.world_restore_stock_issue_and_buyback_allowed = + state.world_restore.stock_issue_and_buyback_allowed; + fields.world_restore_bond_issue_and_repayment_allowed = + state.world_restore.bond_issue_and_repayment_allowed; + fields.world_restore_bankruptcy_allowed = state.world_restore.bankruptcy_allowed; + fields.world_restore_dividend_adjustment_allowed = + state.world_restore.dividend_adjustment_allowed; +} diff --git a/crates/rrt-runtime/src/summary/builders/world/restore_core.rs b/crates/rrt-runtime/src/summary/builders/world/restore_core.rs new file mode 100644 index 0000000..3fcfd6a --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/world/restore_core.rs @@ -0,0 +1,35 @@ +use super::model::WorldSummaryFields; +use crate::RuntimeState; + +pub(super) fn fill_world_restore_core_fields( + fields: &mut WorldSummaryFields, + state: &RuntimeState, +) { + fields.world_restore_selected_year_profile_lane = + state.world_restore.selected_year_profile_lane; + fields.world_restore_campaign_scenario_enabled = state.world_restore.campaign_scenario_enabled; + fields.world_restore_sandbox_enabled = state.world_restore.sandbox_enabled; + fields.world_restore_seed_tuple_written_from_raw_lane = + state.world_restore.seed_tuple_written_from_raw_lane; + fields.world_restore_absolute_counter_requires_shell_context = + state.world_restore.absolute_counter_requires_shell_context; + fields.world_restore_absolute_counter_reconstructible_from_save = state + .world_restore + .absolute_counter_reconstructible_from_save; + fields.world_restore_packed_year_word_raw_u16 = state.world_restore.packed_year_word_raw_u16; + fields.world_restore_partial_year_progress_raw_u8 = + state.world_restore.partial_year_progress_raw_u8; + fields.world_restore_current_calendar_tuple_word_raw_u32 = + state.world_restore.current_calendar_tuple_word_raw_u32; + fields.world_restore_current_calendar_tuple_word_2_raw_u32 = + state.world_restore.current_calendar_tuple_word_2_raw_u32; + fields.world_restore_absolute_counter_raw_u32 = state.world_restore.absolute_counter_raw_u32; + fields.world_restore_absolute_counter_mirror_raw_u32 = + state.world_restore.absolute_counter_mirror_raw_u32; + fields.world_restore_absolute_counter_restore_kind = + state.world_restore.absolute_counter_restore_kind.clone(); + fields.world_restore_absolute_counter_adjustment_context = state + .world_restore + .absolute_counter_adjustment_context + .clone(); +} diff --git a/crates/rrt-runtime/src/summary/builders/world/selected_year.rs b/crates/rrt-runtime/src/summary/builders/world/selected_year.rs new file mode 100644 index 0000000..703ca97 --- /dev/null +++ b/crates/rrt-runtime/src/summary/builders/world/selected_year.rs @@ -0,0 +1,53 @@ +use super::model::WorldSummaryFields; +use crate::RuntimeState; + +pub(super) fn fill_world_selected_year_fields( + fields: &mut WorldSummaryFields, + state: &RuntimeState, +) { + fields.world_restore_selected_year_lane_snapshot_raw_u8 = + state.world_restore.selected_year_lane_snapshot_raw_u8; + fields.world_restore_cached_available_locomotive_rating_raw_u32 = state + .world_restore + .cached_available_locomotive_rating_raw_u32; + fields.world_restore_cached_available_locomotive_rating_value_f32_text = state + .world_restore + .cached_available_locomotive_rating_value_f32_text + .clone(); + fields.world_restore_selected_year_bucket_scalar_raw_u32 = + state.world_restore.selected_year_bucket_scalar_raw_u32; + fields.world_restore_selected_year_bucket_scalar_value_f32_text = state + .world_restore + .selected_year_bucket_scalar_value_f32_text + .clone(); + fields.world_restore_selected_year_bucket_direct_lane_count = state + .world_restore + .selected_year_bucket_direct_lane_raw_u32 + .len(); + fields.world_restore_selected_year_bucket_direct_lane_value_f32_text = state + .world_restore + .selected_year_bucket_direct_lane_value_f32_text + .clone(); + fields.world_restore_selected_year_bucket_complement_lane_count = state + .world_restore + .selected_year_bucket_complement_lane_raw_u32 + .len(); + fields.world_restore_selected_year_bucket_complement_lane_value_f32_text = state + .world_restore + .selected_year_bucket_complement_lane_value_f32_text + .clone(); + fields.world_restore_selected_year_bucket_scaled_companion_lane_count = state + .world_restore + .selected_year_bucket_scaled_companion_lane_raw_u32 + .len(); + fields.world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text = state + .world_restore + .selected_year_bucket_scaled_companion_lane_value_f32_text + .clone(); + fields.world_restore_selected_year_gap_scalar_raw_u32 = + state.world_restore.selected_year_gap_scalar_raw_u32; + fields.world_restore_selected_year_gap_scalar_value_f32_text = state + .world_restore + .selected_year_gap_scalar_value_f32_text + .clone(); +} diff --git a/crates/rrt-runtime/src/summary/mod.rs b/crates/rrt-runtime/src/summary/mod.rs new file mode 100644 index 0000000..e73fe2e --- /dev/null +++ b/crates/rrt-runtime/src/summary/mod.rs @@ -0,0 +1,16 @@ +mod builders; +mod model; + +pub use model::*; + +use crate::state::RuntimeState; +use builders::build_runtime_summary; + +impl RuntimeSummary { + pub fn from_state(state: &RuntimeState) -> Self { + build_runtime_summary(state) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/rrt-runtime/src/summary/model.rs b/crates/rrt-runtime/src/summary/model.rs new file mode 100644 index 0000000..24ff816 --- /dev/null +++ b/crates/rrt-runtime/src/summary/model.rs @@ -0,0 +1,308 @@ +use serde::{Deserialize, Serialize}; + +use crate::state::CalendarPoint; + +pub(super) fn raw_u32_to_f32_text(raw: u32) -> String { + format!("{:.6}", f32::from_bits(raw)) +} + +pub(super) fn raw_u64_to_f64_text(raw: u64) -> String { + format!("{:.6}", f64::from_bits(raw)) +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeSummary { + pub calendar: CalendarPoint, + pub calendar_projection_source: Option, + pub calendar_projection_is_placeholder: bool, + pub world_flag_count: usize, + pub world_restore_selected_year_profile_lane: Option, + pub world_restore_campaign_scenario_enabled: Option, + pub world_restore_sandbox_enabled: Option, + pub world_restore_seed_tuple_written_from_raw_lane: Option, + pub world_restore_absolute_counter_requires_shell_context: Option, + pub world_restore_absolute_counter_reconstructible_from_save: Option, + pub world_restore_packed_year_word_raw_u16: Option, + pub world_restore_partial_year_progress_raw_u8: Option, + pub world_restore_current_calendar_tuple_word_raw_u32: Option, + pub world_restore_current_calendar_tuple_word_2_raw_u32: Option, + pub world_restore_absolute_counter_raw_u32: Option, + pub world_restore_absolute_counter_mirror_raw_u32: Option, + pub world_restore_disable_cargo_economy_special_condition_slot: Option, + pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: + Option, + pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option, + pub world_restore_disable_cargo_economy_special_condition_enabled: Option, + pub world_restore_use_bio_accelerator_cars_enabled: Option, + pub world_restore_use_wartime_cargos_enabled: Option, + pub world_restore_disable_train_crashes_enabled: Option, + pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option, + pub world_restore_ai_ignore_territories_at_startup_enabled: Option, + pub world_restore_limited_track_building_amount: Option, + pub world_restore_economic_status_code: Option, + pub world_restore_territory_access_cost: Option, + pub world_restore_linked_site_removal_follow_on_gate_raw_u8: Option, + pub world_restore_linked_site_removal_follow_on_gate_enabled: Option, + pub world_restore_auto_show_grade_during_track_lay_raw_u8: Option, + pub world_restore_starting_building_density_level_raw_u8: Option, + pub world_restore_post_text_building_density_growth_raw_u8: Option, + pub world_restore_leftover_simulation_time_accumulator_raw_u32: Option, + pub world_restore_leftover_simulation_time_accumulator_value_f32_text: Option, + pub world_restore_selected_year_lane_snapshot_raw_u8: Option, + pub world_restore_all_steam_locomotives_available_raw_u8: Option, + pub world_restore_all_steam_locomotives_available_enabled: Option, + pub world_restore_all_diesel_locomotives_available_raw_u8: Option, + pub world_restore_all_diesel_locomotives_available_enabled: Option, + pub world_restore_all_electric_locomotives_available_raw_u8: Option, + pub world_restore_all_electric_locomotives_available_enabled: Option, + pub world_restore_issue_37_value: Option, + pub world_restore_issue_38_value: Option, + pub world_restore_issue_39_value: Option, + pub world_restore_issue_3a_value: Option, + pub world_restore_issue_37_multiplier_raw_u32: Option, + pub world_restore_issue_37_multiplier_value_f32_text: Option, + pub world_restore_stock_issue_and_buyback_policy_raw_u8: Option, + pub world_restore_bond_issue_and_repayment_policy_raw_u8: Option, + pub world_restore_bankruptcy_policy_raw_u8: Option, + pub world_restore_dividend_policy_raw_u8: Option, + pub world_restore_building_density_growth_setting_raw_u32: Option, + pub world_restore_stock_issue_and_buyback_allowed: Option, + pub world_restore_bond_issue_and_repayment_allowed: Option, + pub world_restore_bankruptcy_allowed: Option, + pub world_restore_dividend_adjustment_allowed: Option, + pub world_restore_finance_neighborhood_count: usize, + pub world_restore_finance_neighborhood_labels: Vec, + pub world_restore_economic_tuning_mirror_raw_u32: Option, + pub world_restore_economic_tuning_mirror_value_f32_text: Option, + pub world_restore_economic_tuning_lane_count: usize, + pub world_restore_economic_tuning_lane_value_f32_text: Vec, + pub world_restore_cached_available_locomotive_rating_raw_u32: Option, + pub world_restore_cached_available_locomotive_rating_value_f32_text: Option, + pub world_restore_selected_year_bucket_scalar_raw_u32: Option, + pub world_restore_selected_year_bucket_scalar_value_f32_text: Option, + pub world_restore_selected_year_bucket_direct_lane_count: usize, + pub world_restore_selected_year_bucket_direct_lane_value_f32_text: Vec, + pub world_restore_selected_year_bucket_complement_lane_count: usize, + pub world_restore_selected_year_bucket_complement_lane_value_f32_text: Vec, + pub world_restore_selected_year_bucket_scaled_companion_lane_count: usize, + pub world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text: Vec, + pub world_restore_selected_year_gap_scalar_raw_u32: Option, + pub world_restore_selected_year_gap_scalar_value_f32_text: Option, + pub world_restore_absolute_counter_restore_kind: Option, + pub world_restore_absolute_counter_adjustment_context: Option, + pub metadata_count: usize, + pub company_count: usize, + pub active_company_count: usize, + pub company_market_state_owner_count: usize, + pub selected_company_outstanding_shares: Option, + pub selected_company_bond_count: Option, + pub selected_company_largest_live_bond_principal: Option, + pub selected_company_highest_coupon_live_bond_principal: Option, + pub selected_company_assigned_share_pool: Option, + pub selected_company_unassigned_share_pool: Option, + pub selected_company_cached_share_price: Option, + pub selected_company_cached_share_price_value_f32_text: Option, + pub selected_company_recent_per_share_cache_absolute_counter: Option, + pub selected_company_recent_per_share_cached_value_f64_text: Option, + pub selected_company_recent_per_share_subscore_value_f32_text: Option, + pub selected_company_mutable_support_scalar_value_f32_text: Option, + pub selected_company_stat_band_root_0cfb_count: usize, + pub selected_company_stat_band_root_0d7f_count: usize, + pub selected_company_stat_band_root_1c47_count: usize, + pub selected_company_last_dividend_year: Option, + pub selected_company_years_since_founding: Option, + pub selected_company_years_since_last_bankruptcy: Option, + pub selected_company_years_since_last_dividend: Option, + pub selected_company_current_partial_year_weight_numerator: Option, + pub selected_company_current_issue_absolute_counter: Option, + pub selected_company_prior_issue_absolute_counter: Option, + pub selected_company_current_issue_age_absolute_counter_delta: Option, + pub selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8: Option, + pub selected_company_periodic_side_latch_city_connection_latch: Option, + pub selected_company_periodic_side_latch_linked_transit_latch: Option, + #[serde(default)] + pub selected_company_linked_transit_route_anchor_entry_id: Option, + #[serde(default)] + pub selected_company_linked_transit_route_anchor_fallback_counts: Vec, + pub selected_company_periodic_service_base_route_preference_raw_u8: Option, + pub selected_company_periodic_service_effective_route_preference_raw_u8: Option, + pub selected_company_periodic_service_electric_route_preference_override_active: Option, + pub selected_company_periodic_service_route_quality_multiplier_basis_points: Option, + pub active_periodic_route_preference_override_company_id: Option, + pub active_periodic_route_preference_override_effective_raw_u8: Option, + pub last_periodic_route_preference_override_company_id: Option, + pub last_periodic_route_preference_override_effective_raw_u8: Option, + pub selected_company_chairman_bonus_year: Option, + pub selected_company_chairman_bonus_amount: Option, + pub selected_company_creditor_pressure_recent_bad_net_profit_year_count: Option, + pub selected_company_creditor_pressure_recent_peak_revenue: Option, + pub selected_company_creditor_pressure_recent_three_year_net_profit_total: Option, + pub selected_company_creditor_pressure_cash_floor: Option, + pub selected_company_creditor_pressure_cash_plus_slot_12_total: Option, + pub selected_company_creditor_pressure_share_price_floor: Option, + pub selected_company_creditor_pressure_share_price_scalar: Option, + pub selected_company_creditor_pressure_current_fuel_cost: Option, + pub selected_company_creditor_pressure_current_fuel_cost_floor: Option, + pub selected_company_creditor_pressure_eligible_for_bankruptcy_branch: Option, + pub selected_company_deep_distress_current_cash: Option, + pub selected_company_deep_distress_recent_first_three_net_profit_years: Vec, + pub selected_company_deep_distress_cash_floor: Option, + pub selected_company_deep_distress_net_profit_floor: Option, + pub selected_company_deep_distress_eligible_for_bankruptcy_fallback: Option, + pub selected_company_annual_bond_linked_transit_latch: Option, + pub selected_company_annual_bond_live_bond_count: Option, + pub selected_company_annual_bond_live_bond_principal_total: Option, + pub selected_company_annual_bond_matured_live_bond_count: Option, + pub selected_company_annual_bond_matured_live_bond_principal_total: Option, + pub selected_company_annual_bond_next_live_bond_maturity_year: Option, + pub selected_company_annual_bond_live_bond_coupon_burden_total: Option, + pub selected_company_annual_bond_current_cash: Option, + pub selected_company_annual_bond_cash_after_full_repayment: Option, + pub selected_company_annual_bond_issue_cash_floor: Option, + pub selected_company_annual_bond_issue_principal_step: Option, + pub selected_company_annual_bond_proposed_issue_bond_count: Option, + pub selected_company_annual_bond_proposed_issue_total_principal: Option, + pub selected_company_annual_bond_proposed_issue_years_to_maturity: Option, + pub selected_company_annual_bond_eligible_for_issue_branch: Option, + pub selected_company_stock_repurchase_city_connection_latch: Option, + pub selected_company_stock_repurchase_building_density_growth_setting: Option, + pub selected_company_stock_repurchase_linked_chairman_profile_id: Option, + pub selected_company_stock_repurchase_linked_chairman_personality_raw_u8: Option, + pub selected_company_stock_repurchase_batch_size: Option, + pub selected_company_stock_repurchase_factor_basis_points: Option, + pub selected_company_stock_repurchase_current_cash: Option, + pub selected_company_stock_repurchase_stock_value_gate_cash_floor: Option, + pub selected_company_stock_repurchase_support_adjusted_share_price_scalar: Option, + pub selected_company_stock_repurchase_affordability_cash_floor: Option, + pub selected_company_stock_repurchase_unassigned_share_pool: Option, + pub selected_company_stock_repurchase_eligible_for_single_batch: Option, + pub selected_company_stock_issue_live_bond_count: Option, + pub selected_company_stock_issue_initial_batch_size: Option, + pub selected_company_stock_issue_trimmed_batch_size: Option, + pub selected_company_stock_issue_share_pressure_basis_points: Option, + pub selected_company_stock_issue_pressured_share_price_scalar: Option, + pub selected_company_stock_issue_pressured_proceeds: Option, + pub selected_company_stock_issue_book_value_per_share_floor_applied: Option, + pub selected_company_stock_issue_price_to_book_ratio_basis_points: Option, + pub selected_company_stock_issue_current_cash: Option, + pub selected_company_stock_issue_highest_coupon_live_bond_principal: Option, + pub selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points: Option, + pub selected_company_stock_issue_current_issue_age_absolute_counter_delta: Option, + pub selected_company_stock_issue_current_issue_cooldown_floor: Option, + pub selected_company_stock_issue_minimum_price_to_book_ratio_basis_points: Option, + pub selected_company_stock_issue_passes_share_price_floor: Option, + pub selected_company_stock_issue_passes_proceeds_floor: Option, + pub selected_company_stock_issue_passes_cash_gate: Option, + pub selected_company_stock_issue_passes_issue_cooldown_gate: Option, + pub selected_company_stock_issue_passes_coupon_price_to_book_gate: Option, + pub selected_company_stock_issue_eligible_for_double_tranche: Option, + pub selected_company_dividend_weighted_recent_net_profit_total: Option, + pub selected_company_dividend_weighted_recent_net_profit_average: Option, + pub selected_company_dividend_current_cash: Option, + pub selected_company_dividend_tiny_unassigned_share_cash_supplement_branch: Option, + pub selected_company_dividend_tentative_target_per_share_tenths: Option, + pub selected_company_dividend_current_per_share_tenths: Option, + pub selected_company_dividend_growth_adjusted_current_per_share_tenths: Option, + pub selected_company_dividend_board_approved_ceiling_tenths: Option, + pub selected_company_dividend_proposed_per_share_tenths: Option, + pub selected_company_dividend_eligible_for_adjustment_branch: Option, + pub selected_company_annual_finance_policy_action: Option, + pub selected_company_annual_finance_news_family_candidate: Option, + pub selected_company_annual_finance_last_news_selector: Option, + pub annual_finance_last_news_event_count: usize, + pub selected_company_annual_finance_policy_creditor_pressure_bankruptcy_eligible: Option, + pub selected_company_annual_finance_policy_deep_distress_bankruptcy_fallback_eligible: + Option, + pub selected_company_annual_finance_policy_bond_issue_eligible: Option, + pub selected_company_annual_finance_policy_stock_repurchase_eligible: Option, + pub selected_company_annual_finance_policy_stock_issue_eligible: Option, + pub selected_company_annual_finance_policy_dividend_adjustment_eligible: Option, + pub player_count: usize, + pub chairman_profile_count: usize, + pub active_chairman_profile_count: usize, + pub selected_chairman_profile_id: Option, + pub linked_chairman_company_count: usize, + pub company_takeover_cooldown_count: usize, + pub company_merger_cooldown_count: usize, + pub train_count: usize, + pub active_train_count: usize, + pub retired_train_count: usize, + pub locomotive_catalog_count: usize, + pub cargo_catalog_count: usize, + pub territory_count: usize, + pub company_territory_track_count: usize, + pub packed_event_collection_present: bool, + pub packed_event_record_count: usize, + pub packed_event_decoded_record_count: usize, + pub packed_event_imported_runtime_record_count: usize, + pub packed_event_parity_only_record_count: usize, + pub packed_event_unsupported_record_count: usize, + pub packed_event_blocked_missing_company_context_count: usize, + pub packed_event_blocked_missing_selection_context_count: usize, + pub packed_event_blocked_missing_company_role_context_count: usize, + pub packed_event_blocked_missing_player_context_count: usize, + pub packed_event_blocked_missing_player_selection_context_count: usize, + pub packed_event_blocked_missing_player_role_context_count: usize, + pub packed_event_blocked_missing_chairman_context_count: usize, + pub packed_event_blocked_chairman_target_scope_count: usize, + pub packed_event_blocked_missing_condition_context_count: usize, + pub packed_event_blocked_missing_player_condition_context_count: usize, + pub packed_event_blocked_company_condition_scope_disabled_count: usize, + pub packed_event_blocked_player_condition_scope_count: usize, + pub packed_event_blocked_territory_condition_scope_count: usize, + pub packed_event_blocked_missing_territory_context_count: usize, + pub packed_event_blocked_named_territory_binding_count: usize, + pub packed_event_blocked_unmapped_ordinary_condition_count: usize, + pub packed_event_blocked_unmapped_world_condition_count: usize, + pub packed_event_blocked_missing_compact_control_count: usize, + pub packed_event_blocked_shell_owned_descriptor_count: usize, + pub packed_event_blocked_evidence_blocked_descriptor_count: usize, + pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: usize, + pub packed_event_blocked_unmapped_real_descriptor_count: usize, + pub packed_event_blocked_unmapped_world_descriptor_count: usize, + pub packed_event_blocked_territory_access_variant_count: usize, + pub packed_event_blocked_territory_access_scope_count: usize, + pub packed_event_blocked_missing_train_context_count: usize, + pub packed_event_blocked_missing_train_territory_context_count: usize, + pub packed_event_blocked_missing_locomotive_catalog_context_count: usize, + pub packed_event_blocked_confiscation_variant_count: usize, + pub packed_event_blocked_retire_train_variant_count: usize, + pub packed_event_blocked_retire_train_scope_count: usize, + pub packed_event_blocked_structural_only_count: usize, + pub event_runtime_record_count: usize, + pub candidate_availability_count: usize, + pub zero_candidate_availability_count: usize, + pub named_locomotive_availability_count: usize, + pub zero_named_locomotive_availability_count: usize, + pub named_locomotive_cost_count: usize, + pub cargo_production_override_count: usize, + pub world_runtime_variable_count: usize, + pub company_runtime_variable_owner_count: usize, + pub player_runtime_variable_owner_count: usize, + pub territory_runtime_variable_owner_count: usize, + pub world_scalar_override_count: usize, + pub special_condition_count: usize, + pub enabled_special_condition_count: usize, + pub save_profile_kind: Option, + pub save_profile_family: Option, + pub save_profile_map_path: Option, + pub save_profile_display_name: Option, + pub save_profile_selected_year_profile_lane: Option, + pub save_profile_sandbox_enabled: Option, + pub save_profile_campaign_scenario_enabled: Option, + pub save_profile_staged_profile_copy_on_restore: Option, + pub total_event_record_service_count: u64, + pub periodic_boundary_call_count: u64, + pub annual_finance_service_call_count: u64, + pub periodic_route_preference_override_apply_count: u64, + pub periodic_route_preference_override_restore_count: u64, + pub annual_dividend_adjustment_commit_count: u64, + pub annual_bond_last_retired_principal_total: u64, + pub annual_bond_last_issued_principal_total: u64, + pub annual_bond_last_principal_flow_relation: Option, + pub annual_stock_repurchase_last_share_count: u64, + pub annual_stock_issue_last_share_count: u64, + pub total_trigger_dispatch_count: u64, + pub dirty_rerun_count: u64, + pub total_company_cash: i64, +} diff --git a/crates/rrt-runtime/src/summary/tests.rs b/crates/rrt-runtime/src/summary/tests.rs new file mode 100644 index 0000000..146b6c9 --- /dev/null +++ b/crates/rrt-runtime/src/summary/tests.rs @@ -0,0 +1,2495 @@ +use std::collections::BTreeMap; + +use crate::event::metrics::*; +use crate::event::news::*; +use crate::event::packed::*; +use crate::state::*; +use crate::{CalendarPoint, RuntimeState}; + +use super::RuntimeSummary; + +#[test] +fn counts_structural_only_and_missing_context_frontiers() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + auto_show_grade_during_track_lay_raw_u8: Some(2), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(1), + trains: Vec::new(), + locomotive_catalog: vec![ + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 10, + name: "Big Boy 4-8-8-4".to_string(), + }, + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 58, + name: "VL80T".to_string(), + }, + ], + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: Some(RuntimePackedEventCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 11, + live_record_count: 5, + live_entry_ids: vec![3, 7, 9, 10, 11], + decoded_record_count: 5, + imported_runtime_record_count: 0, + records: vec![ + RuntimePackedEventRecordSummary { + record_index: 0, + live_entry_id: 3, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_missing_compact_control".to_string()), + notes: Vec::new(), + }, + RuntimePackedEventRecordSummary { + record_index: 1, + live_entry_id: 7, + payload_offset: Some(0x7262), + payload_len: Some(48), + decode_status: "parity_only".to_string(), + payload_family: "synthetic_harness".to_string(), + trigger_kind: Some(7), + active: Some(true), + marks_collection_dirty: Some(false), + one_shot: Some(false), + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_missing_company_context".to_string()), + notes: Vec::new(), + }, + RuntimePackedEventRecordSummary { + record_index: 2, + live_entry_id: 9, + payload_offset: Some(0x7292), + payload_len: Some(48), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_company_condition_scope_disabled".to_string()), + notes: Vec::new(), + }, + RuntimePackedEventRecordSummary { + record_index: 3, + live_entry_id: 10, + payload_offset: Some(0x72c2), + payload_len: Some(48), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_player_condition_scope".to_string()), + notes: Vec::new(), + }, + RuntimePackedEventRecordSummary { + record_index: 4, + live_entry_id: 11, + payload_offset: Some(0x72f2), + payload_len: Some(48), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_territory_condition_scope".to_string()), + notes: Vec::new(), + }, + ], + }), + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.packed_event_blocked_missing_compact_control_count, + 1 + ); + assert_eq!( + summary.packed_event_blocked_unmapped_real_descriptor_count, + 0 + ); + assert_eq!(summary.packed_event_blocked_structural_only_count, 0); + assert_eq!( + summary.packed_event_blocked_missing_company_context_count, + 1 + ); + assert_eq!( + summary.packed_event_blocked_missing_selection_context_count, + 0 + ); + assert_eq!( + summary.packed_event_blocked_missing_company_role_context_count, + 0 + ); + assert_eq!( + summary.packed_event_blocked_missing_condition_context_count, + 0 + ); + assert_eq!( + summary.packed_event_blocked_company_condition_scope_disabled_count, + 1 + ); + assert_eq!(summary.packed_event_blocked_player_condition_scope_count, 1); + assert_eq!( + summary.packed_event_blocked_territory_condition_scope_count, + 1 + ); +} + +#[test] +fn summarizes_save_world_issue_and_economic_tuning_restore_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(0x0201), + partial_year_progress_raw_u8: Some(3), + current_calendar_tuple_word_raw_u32: Some(0x0108_0210), + current_calendar_tuple_word_2_raw_u32: Some(0x35e6_3160), + absolute_counter_raw_u32: Some(5), + absolute_counter_mirror_raw_u32: Some(5), + issue_37_value: Some(3), + issue_38_value: Some(1), + issue_39_value: Some(2), + issue_3a_value: Some(4), + issue_37_multiplier_raw_u32: Some(0x3d75c28f), + issue_37_multiplier_value_f32_text: Some("0.06".to_string()), + stock_issue_and_buyback_policy_raw_u8: Some(0), + bond_issue_and_repayment_policy_raw_u8: Some(1), + bankruptcy_policy_raw_u8: Some(0), + dividend_policy_raw_u8: Some(1), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_allowed: Some(false), + bankruptcy_allowed: Some(true), + dividend_adjustment_allowed: Some(false), + economic_tuning_mirror_raw_u32: Some(0x3f46dff5), + economic_tuning_mirror_value_f32_text: Some("0.7766201".to_string()), + economic_tuning_lane_raw_u32: vec![ + 0x3f400000, 0x3be56042, 0x3c03126f, 0x3c1374bc, 0x3c23d70a, 0x3c23d70a, + ], + economic_tuning_lane_value_f32_text: vec![ + "0.75".to_string(), + "0.007".to_string(), + "0.008".to_string(), + "0.009".to_string(), + "0.01".to_string(), + "0.01".to_string(), + ], + linked_site_removal_follow_on_gate_raw_u8: Some(1), + linked_site_removal_follow_on_gate_enabled: Some(true), + auto_show_grade_during_track_lay_raw_u8: Some(2), + starting_building_density_level_raw_u8: Some(3), + post_text_building_density_growth_raw_u8: Some(1), + leftover_simulation_time_accumulator_raw_u32: Some(0x3f000000), + leftover_simulation_time_accumulator_value_f32_text: Some("0.500000".to_string()), + selected_year_lane_snapshot_raw_u8: Some(7), + all_steam_locomotives_available_raw_u8: Some(1), + all_steam_locomotives_available_enabled: Some(true), + all_diesel_locomotives_available_raw_u8: Some(0), + all_diesel_locomotives_available_enabled: Some(false), + all_electric_locomotives_available_raw_u8: Some(1), + all_electric_locomotives_available_enabled: Some(true), + cached_available_locomotive_rating_raw_u32: Some(0x41a00000), + cached_available_locomotive_rating_value_f32_text: Some("20.000000".to_string()), + selected_year_bucket_scalar_raw_u32: Some(25.0f32.to_bits()), + selected_year_bucket_scalar_value_f32_text: Some("25.000000".to_string()), + selected_year_bucket_direct_lane_raw_u32: vec![ + 22.5f32.to_bits(), + 26.25f32.to_bits(), + 17.5f32.to_bits(), + ], + selected_year_bucket_direct_lane_value_f32_text: vec![ + "22.500000".to_string(), + "26.250000".to_string(), + "17.500000".to_string(), + ], + selected_year_bucket_complement_lane_raw_u32: vec![ + 0.999121f32.to_bits(), + 0.998998f32.to_bits(), + 0.999210f32.to_bits(), + ], + selected_year_bucket_complement_lane_value_f32_text: vec![ + "0.999121".to_string(), + "0.998998".to_string(), + "0.999210".to_string(), + ], + selected_year_bucket_scaled_companion_lane_raw_u32: vec![ + 139.16667f32.to_bits(), + 122.5f32.to_bits(), + 171.42857f32.to_bits(), + ], + selected_year_bucket_scaled_companion_lane_value_f32_text: vec![ + "139.166672".to_string(), + "122.500000".to_string(), + "171.428574".to_string(), + ], + selected_year_gap_scalar_raw_u32: Some(0x3eaaaaab), + selected_year_gap_scalar_value_f32_text: Some("0.333333".to_string()), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(1), + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_production_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + + assert_eq!(summary.world_restore_packed_year_word_raw_u16, Some(0x0201)); + assert_eq!(summary.world_restore_partial_year_progress_raw_u8, Some(3)); + assert_eq!( + summary.world_restore_current_calendar_tuple_word_raw_u32, + Some(0x0108_0210) + ); + assert_eq!( + summary.world_restore_current_calendar_tuple_word_2_raw_u32, + Some(0x35e6_3160) + ); + assert_eq!(summary.world_restore_absolute_counter_raw_u32, Some(5)); + assert_eq!( + summary.world_restore_absolute_counter_mirror_raw_u32, + Some(5) + ); + assert_eq!(summary.world_restore_issue_37_value, Some(3)); + assert_eq!(summary.world_restore_issue_38_value, Some(1)); + assert_eq!(summary.world_restore_issue_39_value, Some(2)); + assert_eq!(summary.world_restore_issue_3a_value, Some(4)); + assert_eq!( + summary.world_restore_issue_37_multiplier_raw_u32, + Some(0x3d75c28f) + ); + assert_eq!( + summary.world_restore_stock_issue_and_buyback_policy_raw_u8, + Some(0) + ); + assert_eq!( + summary.world_restore_bond_issue_and_repayment_policy_raw_u8, + Some(1) + ); + assert_eq!(summary.world_restore_bankruptcy_policy_raw_u8, Some(0)); + assert_eq!(summary.world_restore_dividend_policy_raw_u8, Some(1)); + assert_eq!( + summary.world_restore_stock_issue_and_buyback_allowed, + Some(true) + ); + assert_eq!( + summary.world_restore_bond_issue_and_repayment_allowed, + Some(false) + ); + assert_eq!(summary.world_restore_bankruptcy_allowed, Some(true)); + assert_eq!( + summary.world_restore_dividend_adjustment_allowed, + Some(false) + ); + assert_eq!( + summary + .world_restore_issue_37_multiplier_value_f32_text + .as_deref(), + Some("0.06") + ); + assert_eq!( + summary.world_restore_economic_tuning_mirror_raw_u32, + Some(0x3f46dff5) + ); + assert_eq!( + summary + .world_restore_economic_tuning_mirror_value_f32_text + .as_deref(), + Some("0.7766201") + ); + assert_eq!( + summary.world_restore_linked_site_removal_follow_on_gate_raw_u8, + Some(1) + ); + assert_eq!( + summary.world_restore_linked_site_removal_follow_on_gate_enabled, + Some(true) + ); + assert_eq!( + summary.world_restore_auto_show_grade_during_track_lay_raw_u8, + Some(2) + ); + assert_eq!( + summary.world_restore_starting_building_density_level_raw_u8, + Some(3) + ); + assert_eq!( + summary.world_restore_post_text_building_density_growth_raw_u8, + Some(1) + ); + assert_eq!( + summary.world_restore_leftover_simulation_time_accumulator_raw_u32, + Some(0x3f000000) + ); + assert_eq!( + summary + .world_restore_leftover_simulation_time_accumulator_value_f32_text + .as_deref(), + Some("0.500000") + ); + assert_eq!( + summary.world_restore_selected_year_lane_snapshot_raw_u8, + Some(7) + ); + assert_eq!( + summary.world_restore_all_steam_locomotives_available_raw_u8, + Some(1) + ); + assert_eq!( + summary.world_restore_all_steam_locomotives_available_enabled, + Some(true) + ); + assert_eq!( + summary.world_restore_all_diesel_locomotives_available_raw_u8, + Some(0) + ); + assert_eq!( + summary.world_restore_all_diesel_locomotives_available_enabled, + Some(false) + ); + assert_eq!( + summary.world_restore_all_electric_locomotives_available_raw_u8, + Some(1) + ); + assert_eq!( + summary.world_restore_all_electric_locomotives_available_enabled, + Some(true) + ); + assert_eq!( + summary.world_restore_cached_available_locomotive_rating_raw_u32, + Some(0x41a00000) + ); + assert_eq!( + summary + .world_restore_cached_available_locomotive_rating_value_f32_text + .as_deref(), + Some("20.000000") + ); + assert_eq!( + summary.world_restore_selected_year_bucket_scalar_raw_u32, + Some(25.0f32.to_bits()) + ); + assert_eq!( + summary + .world_restore_selected_year_bucket_scalar_value_f32_text + .as_deref(), + Some("25.000000") + ); + assert_eq!( + summary.world_restore_selected_year_bucket_direct_lane_count, + 3 + ); + assert_eq!( + summary.world_restore_selected_year_bucket_direct_lane_value_f32_text, + vec!["22.500000", "26.250000", "17.500000"] + ); + assert_eq!( + summary.world_restore_selected_year_bucket_complement_lane_count, + 3 + ); + assert_eq!( + summary.world_restore_selected_year_bucket_complement_lane_value_f32_text, + vec!["0.999121", "0.998998", "0.999210"] + ); + assert_eq!( + summary.world_restore_selected_year_bucket_scaled_companion_lane_count, + 3 + ); + assert_eq!( + summary.world_restore_selected_year_bucket_scaled_companion_lane_value_f32_text, + vec!["139.166672", "122.500000", "171.428574"] + ); + assert_eq!(summary.world_restore_economic_tuning_lane_count, 6); + assert_eq!( + summary.world_restore_economic_tuning_lane_value_f32_text, + vec!["0.75", "0.007", "0.008", "0.009", "0.01", "0.01"] + ); + assert_eq!( + summary.world_restore_selected_year_gap_scalar_raw_u32, + Some(0x3eaaaaab) + ); + assert_eq!( + summary + .world_restore_selected_year_gap_scalar_value_f32_text + .as_deref(), + Some("0.333333") + ); +} + +#[test] +fn counts_active_companies_separately_from_total_companies() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + auto_show_grade_during_track_lay_raw_u8: Some(2), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![ + RuntimeCompany { + company_id: 1, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }, + RuntimeCompany { + company_id: 2, + current_cash: 20, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: false, + available_track_laying_capacity: Some(7), + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Ai, + }, + ], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(1), + trains: Vec::new(), + locomotive_catalog: vec![ + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 10, + name: "Big Boy 4-8-8-4".to_string(), + }, + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 58, + name: "VL80T".to_string(), + }, + ], + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!(summary.company_count, 2); + assert_eq!(summary.active_company_count, 1); +} + +#[test] +fn counts_named_locomotive_availability_entries_and_zero_values() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + auto_show_grade_during_track_lay_raw_u8: Some(2), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: vec![ + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 10, + name: "Big Boy 4-8-8-4".to_string(), + }, + crate::state::RuntimeLocomotiveCatalogEntry { + locomotive_id: 58, + name: "VL80T".to_string(), + }, + ], + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::from([ + ("Big Boy".to_string(), 0), + ("GP7".to_string(), 1), + ("Mikado".to_string(), 0), + ]), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!(summary.locomotive_catalog_count, 2); + assert_eq!(summary.named_locomotive_availability_count, 3); + assert_eq!(summary.zero_named_locomotive_availability_count, 2); + assert_eq!(summary.named_locomotive_cost_count, 0); +} + +#[test] +fn counts_named_locomotive_cost_entries() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::from([ + ("Big Boy".to_string(), 250000), + ("GP7".to_string(), 175000), + ]), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + + assert_eq!(summary.named_locomotive_cost_count, 2); +} + +#[test] +fn counts_world_scalar_override_surfaces() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + territory_access_cost: Some(750000), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::from([(1, 125), (2, 250)]), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + + assert_eq!(summary.cargo_production_override_count, 2); + assert_eq!(summary.world_restore_territory_access_cost, Some(750000)); +} + +#[test] +fn counts_runtime_variable_surfaces() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: vec![RuntimePlayer { + player_id: 2, + current_cash: 0, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 3, + name: Some("East".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::from([(1, 9)]), + company_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(2, 11)]))]), + player_runtime_variables: BTreeMap::from([(2, BTreeMap::from([(3, 13)]))]), + territory_runtime_variables: BTreeMap::from([(3, BTreeMap::from([(4, 15)]))]), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + + assert_eq!(summary.world_runtime_variable_count, 1); + assert_eq!(summary.company_runtime_variable_owner_count, 1); + assert_eq!(summary.player_runtime_variable_owner_count, 1); + assert_eq!(summary.territory_runtime_variable_owner_count, 1); +} + +#[test] +fn counts_world_frontier_buckets_separately() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: Some(RuntimePackedEventCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 2, + live_record_count: 2, + live_entry_ids: vec![21, 22], + decoded_record_count: 2, + imported_runtime_record_count: 0, + records: vec![ + RuntimePackedEventRecordSummary { + record_index: 0, + live_entry_id: 21, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_unmapped_world_descriptor".to_string()), + notes: Vec::new(), + }, + RuntimePackedEventRecordSummary { + record_index: 1, + live_entry_id: 22, + payload_offset: Some(0x7242), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 1, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_unmapped_world_condition".to_string()), + notes: Vec::new(), + }, + ], + }), + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.packed_event_blocked_unmapped_world_descriptor_count, + 1 + ); + assert_eq!( + summary.packed_event_blocked_unmapped_world_condition_count, + 1 + ); +} + +#[test] +fn counts_missing_locomotive_catalog_context_frontier() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: Some(RuntimePackedEventCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 1, + live_record_count: 1, + live_entry_ids: vec![1], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![RuntimePackedEventRecordSummary { + record_index: 0, + live_entry_id: 1, + payload_offset: Some(0x7202), + payload_len: Some(96), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_missing_locomotive_catalog_context".to_string()), + notes: Vec::new(), + }], + }), + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.packed_event_blocked_missing_locomotive_catalog_context_count, + 1 + ); +} + +#[test] +fn counts_shell_owned_descriptor_frontier() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: Some(RuntimePackedEventCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 1, + live_record_count: 1, + live_entry_ids: vec![1], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![RuntimePackedEventRecordSummary { + record_index: 0, + live_entry_id: 1, + payload_offset: Some(0), + payload_len: Some(0), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_shell_owned_descriptor".to_string()), + notes: Vec::new(), + }], + }), + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!(summary.packed_event_blocked_shell_owned_descriptor_count, 1); +} + +#[test] +fn summarizes_selected_company_market_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + auto_show_grade_during_track_lay_raw_u8: Some(2), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(1), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(1), + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 2, + live_bond_slots: Vec::new(), + highest_coupon_live_bond_principal: Some(350_000), + largest_live_bond_principal: Some(500_000), + mutable_support_scalar_raw_u32: 0x3f800000, + young_company_support_scalar_raw_u32: 0x42340000, + support_progress_word: 12, + recent_per_share_cache_absolute_counter: 42, + recent_per_share_cached_value_bits: 14.5f64.to_bits(), + recent_per_share_subscore_raw_u32: 0x420c0000, + cached_share_price_raw_u32: 0x42200000, + chairman_salary_baseline: 24, + chairman_salary_current: 30, + chairman_bonus_year: 1842, + chairman_bonus_amount: 750, + founding_year: 1831, + last_bankruptcy_year: 0, + last_dividend_year: 1841, + current_issue_calendar_word: 5, + current_issue_calendar_word_2: 6, + prior_issue_calendar_word: 4, + prior_issue_calendar_word_2: 5, + city_connection_latch: true, + linked_transit_latch: false, + linked_transit_route_anchor_entry_id: Some(77), + linked_transit_route_anchor_fallback_counts: vec![3, 5, 8], + stat_band_root_0cfb_candidates: vec![ + crate::state::RuntimeCompanyStatBandCandidate { + label: "stat_band_0cfb_word_1".to_string(), + relative_offset: 0x0cfb, + relative_offset_hex: "0xcfb".to_string(), + raw_u32: 1, + raw_u32_hex: "0x00000001".to_string(), + value_i32: 1, + value_f32_text: "0.000000".to_string(), + }, + crate::state::RuntimeCompanyStatBandCandidate { + label: "stat_band_0cfb_word_2".to_string(), + relative_offset: 0x0cff, + relative_offset_hex: "0xcff".to_string(), + raw_u32: 2, + raw_u32_hex: "0x00000002".to_string(), + value_i32: 2, + value_f32_text: "0.000000".to_string(), + }, + ], + stat_band_root_0d7f_candidates: vec![ + crate::state::RuntimeCompanyStatBandCandidate { + label: "stat_band_0d7f_word_1".to_string(), + relative_offset: 0x0d7f, + relative_offset_hex: "0xd7f".to_string(), + raw_u32: 3, + raw_u32_hex: "0x00000003".to_string(), + value_i32: 3, + value_f32_text: "0.000000".to_string(), + }, + ], + stat_band_root_1c47_candidates: vec![ + crate::state::RuntimeCompanyStatBandCandidate { + label: "stat_band_1c47_word_1".to_string(), + relative_offset: 0x1c47, + relative_offset_hex: "0x1c47".trim_start_matches("0x").to_string(), + raw_u32: 4, + raw_u32_hex: "0x00000004".to_string(), + value_i32: 4, + value_f32_text: "0.000000".to_string(), + }, + ], + year_stat_family_qword_bits: Vec::new(), + special_stat_family_232a_qword_bits: Vec::new(), + issue_opinion_terms_raw_i32: Vec::new(), + direct_control_transfer_float_fields_raw_u32: BTreeMap::new(), + direct_control_transfer_int_fields_raw_u32: BTreeMap::new(), + }, + )]), + company_periodic_side_latch_state: BTreeMap::from([( + 1, + crate::state::RuntimeCompanyPeriodicSideLatchState { + preferred_locomotive_engine_type_raw_u8: Some(2), + city_connection_latch: true, + linked_transit_latch: false, + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!(summary.company_market_state_owner_count, 1); + assert_eq!(summary.selected_company_outstanding_shares, Some(20_000)); + assert_eq!(summary.selected_company_bond_count, Some(2)); + assert_eq!( + summary.selected_company_largest_live_bond_principal, + Some(500_000) + ); + assert_eq!( + summary.selected_company_highest_coupon_live_bond_principal, + Some(350_000) + ); + assert_eq!(summary.selected_company_assigned_share_pool, Some(5_000)); + assert_eq!(summary.selected_company_unassigned_share_pool, Some(15_000)); + assert_eq!(summary.selected_company_cached_share_price, Some(40)); + assert_eq!( + summary.selected_company_cached_share_price_value_f32_text, + Some("40.000000".to_string()) + ); + assert_eq!( + summary.selected_company_recent_per_share_cache_absolute_counter, + Some(42) + ); + assert_eq!( + summary.selected_company_recent_per_share_cached_value_f64_text, + Some("14.500000".to_string()) + ); + assert_eq!( + summary.selected_company_recent_per_share_subscore_value_f32_text, + Some("35.000000".to_string()) + ); + assert_eq!( + summary.selected_company_mutable_support_scalar_value_f32_text, + Some("1.000000".to_string()) + ); + assert_eq!(summary.selected_company_stat_band_root_0cfb_count, 2); + assert_eq!(summary.selected_company_stat_band_root_0d7f_count, 1); + assert_eq!(summary.selected_company_stat_band_root_1c47_count, 1); + assert_eq!(summary.selected_company_last_dividend_year, Some(1841)); + assert_eq!(summary.selected_company_years_since_founding, None); + assert_eq!(summary.selected_company_years_since_last_bankruptcy, None); + assert_eq!(summary.selected_company_years_since_last_dividend, None); + assert_eq!( + summary.selected_company_periodic_side_latch_preferred_locomotive_engine_type_raw_u8, + Some(2) + ); + assert_eq!( + summary.selected_company_periodic_side_latch_city_connection_latch, + Some(true) + ); + assert_eq!( + summary.selected_company_periodic_side_latch_linked_transit_latch, + Some(false) + ); + assert_eq!( + summary.selected_company_linked_transit_route_anchor_entry_id, + Some(77) + ); + assert_eq!( + summary.selected_company_linked_transit_route_anchor_fallback_counts, + vec![3, 5, 8] + ); + assert_eq!( + summary.selected_company_periodic_service_base_route_preference_raw_u8, + Some(2) + ); + assert_eq!( + summary.selected_company_periodic_service_effective_route_preference_raw_u8, + Some(2) + ); + assert_eq!( + summary.selected_company_periodic_service_electric_route_preference_override_active, + Some(true) + ); + assert_eq!( + summary.selected_company_periodic_service_route_quality_multiplier_basis_points, + Some(180) + ); + assert_eq!( + summary.active_periodic_route_preference_override_company_id, + None + ); + assert_eq!( + summary.last_periodic_route_preference_override_company_id, + None + ); + assert_eq!(summary.periodic_route_preference_override_apply_count, 0); + assert_eq!(summary.periodic_route_preference_override_restore_count, 0); + assert_eq!(summary.selected_company_chairman_bonus_year, Some(1842)); + assert_eq!(summary.selected_company_chairman_bonus_amount, Some(750)); +} + +#[test] +fn summarizes_selected_company_creditor_pressure_branch_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x0d, -700_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x12, 0.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 80_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -115_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -110_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(7), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 7, + crate::state::RuntimeCompanyMarketState { + founding_year: 1841, + last_bankruptcy_year: 1832, + cached_share_price_raw_u32: 25.0f32.to_bits(), + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.selected_company_creditor_pressure_recent_bad_net_profit_year_count, + Some(3) + ); + assert_eq!( + summary.selected_company_creditor_pressure_recent_peak_revenue, + Some(100_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_recent_three_year_net_profit_total, + Some(-65_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_cash_floor, + Some(-600_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_cash_plus_slot_12_total, + Some(-700_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_share_price_floor, + Some(20) + ); + assert_eq!( + summary.selected_company_creditor_pressure_share_price_scalar, + Some(25) + ); + assert_eq!( + summary.selected_company_creditor_pressure_current_fuel_cost, + Some(-50_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_current_fuel_cost_floor, + Some(-48_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_eligible_for_bankruptcy_branch, + Some(true) + ); +} + +#[test] +fn summarizes_selected_company_deep_distress_fallback_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, -350_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 9, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(9), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 9, + crate::state::RuntimeCompanyMarketState { + founding_year: 1841, + last_bankruptcy_year: 1840, + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.selected_company_deep_distress_current_cash, + Some(-350_000) + ); + assert_eq!( + summary.selected_company_deep_distress_recent_first_three_net_profit_years, + vec![-25_000, -23_000, -21_000] + ); + assert_eq!( + summary.selected_company_deep_distress_cash_floor, + Some(-300_000) + ); + assert_eq!( + summary.selected_company_deep_distress_net_profit_floor, + Some(-20_000) + ); + assert_eq!( + summary.selected_company_deep_distress_eligible_for_bankruptcy_fallback, + Some(true) + ); +} + +#[test] +fn summarizes_selected_company_stock_repurchase_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 1_600_000.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + partial_year_progress_raw_u8: Some(0x0c), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(1), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 12, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(3), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(12), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 3, + name: "Jay".to_string(), + active: true, + current_cash: 200, + linked_company_id: Some(12), + company_holdings: BTreeMap::from([(12, 14_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: Vec::new().into_iter().collect(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 12, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + cached_share_price_raw_u32: 20.0f32.to_bits(), + founding_year: 1835, + city_connection_latch: true, + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + chairman_personality_raw_u8: BTreeMap::from([(3, 20)]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.world_restore_building_density_growth_setting_raw_u32, + Some(1) + ); + assert_eq!( + summary.selected_company_stock_repurchase_building_density_growth_setting, + Some(1) + ); + assert_eq!( + summary.selected_company_stock_repurchase_linked_chairman_personality_raw_u8, + Some(20) + ); + assert_eq!( + summary.selected_company_stock_repurchase_batch_size, + Some(1_000) + ); + assert_eq!( + summary.selected_company_stock_repurchase_factor_basis_points, + Some(432) + ); + assert_eq!( + summary.selected_company_stock_repurchase_current_cash, + Some(1_600_000) + ); + assert_eq!( + summary.selected_company_stock_repurchase_stock_value_gate_cash_floor, + Some(3_456_000) + ); + assert_eq!( + summary.selected_company_stock_repurchase_support_adjusted_share_price_scalar, + Some(20) + ); + assert_eq!( + summary.selected_company_stock_repurchase_affordability_cash_floor, + Some(103_680) + ); + assert_eq!( + summary.selected_company_stock_repurchase_unassigned_share_pool, + Some(5_500) + ); + assert_eq!( + summary.selected_company_stock_repurchase_eligible_for_single_batch, + Some(false) + ); +} + +#[test] +fn summarizes_selected_company_annual_bond_policy_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = (-400_000.0f64).to_bits(); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + partial_year_progress_raw_u8: Some(0x0c), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 11, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(11), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: Vec::new().into_iter().collect(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 11, + crate::state::RuntimeCompanyMarketState { + bond_count: 2, + linked_transit_latch: true, + live_bond_slots: vec![ + crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 200_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.09f32.to_bits(), + }, + crate::state::RuntimeCompanyBondSlot { + slot_index: 1, + principal: 150_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }, + ], + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.selected_company_annual_bond_linked_transit_latch, + Some(true) + ); + assert_eq!( + summary.selected_company_annual_bond_live_bond_count, + Some(2) + ); + assert_eq!( + summary.selected_company_annual_bond_live_bond_principal_total, + Some(350_000) + ); + assert_eq!( + summary.selected_company_annual_bond_matured_live_bond_count, + Some(0) + ); + assert_eq!( + summary.selected_company_annual_bond_matured_live_bond_principal_total, + Some(0) + ); + assert_eq!( + summary.selected_company_annual_bond_next_live_bond_maturity_year, + None + ); + assert_eq!( + summary.selected_company_annual_bond_live_bond_coupon_burden_total, + Some(30_000) + ); + assert_eq!( + summary.selected_company_annual_bond_current_cash, + Some(-400_000) + ); + assert_eq!( + summary.selected_company_annual_bond_cash_after_full_repayment, + Some(-750_000) + ); + assert_eq!( + summary.selected_company_annual_bond_issue_cash_floor, + Some(-30_000) + ); + assert_eq!( + summary.selected_company_annual_bond_issue_principal_step, + Some(500_000) + ); + assert_eq!( + summary.selected_company_annual_bond_proposed_issue_bond_count, + Some(2) + ); + assert_eq!( + summary.selected_company_annual_bond_proposed_issue_total_principal, + Some(1_000_000) + ); + assert_eq!( + summary.selected_company_annual_bond_proposed_issue_years_to_maturity, + Some(30) + ); + assert_eq!( + summary.selected_company_annual_bond_eligible_for_issue_branch, + Some(true) + ); +} + +#[test] +fn summarizes_selected_company_stock_issue_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 250_000.0f64.to_bits(); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState { + partial_year_progress_raw_u8: Some(0x0c), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + issue_37_value: Some(2), + issue_37_multiplier_raw_u32: Some(1.0f32.to_bits()), + issue_37_multiplier_value_f32_text: Some("1.000000".to_string()), + absolute_counter_raw_u32: Some(885_911_040), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 14, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(8), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(14), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 8, + name: "Taylor".to_string(), + active: true, + current_cash: 200, + linked_company_id: Some(14), + company_holdings: BTreeMap::from([(14, 14_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: Vec::new().into_iter().collect(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + chairman_personality_raw_u8: BTreeMap::from([(8, 20)]), + annual_finance_last_actions: BTreeMap::from([( + 14, + crate::state::RuntimeCompanyAnnualFinancePolicyAction::StockIssue, + )]), + annual_finance_last_news_family_candidates: BTreeMap::from([(14, "4053".to_string())]), + annual_finance_last_news_events: vec![RuntimeAnnualFinanceNewsEvent { + company_id: 14, + selector_label: "4053".to_string(), + action_label: "stock_issue".to_string(), + retired_principal_total: 0, + issued_principal_total: 0, + repurchased_share_count: 0, + issued_share_count: 4_000, + }], + company_market_state: BTreeMap::from([( + 14, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 2, + highest_coupon_live_bond_principal: Some(300_000), + current_issue_calendar_word: 0x0101_0725, + current_issue_calendar_word_2: 0x0001_0001, + founding_year: 1840, + cached_share_price_raw_u32: 35.0f32.to_bits(), + recent_per_share_cache_absolute_counter: 885_911_040, + recent_per_share_cached_value_bits: 34.0f64.to_bits(), + live_bond_slots: vec![ + crate::state::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 300_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.11f32.to_bits(), + }, + crate::state::RuntimeCompanyBondSlot { + slot_index: 1, + principal: 200_000, + maturity_year: 0, + coupon_rate_raw_u32: 0.07f32.to_bits(), + }, + ], + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x32f, + 30.0f32.to_bits(), + )]), + year_stat_family_qword_bits, + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.selected_company_stock_issue_live_bond_count, + Some(2) + ); + assert_eq!( + summary.selected_company_stock_issue_initial_batch_size, + Some(2_000) + ); + assert_eq!( + summary.selected_company_stock_issue_trimmed_batch_size, + Some(2_000) + ); + assert_eq!( + summary.selected_company_stock_issue_share_pressure_basis_points, + Some(-1_000) + ); + assert_eq!( + summary.selected_company_stock_issue_pressured_share_price_scalar, + Some(35) + ); + assert_eq!( + summary.selected_company_stock_issue_pressured_proceeds, + Some(70_000) + ); + assert_eq!( + summary.selected_company_stock_issue_book_value_per_share_floor_applied, + Some(30) + ); + assert_eq!( + summary.selected_company_stock_issue_price_to_book_ratio_basis_points, + Some(11_667) + ); + assert_eq!( + summary.selected_company_stock_issue_current_cash, + Some(250_000) + ); + assert_eq!( + summary.selected_company_stock_issue_highest_coupon_live_bond_principal, + Some(300_000) + ); + assert_eq!( + summary.selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points, + Some(1_100) + ); + assert_eq!( + summary.selected_company_stock_issue_current_issue_age_absolute_counter_delta, + Some(967_680) + ); + assert_eq!( + summary.selected_company_stock_issue_current_issue_cooldown_floor, + Some(483_840) + ); + assert_eq!( + summary.selected_company_stock_issue_minimum_price_to_book_ratio_basis_points, + Some(8_000) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_share_price_floor, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_proceeds_floor, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_cash_gate, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_issue_cooldown_gate, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_coupon_price_to_book_gate, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_eligible_for_double_tranche, + Some(true) + ); + assert_eq!( + summary + .selected_company_annual_finance_news_family_candidate + .as_deref(), + Some("4053") + ); + assert_eq!( + summary + .selected_company_annual_finance_last_news_selector + .as_deref(), + Some("4053") + ); + assert_eq!(summary.annual_finance_last_news_event_count, 1); +} + +#[test] +fn summarizes_selected_company_annual_dividend_policy_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); + + let state = RuntimeState { + calendar: CalendarPoint { + year: 1845, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::state::RuntimeSaveProfileState::default(), + world_restore: crate::state::RuntimeWorldRestoreState { + packed_year_word_raw_u16: Some(1845), + partial_year_progress_raw_u8: Some(0x0c), + dividend_policy_raw_u8: Some(0), + dividend_adjustment_allowed: Some(true), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(1), + ..crate::state::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::state::RuntimeCompany { + company_id: 15, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::state::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: crate::state::RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(15), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::state::RuntimeChairmanProfile { + profile_id: 3, + name: "Chairman Three".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(15), + company_holdings: BTreeMap::from([(15, 9_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: crate::state::RuntimeServiceState { + company_market_state: BTreeMap::from([( + 15, + crate::state::RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1840, + last_dividend_year: 1844, + year_stat_family_qword_bits, + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x33f, + 0.4f32.to_bits(), + )]), + ..crate::state::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::state::RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.selected_company_dividend_weighted_recent_net_profit_total, + Some(600_000) + ); + assert_eq!( + summary.selected_company_dividend_weighted_recent_net_profit_average, + Some(100_000) + ); + assert_eq!( + summary.selected_company_dividend_current_cash, + Some(300_000) + ); + assert_eq!( + summary.selected_company_dividend_tiny_unassigned_share_cash_supplement_branch, + Some(true) + ); + assert_eq!( + summary.selected_company_dividend_tentative_target_per_share_tenths, + Some(133) + ); + assert_eq!( + summary.selected_company_dividend_current_per_share_tenths, + Some(4) + ); + assert_eq!( + summary.selected_company_dividend_growth_adjusted_current_per_share_tenths, + Some(3) + ); + assert_eq!( + summary.selected_company_dividend_board_approved_ceiling_tenths, + Some(18) + ); + assert_eq!( + summary.selected_company_dividend_proposed_per_share_tenths, + Some(18) + ); + assert_eq!( + summary.selected_company_dividend_eligible_for_adjustment_branch, + Some(true) + ); + assert_eq!( + summary + .selected_company_annual_finance_policy_action + .as_deref(), + Some("dividend_adjustment") + ); + assert_eq!( + summary.selected_company_annual_finance_policy_dividend_adjustment_eligible, + Some(true) + ); +} diff --git a/crates/rrt-runtime/src/test_support/fs.rs b/crates/rrt-runtime/src/test_support/fs.rs new file mode 100644 index 0000000..a51ee4a --- /dev/null +++ b/crates/rrt-runtime/src/test_support/fs.rs @@ -0,0 +1,18 @@ +use std::path::PathBuf; + +use serde::Serialize; + +pub(crate) fn unique_temp_path(stem: &str, extension: &str) -> PathBuf { + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time should be after epoch") + .as_nanos(); + std::env::temp_dir().join(format!("rrt-{stem}-{nonce}.{extension}")) +} + +pub(crate) fn write_temp_json(stem: &str, value: &T) -> PathBuf { + let path = unique_temp_path(stem, "json"); + let bytes = serde_json::to_vec_pretty(value).expect("json serialization should succeed"); + std::fs::write(&path, bytes).expect("temp json should be written"); + path +} diff --git a/crates/rrt-runtime/src/test_support/mod.rs b/crates/rrt-runtime/src/test_support/mod.rs new file mode 100644 index 0000000..bd0614a --- /dev/null +++ b/crates/rrt-runtime/src/test_support/mod.rs @@ -0,0 +1,7 @@ +mod fs; +mod state; + +#[allow(unused_imports)] +pub(crate) use fs::*; +#[allow(unused_imports)] +pub(crate) use state::*; diff --git a/crates/rrt-runtime/src/test_support/state.rs b/crates/rrt-runtime/src/test_support/state.rs new file mode 100644 index 0000000..9029a18 --- /dev/null +++ b/crates/rrt-runtime/src/test_support/state.rs @@ -0,0 +1,56 @@ +use std::collections::BTreeMap; + +use crate::CalendarPoint; +use crate::state::{ + RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, +}; + +pub(crate) fn calendar_1830() -> CalendarPoint { + CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + } +} + +pub(crate) fn empty_runtime_state() -> RuntimeState { + RuntimeState { + calendar: calendar_1830(), + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: Vec::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + } +} diff --git a/crates/rrt-runtime/src/validation/documents.rs b/crates/rrt-runtime/src/validation/documents.rs new file mode 100644 index 0000000..7d69d7f --- /dev/null +++ b/crates/rrt-runtime/src/validation/documents.rs @@ -0,0 +1,108 @@ +use crate::documents::{ + OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument, RuntimeSaveSliceDocument, + RuntimeStateInputDocument, SAVE_SLICE_DOCUMENT_FORMAT_VERSION, + STATE_INPUT_DOCUMENT_FORMAT_VERSION, +}; + +pub fn validate_runtime_state_input_document( + document: &RuntimeStateInputDocument, +) -> Result<(), String> { + if document.format_version != STATE_INPUT_DOCUMENT_FORMAT_VERSION { + return Err(format!( + "unsupported runtime state input document format_version {} (expected {})", + document.format_version, STATE_INPUT_DOCUMENT_FORMAT_VERSION + )); + } + if document.input_id.trim().is_empty() { + return Err("input_id must not be empty".to_string()); + } + document.state.validate() +} + +pub fn validate_runtime_save_slice_document( + document: &RuntimeSaveSliceDocument, +) -> Result<(), String> { + if document.format_version != SAVE_SLICE_DOCUMENT_FORMAT_VERSION { + return Err(format!( + "unsupported save slice document format_version {} (expected {})", + document.format_version, SAVE_SLICE_DOCUMENT_FORMAT_VERSION + )); + } + if document.save_slice_id.trim().is_empty() { + return Err("save_slice_id must not be empty".to_string()); + } + if document + .source + .description + .as_deref() + .is_some_and(|text| text.trim().is_empty()) + { + return Err("save slice source.description must not be empty".to_string()); + } + if document + .source + .original_save_filename + .as_deref() + .is_some_and(|text| text.trim().is_empty()) + { + return Err("save slice source.original_save_filename must not be empty".to_string()); + } + if document + .source + .original_save_sha256 + .as_deref() + .is_some_and(|text| text.trim().is_empty()) + { + return Err("save slice source.original_save_sha256 must not be empty".to_string()); + } + for (index, note) in document.source.notes.iter().enumerate() { + if note.trim().is_empty() { + return Err(format!( + "save slice source.notes[{index}] must not be empty" + )); + } + } + if document.save_slice.mechanism_family.trim().is_empty() { + return Err("save_slice.mechanism_family must not be empty".to_string()); + } + if document.save_slice.mechanism_confidence.trim().is_empty() { + return Err("save_slice.mechanism_confidence must not be empty".to_string()); + } + Ok(()) +} + +pub fn validate_runtime_overlay_import_document( + document: &RuntimeOverlayImportDocument, +) -> Result<(), String> { + if document.format_version != OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION { + return Err(format!( + "unsupported overlay import document format_version {} (expected {})", + document.format_version, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION + )); + } + if document.import_id.trim().is_empty() { + return Err("import_id must not be empty".to_string()); + } + if document + .source + .description + .as_deref() + .is_some_and(|text| text.trim().is_empty()) + { + return Err("overlay import source.description must not be empty".to_string()); + } + for (index, note) in document.source.notes.iter().enumerate() { + if note.trim().is_empty() { + return Err(format!( + "overlay import source.notes[{index}] must not be empty" + )); + } + } + if document.base_snapshot_path.trim().is_empty() { + return Err("base_snapshot_path must not be empty".to_string()); + } + if document.save_slice_path.trim().is_empty() { + return Err("save_slice_path must not be empty".to_string()); + } + Ok(()) +} diff --git a/crates/rrt-runtime/src/validation/mod.rs b/crates/rrt-runtime/src/validation/mod.rs new file mode 100644 index 0000000..92adf77 --- /dev/null +++ b/crates/rrt-runtime/src/validation/mod.rs @@ -0,0 +1,5 @@ +mod documents; +mod runtime; + +pub use documents::*; +pub(crate) use runtime::*; diff --git a/crates/rrt-runtime/src/validation/runtime/conditions.rs b/crates/rrt-runtime/src/validation/runtime/conditions.rs new file mode 100644 index 0000000..d966d9b --- /dev/null +++ b/crates/rrt-runtime/src/validation/runtime/conditions.rs @@ -0,0 +1,98 @@ +use std::collections::BTreeSet; + +use crate::event::conditions::RuntimeCondition; + +use super::{ + validate_chairman_target, validate_company_target, validate_player_target, + validate_territory_target, +}; + +pub(crate) fn validate_runtime_condition( + condition: &RuntimeCondition, + valid_company_ids: &BTreeSet, + valid_player_ids: &BTreeSet, + valid_chairman_profile_ids: &BTreeSet, + valid_territory_ids: &BTreeSet, +) -> Result<(), String> { + match condition { + RuntimeCondition::WorldVariableThreshold { index, .. } => { + if !(1..=4).contains(index) { + Err("index must be in 1..=4".to_string()) + } else { + Ok(()) + } + } + RuntimeCondition::CompanyNumericThreshold { target, .. } => { + validate_company_target(target, valid_company_ids) + } + RuntimeCondition::CompanyVariableThreshold { target, index, .. } => { + validate_company_target(target, valid_company_ids)?; + if !(1..=4).contains(index) { + Err("index must be in 1..=4".to_string()) + } else { + Ok(()) + } + } + RuntimeCondition::ChairmanNumericThreshold { target, .. } => { + validate_chairman_target(target, valid_chairman_profile_ids) + } + RuntimeCondition::PlayerVariableThreshold { target, index, .. } => { + validate_player_target(target, valid_player_ids)?; + if !(1..=4).contains(index) { + Err("index must be in 1..=4".to_string()) + } else { + Ok(()) + } + } + RuntimeCondition::TerritoryNumericThreshold { target, .. } => { + validate_territory_target(target, valid_territory_ids) + } + RuntimeCondition::TerritoryVariableThreshold { target, index, .. } => { + validate_territory_target(target, valid_territory_ids)?; + if !(1..=4).contains(index) { + Err("index must be in 1..=4".to_string()) + } else { + Ok(()) + } + } + RuntimeCondition::CompanyTerritoryNumericThreshold { + target, territory, .. + } => { + validate_company_target(target, valid_company_ids)?; + validate_territory_target(territory, valid_territory_ids) + } + RuntimeCondition::SpecialConditionThreshold { label, .. } + | RuntimeCondition::CandidateAvailabilityThreshold { name: label, .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { name: label, .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { name: label, .. } => { + if label.trim().is_empty() { + Err("label must not be empty".to_string()) + } else { + Ok(()) + } + } + RuntimeCondition::CargoProductionSlotThreshold { slot, label, .. } => { + if !(1..=11).contains(slot) { + Err("slot must be in 1..=11".to_string()) + } else if label.trim().is_empty() { + Err("label must not be empty".to_string()) + } else { + Ok(()) + } + } + RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::FactoryProductionTotalThreshold { .. } + | RuntimeCondition::FarmMineProductionTotalThreshold { .. } + | RuntimeCondition::OtherCargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } + | RuntimeCondition::EconomicStatusCodeThreshold { .. } => Ok(()), + RuntimeCondition::WorldFlagEquals { key, .. } => { + if key.trim().is_empty() { + Err("key must not be empty".to_string()) + } else { + Ok(()) + } + } + } +} diff --git a/crates/rrt-runtime/src/validation/runtime/effects.rs b/crates/rrt-runtime/src/validation/runtime/effects.rs new file mode 100644 index 0000000..f9bcdf8 --- /dev/null +++ b/crates/rrt-runtime/src/validation/runtime/effects.rs @@ -0,0 +1,158 @@ +use std::collections::BTreeSet; + +use crate::event::effects::RuntimeEffect; +use crate::event::targets::{RuntimeCargoPriceTarget, RuntimeCargoProductionTarget}; + +use super::{ + validate_chairman_target, validate_company_governance_scalar_metric, validate_company_target, + validate_event_record_template, validate_player_target, validate_territory_target, +}; + +pub(crate) fn validate_runtime_effect( + effect: &RuntimeEffect, + valid_company_ids: &BTreeSet, + valid_player_ids: &BTreeSet, + valid_chairman_profile_ids: &BTreeSet, + valid_territory_ids: &BTreeSet, +) -> Result<(), String> { + match effect { + RuntimeEffect::SetWorldFlag { key, .. } => { + if key.trim().is_empty() { + return Err("key must not be empty".to_string()); + } + } + RuntimeEffect::SetWorldVariable { index, .. } => { + if !(1..=4).contains(index) { + return Err(format!( + "world runtime variable index {index} must be in 1..=4" + )); + } + } + RuntimeEffect::SetWorldScalarOverride { key, .. } => { + if key.trim().is_empty() { + return Err("key must not be empty".to_string()); + } + } + RuntimeEffect::SetLimitedTrackBuildingAmount { .. } + | RuntimeEffect::SetEconomicStatusCode { .. } => {} + RuntimeEffect::SetCompanyVariable { target, index, .. } => { + validate_company_target(target, valid_company_ids)?; + if !(1..=4).contains(index) { + return Err(format!("runtime variable index {index} must be in 1..=4")); + } + } + RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::ConfiscateCompanyAssets { target } + | RuntimeEffect::DeactivateCompany { target } + | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } + | RuntimeEffect::AdjustCompanyCash { target, .. } + | RuntimeEffect::AdjustCompanyDebt { target, .. } => { + validate_company_target(target, valid_company_ids)?; + } + RuntimeEffect::SetCompanyGovernanceScalar { target, metric, .. } => { + validate_company_target(target, valid_company_ids)?; + validate_company_governance_scalar_metric(*metric)?; + } + RuntimeEffect::SetCompanyTerritoryAccess { + target, territory, .. + } => { + validate_company_target(target, valid_company_ids)?; + validate_territory_target(territory, valid_territory_ids)?; + } + RuntimeEffect::SetPlayerVariable { target, index, .. } => { + validate_player_target(target, valid_player_ids)?; + if !(1..=4).contains(index) { + return Err(format!("runtime variable index {index} must be in 1..=4")); + } + } + RuntimeEffect::SetPlayerCash { target, .. } + | RuntimeEffect::DeactivatePlayer { target } => { + validate_player_target(target, valid_player_ids)?; + } + RuntimeEffect::SetChairmanCash { target, .. } + | RuntimeEffect::DeactivateChairman { target } => { + validate_chairman_target(target, valid_chairman_profile_ids)?; + } + RuntimeEffect::RetireTrains { + company_target, + territory_target, + locomotive_name, + } => { + if let Some(company_target) = company_target { + validate_company_target(company_target, valid_company_ids)?; + } + if let Some(territory_target) = territory_target { + validate_territory_target(territory_target, valid_territory_ids)?; + } + if company_target.is_none() && territory_target.is_none() && locomotive_name.is_none() { + return Err( + "retire_trains requires at least one company_target, territory_target, or locomotive_name filter" + .to_string(), + ); + } + if locomotive_name + .as_deref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err("locomotive_name must not be empty".to_string()); + } + } + RuntimeEffect::SetCandidateAvailability { name, .. } + | RuntimeEffect::SetNamedLocomotiveAvailability { name, .. } + | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, .. } + | RuntimeEffect::SetNamedLocomotiveCost { name, .. } => { + if name.trim().is_empty() { + return Err("name must not be empty".to_string()); + } + } + RuntimeEffect::SetCargoPriceOverride { target, .. } => match target { + RuntimeCargoPriceTarget::All => {} + RuntimeCargoPriceTarget::Named { name } => { + if name.trim().is_empty() { + return Err("name must not be empty".to_string()); + } + } + }, + RuntimeEffect::SetCargoProductionOverride { target, .. } => match target { + RuntimeCargoProductionTarget::All + | RuntimeCargoProductionTarget::Factory + | RuntimeCargoProductionTarget::FarmMine => {} + RuntimeCargoProductionTarget::Named { name } => { + if name.trim().is_empty() { + return Err("name must not be empty".to_string()); + } + } + }, + RuntimeEffect::SetCargoProductionSlot { slot, .. } => { + if !(1..=11).contains(slot) { + return Err("slot must be in 1..=11".to_string()); + } + } + RuntimeEffect::SetTerritoryVariable { target, index, .. } => { + validate_territory_target(target, valid_territory_ids)?; + if !(1..=4).contains(index) { + return Err(format!("runtime variable index {index} must be in 1..=4")); + } + } + RuntimeEffect::SetTerritoryAccessCost { .. } => {} + RuntimeEffect::SetSpecialCondition { label, .. } => { + if label.trim().is_empty() { + return Err("label must not be empty".to_string()); + } + } + RuntimeEffect::AppendEventRecord { record } => { + validate_event_record_template( + record, + valid_company_ids, + valid_player_ids, + valid_chairman_profile_ids, + valid_territory_ids, + )?; + } + RuntimeEffect::ActivateEventRecord { .. } + | RuntimeEffect::DeactivateEventRecord { .. } + | RuntimeEffect::RemoveEventRecord { .. } => {} + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/validation/runtime/metrics.rs b/crates/rrt-runtime/src/validation/runtime/metrics.rs new file mode 100644 index 0000000..0d385e1 --- /dev/null +++ b/crates/rrt-runtime/src/validation/runtime/metrics.rs @@ -0,0 +1,16 @@ +use crate::event::metrics::RuntimeCompanyMetric; + +pub(crate) fn validate_company_governance_scalar_metric( + metric: RuntimeCompanyMetric, +) -> Result<(), String> { + match metric { + RuntimeCompanyMetric::CreditRating + | RuntimeCompanyMetric::PrimeRate + | RuntimeCompanyMetric::BookValuePerShare + | RuntimeCompanyMetric::InvestorConfidence + | RuntimeCompanyMetric::ManagementAttitude => Ok(()), + _ => Err( + "governance scalar effect requires a writable company governance metric".to_string(), + ), + } +} diff --git a/crates/rrt-runtime/src/validation/runtime/mod.rs b/crates/rrt-runtime/src/validation/runtime/mod.rs new file mode 100644 index 0000000..1fc6270 --- /dev/null +++ b/crates/rrt-runtime/src/validation/runtime/mod.rs @@ -0,0 +1,14 @@ +mod conditions; +mod effects; +mod metrics; +mod targets; +mod templates; + +pub(crate) use conditions::validate_runtime_condition; +pub(crate) use effects::validate_runtime_effect; +pub(super) use metrics::validate_company_governance_scalar_metric; +pub(super) use targets::{ + validate_chairman_target, validate_company_target, validate_player_target, + validate_territory_target, +}; +pub(super) use templates::validate_event_record_template; diff --git a/crates/rrt-runtime/src/validation/runtime/targets.rs b/crates/rrt-runtime/src/validation/runtime/targets.rs new file mode 100644 index 0000000..2da6f71 --- /dev/null +++ b/crates/rrt-runtime/src/validation/runtime/targets.rs @@ -0,0 +1,101 @@ +use std::collections::BTreeSet; + +use crate::event::targets::{ + RuntimeChairmanTarget, RuntimeCompanyTarget, RuntimePlayerTarget, RuntimeTerritoryTarget, +}; + +pub(crate) fn validate_company_target( + target: &RuntimeCompanyTarget, + valid_company_ids: &BTreeSet, +) -> Result<(), String> { + match target { + RuntimeCompanyTarget::AllActive + | RuntimeCompanyTarget::HumanCompanies + | RuntimeCompanyTarget::AiCompanies + | RuntimeCompanyTarget::SelectedCompany + | RuntimeCompanyTarget::ConditionTrueCompany => Ok(()), + RuntimeCompanyTarget::Ids { ids } => { + if ids.is_empty() { + return Err("target ids must not be empty".to_string()); + } + for company_id in ids { + if !valid_company_ids.contains(company_id) { + return Err(format!("target references unknown company_id {company_id}")); + } + } + Ok(()) + } + } +} + +pub(crate) fn validate_player_target( + target: &RuntimePlayerTarget, + valid_player_ids: &BTreeSet, +) -> Result<(), String> { + match target { + RuntimePlayerTarget::AllActive + | RuntimePlayerTarget::HumanPlayers + | RuntimePlayerTarget::AiPlayers + | RuntimePlayerTarget::SelectedPlayer + | RuntimePlayerTarget::ConditionTruePlayer => Ok(()), + RuntimePlayerTarget::Ids { ids } => { + if ids.is_empty() { + return Err("target ids must not be empty".to_string()); + } + for player_id in ids { + if !valid_player_ids.contains(player_id) { + return Err(format!("target references unknown player_id {player_id}")); + } + } + Ok(()) + } + } +} + +pub(crate) fn validate_chairman_target( + target: &RuntimeChairmanTarget, + valid_chairman_profile_ids: &BTreeSet, +) -> Result<(), String> { + match target { + RuntimeChairmanTarget::AllActive + | RuntimeChairmanTarget::HumanChairmen + | RuntimeChairmanTarget::AiChairmen + | RuntimeChairmanTarget::SelectedChairman + | RuntimeChairmanTarget::ConditionTrueChairman => Ok(()), + RuntimeChairmanTarget::Ids { ids } => { + if ids.is_empty() { + return Err("target ids must not be empty".to_string()); + } + for profile_id in ids { + if !valid_chairman_profile_ids.contains(profile_id) { + return Err(format!( + "target references unknown chairman profile_id {profile_id}" + )); + } + } + Ok(()) + } + } +} + +pub(crate) fn validate_territory_target( + target: &RuntimeTerritoryTarget, + valid_territory_ids: &BTreeSet, +) -> Result<(), String> { + match target { + RuntimeTerritoryTarget::AllTerritories => Ok(()), + RuntimeTerritoryTarget::Ids { ids } => { + if ids.is_empty() { + return Err("territory target ids must not be empty".to_string()); + } + for territory_id in ids { + if !valid_territory_ids.contains(territory_id) { + return Err(format!( + "territory target references unknown territory_id {territory_id}" + )); + } + } + Ok(()) + } + } +} diff --git a/crates/rrt-runtime/src/validation/runtime/templates.rs b/crates/rrt-runtime/src/validation/runtime/templates.rs new file mode 100644 index 0000000..6963902 --- /dev/null +++ b/crates/rrt-runtime/src/validation/runtime/templates.rs @@ -0,0 +1,46 @@ +use std::collections::BTreeSet; + +use crate::event::records::RuntimeEventRecordTemplate; + +use super::{validate_runtime_condition, validate_runtime_effect}; + +pub(crate) fn validate_event_record_template( + record: &RuntimeEventRecordTemplate, + valid_company_ids: &BTreeSet, + valid_player_ids: &BTreeSet, + valid_chairman_profile_ids: &BTreeSet, + valid_territory_ids: &BTreeSet, +) -> Result<(), String> { + for (condition_index, condition) in record.conditions.iter().enumerate() { + validate_runtime_condition( + condition, + valid_company_ids, + valid_player_ids, + valid_chairman_profile_ids, + valid_territory_ids, + ) + .map_err(|err| { + format!( + "template record_id={}.conditions[{condition_index}] {err}", + record.record_id + ) + })?; + } + for (effect_index, effect) in record.effects.iter().enumerate() { + validate_runtime_effect( + effect, + valid_company_ids, + valid_player_ids, + valid_chairman_profile_ids, + valid_territory_ids, + ) + .map_err(|err| { + format!( + "template record_id={}.effects[{effect_index}] {err}", + record.record_id + ) + })?; + } + + Ok(()) +} diff --git a/crates/rrt-runtime/src/win.rs b/crates/rrt-runtime/src/win.rs deleted file mode 100644 index 54ac3c0..0000000 --- a/crates/rrt-runtime/src/win.rs +++ /dev/null @@ -1,551 +0,0 @@ -use std::fs; -use std::path::Path; - -use serde::{Deserialize, Serialize}; - -const WIN_COMMON_HEADER_LEN: usize = 0x50; -const WIN_INLINE_RESOURCE_OFFSET: usize = 0x50; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WinHeaderWord { - pub offset: usize, - pub offset_hex: String, - pub value: u32, - pub value_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WinResourceReference { - pub offset: usize, - pub offset_hex: String, - pub name: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WinReferenceDeltaFrequency { - pub delta: usize, - pub delta_hex: String, - pub count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WinResourceRecordSample { - pub offset: usize, - pub offset_hex: String, - pub name: String, - pub delta_from_previous: Option, - pub delta_from_previous_hex: Option, - pub prelude_words: Vec, - pub post_name_word_0: u32, - pub post_name_word_0_hex: String, - pub post_name_word_0_high_u16: u16, - pub post_name_word_0_high_u16_hex: String, - pub post_name_word_0_low_u16: u16, - pub post_name_word_0_low_u16_hex: String, - pub post_name_word_1: u32, - pub post_name_word_1_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WinResourceSelectorRecord { - pub offset: usize, - pub offset_hex: String, - pub name: String, - pub post_name_word_0: u32, - pub post_name_word_0_hex: String, - pub selector_high_u16: u16, - pub selector_high_u16_hex: String, - pub selector_low_u16: u16, - pub selector_low_u16_hex: String, - pub post_name_word_1: u32, - pub post_name_word_1_hex: String, - pub post_name_word_1_high_u16: u16, - pub post_name_word_1_high_u16_hex: String, - pub post_name_word_1_middle_u16: u16, - pub post_name_word_1_middle_u16_hex: String, - pub post_name_word_1_low_u16: u16, - pub post_name_word_1_low_u16_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WinAnonymousSelectorRecord { - pub record_offset: usize, - pub record_offset_hex: String, - pub preceding_named_record_name: Option, - pub preceding_named_record_offset_hex: Option, - pub following_named_record_name: Option, - pub following_named_record_offset_hex: Option, - pub selector_word_0: u32, - pub selector_word_0_hex: String, - pub selector_word_0_high_u16: u16, - pub selector_word_0_high_u16_hex: String, - pub selector_word_0_low_u16: u16, - pub selector_word_0_low_u16_hex: String, - pub selector_word_1: u32, - pub selector_word_1_hex: String, - pub selector_word_1_middle_u16: u16, - pub selector_word_1_middle_u16_hex: String, - pub body_word_0: u32, - pub body_word_0_hex: String, - pub body_word_1: u32, - pub body_word_1_hex: String, - pub body_word_2: u32, - pub body_word_2_hex: String, - pub body_word_3: u32, - pub body_word_3_hex: String, - pub footer_word_0: u32, - pub footer_word_0_hex: String, - pub footer_word_1: u32, - pub footer_word_1_hex: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WinInspectionReport { - pub file_size: usize, - pub common_header_len: usize, - pub common_header_len_hex: String, - pub shared_header_words: Vec, - pub matches_observed_common_signature: bool, - pub common_resource_record_prelude_prefix_words: Option>, - pub name_len_matches_prelude_word_3_plus_nul_count: usize, - pub inline_root_resource_name: Option, - pub inline_root_resource_offset: Option, - pub inline_root_resource_offset_hex: Option, - pub imb_reference_count: usize, - pub unique_imb_reference_count: usize, - pub unique_imb_references: Vec, - pub dominant_reference_deltas: Vec, - pub resource_selector_records: Vec, - pub anonymous_selector_records: Vec, - pub first_resource_record_samples: Vec, - pub first_imb_references: Vec, - pub notes: Vec, -} - -pub fn inspect_win_file(path: &Path) -> Result> { - let bytes = fs::read(path)?; - inspect_win_bytes(&bytes) -} - -pub fn inspect_win_bytes(bytes: &[u8]) -> Result> { - if bytes.len() < WIN_COMMON_HEADER_LEN { - return Err(format!( - "window resource is too short for the observed common header: {} < 0x{WIN_COMMON_HEADER_LEN:x}", - bytes.len() - ) - .into()); - } - - let header_offsets = [ - 0x00usize, 0x04, 0x08, 0x0c, 0x10, 0x14, 0x18, 0x1c, 0x20, 0x24, 0x28, 0x2c, 0x30, 0x34, - 0x38, 0x3c, 0x40, 0x44, 0x48, 0x4c, - ]; - let shared_header_words = header_offsets - .iter() - .map(|offset| { - let value = read_u32_le(bytes, *offset).expect("validated common header length"); - WinHeaderWord { - offset: *offset, - offset_hex: format!("0x{offset:02x}"), - value, - value_hex: format!("0x{value:08x}"), - } - }) - .collect::>(); - - let matches_observed_common_signature = read_u32_le(bytes, 0x00) == Some(0x0000_07d0) - && read_u32_le(bytes, 0x04) == Some(0) - && read_u32_le(bytes, 0x08) == Some(0) - && read_u32_le(bytes, 0x0c) == Some(0x8000_0000) - && read_u32_le(bytes, 0x10) == Some(0x8000_003f) - && read_u32_le(bytes, 0x14) == Some(0x0000_003f) - && read_u32_le(bytes, 0x34) == Some(0x0007_d100) - && read_u32_le(bytes, 0x38) == Some(0x0007_d200) - && read_u32_le(bytes, 0x40) == Some(0x000b_b800) - && read_u32_le(bytes, 0x48) == Some(0x000b_b900); - - let inline_root_resource_name = parse_inline_ascii_name(bytes, WIN_INLINE_RESOURCE_OFFSET); - let inline_root_resource_offset = inline_root_resource_name - .as_ref() - .map(|_| WIN_INLINE_RESOURCE_OFFSET + 1); - let inline_root_resource_offset_hex = - inline_root_resource_offset.map(|offset| format!("0x{offset:04x}")); - - let all_imb_references = collect_imb_references(bytes); - let resource_record_samples = build_resource_record_samples(bytes, &all_imb_references); - let resource_selector_records = build_resource_selector_records(&resource_record_samples); - let anonymous_selector_records = collect_anonymous_selector_records(bytes, &all_imb_references); - let common_resource_record_prelude_prefix_words = - shared_prelude_prefix_hex(&resource_record_samples); - let name_len_matches_prelude_word_3_plus_nul_count = resource_record_samples - .iter() - .filter(|sample| { - sample.prelude_words.len() == 4 - && sample.prelude_words[3].value == (sample.name.len() as u32 + 1) - }) - .count(); - let mut unique_imb_references = Vec::new(); - for reference in &all_imb_references { - if !unique_imb_references.contains(&reference.name) { - unique_imb_references.push(reference.name.clone()); - } - } - - let mut notes = Vec::new(); - if matches_observed_common_signature { - notes.push( - "Header matches the observed shared .win signature seen in Campaign.win, CompanyDetail.win, and setup.win." - .to_string(), - ); - } else { - notes.push( - "Header diverges from the currently observed shared .win signature; treat field meanings as provisional." - .to_string(), - ); - } - if inline_root_resource_name.is_some() { - notes.push( - "The blob carries an inline root .imb resource name immediately after the common 0x50-byte header." - .to_string(), - ); - } else { - notes.push( - "No inline root .imb resource name appears at 0x50; this window likely starts directly with control records." - .to_string(), - ); - } - notes.push( - "Embedded .imb strings are reported as resource references with selector lanes; this inspector still does not decode full control record semantics." - .to_string(), - ); - - Ok(WinInspectionReport { - file_size: bytes.len(), - common_header_len: WIN_COMMON_HEADER_LEN, - common_header_len_hex: format!("0x{WIN_COMMON_HEADER_LEN:02x}"), - shared_header_words, - matches_observed_common_signature, - common_resource_record_prelude_prefix_words, - name_len_matches_prelude_word_3_plus_nul_count, - inline_root_resource_name, - inline_root_resource_offset, - inline_root_resource_offset_hex, - imb_reference_count: all_imb_references.len(), - unique_imb_reference_count: unique_imb_references.len(), - unique_imb_references, - dominant_reference_deltas: build_delta_histogram(&resource_record_samples), - resource_selector_records, - anonymous_selector_records, - first_resource_record_samples: resource_record_samples.into_iter().take(32).collect(), - first_imb_references: all_imb_references.into_iter().take(32).collect(), - notes, - }) -} - -fn collect_imb_references(bytes: &[u8]) -> Vec { - let mut references = Vec::new(); - let mut offset = 0usize; - while offset < bytes.len() { - if let Some(name) = parse_imb_reference_at(bytes, offset) { - references.push(WinResourceReference { - offset, - offset_hex: format!("0x{offset:04x}"), - name, - }); - } - offset += 1; - } - references -} - -fn build_resource_record_samples( - bytes: &[u8], - references: &[WinResourceReference], -) -> Vec { - let mut samples = Vec::with_capacity(references.len()); - for (index, reference) in references.iter().enumerate() { - let previous_offset = index - .checked_sub(1) - .and_then(|previous| references.get(previous)) - .map(|previous| previous.offset); - let delta_from_previous = previous_offset.map(|previous| reference.offset - previous); - let delta_from_previous_hex = delta_from_previous.map(|delta| format!("0x{delta:x}")); - - let prelude_words = if reference.offset >= 16 { - (0..4) - .map(|index| { - let offset = reference.offset - 16 + index * 4; - let value = read_u32_le(bytes, offset).unwrap_or(0); - WinHeaderWord { - offset, - offset_hex: format!("0x{offset:04x}"), - value, - value_hex: format!("0x{value:08x}"), - } - }) - .collect() - } else { - Vec::new() - }; - - let name_end = reference.offset + reference.name.len(); - let post_name_word_0 = read_u32_le(bytes, name_end + 1).unwrap_or(0); - let post_name_word_1 = read_u32_le(bytes, name_end + 5).unwrap_or(0); - let post_name_word_0_high_u16 = ((post_name_word_0 >> 16) & 0xffff) as u16; - let post_name_word_0_low_u16 = (post_name_word_0 & 0xffff) as u16; - - samples.push(WinResourceRecordSample { - offset: reference.offset, - offset_hex: reference.offset_hex.clone(), - name: reference.name.clone(), - delta_from_previous, - delta_from_previous_hex, - prelude_words, - post_name_word_0, - post_name_word_0_hex: format!("0x{post_name_word_0:08x}"), - post_name_word_0_high_u16, - post_name_word_0_high_u16_hex: format!("0x{post_name_word_0_high_u16:04x}"), - post_name_word_0_low_u16, - post_name_word_0_low_u16_hex: format!("0x{post_name_word_0_low_u16:04x}"), - post_name_word_1, - post_name_word_1_hex: format!("0x{post_name_word_1:08x}"), - }); - } - samples -} - -fn build_delta_histogram(samples: &[WinResourceRecordSample]) -> Vec { - let mut counts = std::collections::BTreeMap::::new(); - for sample in samples { - if let Some(delta) = sample.delta_from_previous { - *counts.entry(delta).or_default() += 1; - } - } - - let mut frequencies = counts - .into_iter() - .map(|(delta, count)| WinReferenceDeltaFrequency { - delta, - delta_hex: format!("0x{delta:x}"), - count, - }) - .collect::>(); - frequencies.sort_by(|left, right| { - right - .count - .cmp(&left.count) - .then_with(|| left.delta.cmp(&right.delta)) - }); - frequencies.truncate(12); - frequencies -} - -fn build_resource_selector_records( - samples: &[WinResourceRecordSample], -) -> Vec { - samples - .iter() - .map(|sample| { - let post_name_word_1_high_u16 = ((sample.post_name_word_1 >> 16) & 0xffff) as u16; - let post_name_word_1_middle_u16 = ((sample.post_name_word_1 >> 8) & 0xffff) as u16; - let post_name_word_1_low_u16 = (sample.post_name_word_1 & 0xffff) as u16; - WinResourceSelectorRecord { - offset: sample.offset, - offset_hex: sample.offset_hex.clone(), - name: sample.name.clone(), - post_name_word_0: sample.post_name_word_0, - post_name_word_0_hex: sample.post_name_word_0_hex.clone(), - selector_high_u16: sample.post_name_word_0_high_u16, - selector_high_u16_hex: sample.post_name_word_0_high_u16_hex.clone(), - selector_low_u16: sample.post_name_word_0_low_u16, - selector_low_u16_hex: sample.post_name_word_0_low_u16_hex.clone(), - post_name_word_1: sample.post_name_word_1, - post_name_word_1_hex: sample.post_name_word_1_hex.clone(), - post_name_word_1_high_u16, - post_name_word_1_high_u16_hex: format!("0x{post_name_word_1_high_u16:04x}"), - post_name_word_1_middle_u16, - post_name_word_1_middle_u16_hex: format!("0x{post_name_word_1_middle_u16:04x}"), - post_name_word_1_low_u16, - post_name_word_1_low_u16_hex: format!("0x{post_name_word_1_low_u16:04x}"), - } - }) - .collect() -} - -fn collect_anonymous_selector_records( - bytes: &[u8], - references: &[WinResourceReference], -) -> Vec { - const PRELUDE: [u8; 12] = [ - 0xb8, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb9, 0x0b, 0x00, 0x00, - ]; - - let mut records = Vec::new(); - let mut start = 0usize; - while let Some(relative) = bytes.get(start..).and_then(|slice| { - slice - .windows(PRELUDE.len()) - .position(|window| window == PRELUDE) - }) { - let record_offset = start + relative; - let name_len = read_u32_le(bytes, record_offset + PRELUDE.len()).unwrap_or(0); - if name_len == 0 { - let selector_word_0 = read_u32_le(bytes, record_offset + 0x10).unwrap_or(0); - let selector_word_0_low_u16 = (selector_word_0 & 0xffff) as u16; - if (0xc352..=0xc39b).contains(&selector_word_0_low_u16) { - let preceding_named_record = references - .iter() - .rev() - .find(|reference| reference.offset < record_offset); - let following_named_record = references - .iter() - .find(|reference| reference.offset > record_offset); - let selector_word_1 = read_u32_le(bytes, record_offset + 0x14).unwrap_or(0); - let selector_word_0_high_u16 = ((selector_word_0 >> 16) & 0xffff) as u16; - let selector_word_1_middle_u16 = ((selector_word_1 >> 8) & 0xffff) as u16; - let body_word_0 = read_u32_le(bytes, record_offset + 0x18).unwrap_or(0); - let body_word_1 = read_u32_le(bytes, record_offset + 0x1c).unwrap_or(0); - let body_word_2 = read_u32_le(bytes, record_offset + 0x20).unwrap_or(0); - let body_word_3 = read_u32_le(bytes, record_offset + 0x24).unwrap_or(0); - let footer_word_0 = read_u32_le(bytes, record_offset + 0x98).unwrap_or(0); - let footer_word_1 = read_u32_le(bytes, record_offset + 0x9c).unwrap_or(0); - records.push(WinAnonymousSelectorRecord { - record_offset, - record_offset_hex: format!("0x{record_offset:04x}"), - preceding_named_record_name: preceding_named_record - .map(|record| record.name.clone()), - preceding_named_record_offset_hex: preceding_named_record - .map(|record| record.offset_hex.clone()), - following_named_record_name: following_named_record - .map(|record| record.name.clone()), - following_named_record_offset_hex: following_named_record - .map(|record| record.offset_hex.clone()), - selector_word_0, - selector_word_0_hex: format!("0x{selector_word_0:08x}"), - selector_word_0_high_u16, - selector_word_0_high_u16_hex: format!("0x{selector_word_0_high_u16:04x}"), - selector_word_0_low_u16, - selector_word_0_low_u16_hex: format!("0x{selector_word_0_low_u16:04x}"), - selector_word_1, - selector_word_1_hex: format!("0x{selector_word_1:08x}"), - selector_word_1_middle_u16, - selector_word_1_middle_u16_hex: format!("0x{selector_word_1_middle_u16:04x}"), - body_word_0, - body_word_0_hex: format!("0x{body_word_0:08x}"), - body_word_1, - body_word_1_hex: format!("0x{body_word_1:08x}"), - body_word_2, - body_word_2_hex: format!("0x{body_word_2:08x}"), - body_word_3, - body_word_3_hex: format!("0x{body_word_3:08x}"), - footer_word_0, - footer_word_0_hex: format!("0x{footer_word_0:08x}"), - footer_word_1, - footer_word_1_hex: format!("0x{footer_word_1:08x}"), - }); - } - } - start = record_offset + 1; - } - records -} - -fn shared_prelude_prefix_hex(samples: &[WinResourceRecordSample]) -> Option> { - let first = samples.first()?; - if first.prelude_words.len() < 3 { - return None; - } - let prefix = first.prelude_words[..3] - .iter() - .map(|word| word.value) - .collect::>(); - if samples.iter().all(|sample| { - sample.prelude_words.len() >= 3 - && sample.prelude_words[..3] - .iter() - .map(|word| word.value) - .collect::>() - == prefix - }) { - return Some( - prefix - .into_iter() - .map(|value| format!("0x{value:08x}")) - .collect(), - ); - } - None -} - -fn parse_imb_reference_at(bytes: &[u8], offset: usize) -> Option { - if offset > 0 { - let previous = *bytes.get(offset - 1)?; - if previous != 0 { - return None; - } - } - let slice = bytes.get(offset..)?; - let nul = slice.iter().position(|byte| *byte == 0)?; - let candidate = slice.get(..nul)?; - if candidate.len() < 5 { - return None; - } - let value = std::str::from_utf8(candidate).ok()?; - if !value.ends_with(".imb") { - return None; - } - if !value - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b' ')) - { - return None; - } - Some(value.to_string()) -} - -fn parse_inline_ascii_name(bytes: &[u8], offset: usize) -> Option { - let prefix = *bytes.get(offset)?; - if prefix != 0 { - return None; - } - parse_imb_reference_at(bytes, offset + 1) -} - -fn read_u32_le(bytes: &[u8], offset: usize) -> Option { - let slice = bytes.get(offset..offset + 4)?; - Some(u32::from_le_bytes(slice.try_into().ok()?)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn inspects_synthetic_window_blob() { - let mut bytes = vec![0u8; 0x90]; - bytes[0x00..0x04].copy_from_slice(&0x0000_07d0u32.to_le_bytes()); - bytes[0x0c..0x10].copy_from_slice(&0x8000_0000u32.to_le_bytes()); - bytes[0x10..0x14].copy_from_slice(&0x8000_003fu32.to_le_bytes()); - bytes[0x14..0x18].copy_from_slice(&0x0000_003fu32.to_le_bytes()); - bytes[0x34..0x38].copy_from_slice(&0x0007_d100u32.to_le_bytes()); - bytes[0x38..0x3c].copy_from_slice(&0x0007_d200u32.to_le_bytes()); - bytes[0x40..0x44].copy_from_slice(&0x000b_b800u32.to_le_bytes()); - bytes[0x48..0x4c].copy_from_slice(&0x000b_b900u32.to_le_bytes()); - bytes[0x50] = 0; - bytes[0x51..0x51 + "Root.imb".len()].copy_from_slice(b"Root.imb"); - bytes[0x59] = 0; - bytes.extend_from_slice(b"\0Button.imb\0"); - - let report = inspect_win_bytes(&bytes).expect("inspection should succeed"); - assert!(report.matches_observed_common_signature); - assert_eq!( - report.inline_root_resource_name.as_deref(), - Some("Root.imb") - ); - assert_eq!(report.imb_reference_count, 2); - assert_eq!(report.unique_imb_reference_count, 2); - assert_eq!(report.resource_selector_records.len(), 2); - assert_eq!(report.resource_selector_records[0].name, "Root.imb"); - assert!(report.anonymous_selector_records.is_empty()); - } -} diff --git a/docs/README.md b/docs/README.md index 5630bf5..4e569f6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,18 +13,31 @@ project is already mature. ## Documents +- Stable docs - `setup-workstation.md`: toolchain baseline and local environment setup. - `re-workflow.md`: how to analyze the binary, record findings, and export reusable artifacts. - `function-map.md`: canonical schema and conventions for function-by-function mapping. - `control-loop-atlas.md`: compatibility index for the split atlas, preserving legacy anchors. - `control-loop-atlas/`: canonical section files for the atlas narrative. +- `subsystem-views/`: curated cross-cut subsystem views over the atlas. - `runtime-rehost-plan.md`: bottom-up runtime replacement plan and milestone breakdown. +- Active queue and history +- `rehost-queue.md`: active implementation and research queue. +- `rehost-queue/`: archived queue snapshots and related preserved worklog material. +- `history/progress-history.md`: preserved high-detail milestone narrative from the former root README. + +- Artifact indexes +- `../artifacts/exports/rt3-1.06/README.md`: canonical 1.06 export manifest. +- `../artifacts/captures/README.md`: committed capture inventory and policy. + ## Repo Conventions - `docs/`: stable project guidance and durable design notes. - `tools/py/`: committed Python helpers for analysis and validation. - `artifacts/exports/`: committed derived outputs that can be regenerated. +- `artifacts/captures/`: committed logs, sample binaries, and retained capture evidence. +- `artifacts/tmp/`: scratch-only local working area; do not commit new files here. - Local-only state stays untracked: `.venv/`, Ghidra projects, Rizin databases, crash dumps, and other bulky/generated working files. diff --git a/docs/control-loop-atlas.md b/docs/control-loop-atlas.md index 21183c1..39cce69 100644 --- a/docs/control-loop-atlas.md +++ b/docs/control-loop-atlas.md @@ -2,11 +2,11 @@ This atlas is now split by top-level section under [docs/control-loop-atlas/](/home/jan/projects/rrt/docs/control-loop-atlas/README.md). -This file remains the compatibility index and keeps the original top-level anchors stable for +This file remains the compatibility index that keeps the original top-level anchors stable for older links. -Subsystem notes still live under [docs/atlas](/home/jan/projects/rrt/docs/atlas/README.md) for -faster cross-cut navigation. +For curated cross-cut navigation, use +[docs/subsystem-views](/home/jan/projects/rrt/docs/subsystem-views/README.md). ## Section Index @@ -19,6 +19,20 @@ faster cross-cut navigation. - [Input, Save/Load, and Simulation](/home/jan/projects/rrt/docs/control-loop-atlas/input-save-load-and-simulation.md) - [Next Mapping Passes](/home/jan/projects/rrt/docs/control-loop-atlas/next-mapping-passes.md) +## Atlas Field Guide + +This compatibility index keeps the original top-level anchors stable. The grounded atlas fields now +live in the split section files under +[docs/control-loop-atlas/](/home/jan/projects/rrt/docs/control-loop-atlas/README.md). + +- Roots: see each section file's opening field block for the concrete root functions and entrypoints. +- Trigger/Cadence: see the split section files for the cadence notes attached to each loop or ingress. +- Key Dispatchers: see the section-local dispatcher bullets in the split atlas pages. +- State Anchors: see the state-anchor bullets in each split atlas section. +- Subsystem Handoffs: see the handoff bullets in each split atlas section. +- Evidence: see the section-local evidence bullets in the split atlas pages. +- Open Questions: see the section-local open-question bullets in the split atlas pages. + ## CRT and Process Startup Detailed section: diff --git a/docs/control-loop-atlas/post-load-generation-paintterrain-and-save-load-restore.md b/docs/control-loop-atlas/post-load-generation-paintterrain-and-save-load-restore.md index b5d0fed..56a2e2b 100644 --- a/docs/control-loop-atlas/post-load-generation-paintterrain-and-save-load-restore.md +++ b/docs/control-loop-atlas/post-load-generation-paintterrain-and-save-load-restore.md @@ -103,7 +103,7 @@ The same brush strip is tighter now too: now as well, because it checks the per-node promotion-latch dword `[node+0x0c] == 1` rather than reusing the queue kind field. `0x00438840` is the tiny dispatch-or-fallback sibling: it forwards the currently active queue node at `[world+0x66aa]` - into `simulation_dispatch_runtime_effect_queue_record_by_kind_into_shell_or_world_handlers` + into `simulation_dispatch_runtime_effect_queue_node_by_kind` `0x00437c00`, or opens the fixed custom modal rooted at localized id `0x153` when no active node is staged. company-credit side effect. One neighboring narrow counter is bounded too: `0x00422850` @@ -920,7 +920,7 @@ The same brush strip is tighter now too: `[desc+0x24/+0x28]`, not an anonymous cargo-economy mode byte. Slot `34` `[0x006cec78+0x4b07]` is similarly bounded on the runtime side: - `world_run_company_start_or_city_connection_chooser_with_region_field_0x2d_temporarily_cleared_if_rule_0x4b07` + `world_try_publish_startup_company_or_city_connection_news_ignoring_territories` `0x004013f0`, which sits immediately above the broader company-start or city-connection chooser `0x00404ce0`, snapshots region dword `[entry+0x2d]` across all `0x18` live region records in `0x006cfc9c`, zeros that field while the chooser runs, and then restores the original values on diff --git a/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md b/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md index 14415ba..1fedaa6 100644 --- a/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md +++ b/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md @@ -2494,11 +2494,11 @@ Current evidence grounds the shell-controller-backed input and frame path as the actual field-seeding layer beneath that allocator. The old unresolved higher placement chooser is bounded more cleanly now too: the `0x00403ed5` and `0x0040446b` direct-placement commits sit inside one larger helper, - `city_connection_try_build_route_with_optional_direct_site_placement` `0x00402cb0`. That shared + `city_connection_try_build_route_and_optionally_place_direct_site` `0x00402cb0`. That shared heavy builder is now the common target of the compact wrapper `0x00404640`, the peer-route candidate builder `0x004046a0`, the region-entry pair wrapper `city_connection_try_build_route_between_region_entry_pair` `0x00404c60`, and the direct retry - paths inside `simulation_try_select_and_publish_company_start_or_city_connection_news` + paths inside `simulation_try_publish_startup_company_or_city_connection_news` `0x00404ce0`. Internally it now has three bounded lanes: an early route-entry search or synthesis attempt through `route_entry_collection_try_build_path_between_optional_endpoint_entries` `0x004a01a0`, @@ -3830,11 +3830,11 @@ The low helper strip beneath that shared family is tighter now too: `0x0052ecd0` against the scenario-side scale returned by `0x00437d20(index)` and rounds the result through `0x005a10d0`. The broader company-side owner above these pieces is tighter now too. The periodic service pass - `company_service_periodic_city_connection_finance_and_linked_transit_lanes` `0x004019e0` now + `company_service_periodic_city_connection_finance_and_linked_transit` `0x004019e0` now reads as the current outer owner for this branch: it sequences the city-connection announcement lanes, the linked-transit train-roster balancer, the acquisition-side sibling - `company_try_buy_unowned_industry_near_city_and_publish_news` `0x004014b0`, the annual finance-policy helper - `company_evaluate_annual_finance_policy_and_publish_news` `0x00401c50`, and the shorter-versus- + `company_try_acquire_unowned_industry_near_city_and_publish_news` `0x004014b0`, the annual finance-policy helper + `company_apply_annual_finance_policy_and_publish_news` `0x00401c50`, and the shorter-versus- longer linked-transit cache refresh tail through `0x004093d0` and `0x00407bd0`. That outer pass also has one tighter temporary-state role now: it clears company bytes `0x0d17/0x0d18/0x0d56`, mirrors `0x0d17` into scenario field `0x006cec78+0x4c74` only while the earlier route-building @@ -3898,7 +3898,7 @@ The low helper strip beneath that shared family is tighter now too: `0x0052ecd0` support-adjusted-share-price-times-factor-times-`1000`-times-`1.2` affordability gate before the repeated `1000`-share buyback commits behind RT3.lng `2887`. The ordering above this helper is tighter now too: - `company_service_periodic_city_connection_finance_and_linked_transit_lanes` clears those latches + `company_service_periodic_city_connection_finance_and_linked_transit` clears those latches first, runs the city-connection and linked-transit branches, and only then enters the annual finance helper, so these look like same-cycle reaction gates rather than long-lived balance-sheet flags. diff --git a/docs/control-loop-atlas/station-detail-overlay.md b/docs/control-loop-atlas/station-detail-overlay.md index 2cf45bd..a96fb5c 100644 --- a/docs/control-loop-atlas/station-detail-overlay.md +++ b/docs/control-loop-atlas/station-detail-overlay.md @@ -65,7 +65,7 @@ One reusable site helper is grounded now too. before choosing whether a scanned site should carry `3871` `Connected By Another Company` or `3872` `Already Connected by Another Company`. The larger caller boundary is no longer open either: the first bounded announcement owner above this formatter family is now - `company_evaluate_and_publish_city_connection_bonus_news` at `0x00406050`, which re-enters the + `company_try_publish_city_connection_bonus_news` at `0x00406050`, which re-enters the peer-route candidate builder at `0x004046a0` and later publishes one of the localized city-connection bonus news strings `2888`, `2890`, or `2921` through the shell news path. #### Peer-selector side @@ -88,7 +88,7 @@ The reusable bridge between the status formatter and the `0x004046a0` reuses `city_connection_bonus_select_first_matching_peer_site` with both selector flags forced on, rounds both the source-city and selected-peer normalized coordinates through `0x005a10d0`, and then first tries the shared heavy builder - `city_connection_try_build_route_with_optional_direct_site_placement` `0x00402cb0` when the + `city_connection_try_build_route_and_optionally_place_direct_site` `0x00402cb0` when the caller already supplied a live route-anchor tuple. When that direct attempt does not apply or no peer survives, it falls back to the smaller wrapper `city_connection_bonus_try_compact_route_builder_from_region_entry` `0x00404640`; and when both @@ -119,7 +119,7 @@ The reusable bridge between the status formatter and the debt headline family, then publishes `2887` separately from the accumulated repurchased-share count. The sibling news owner above the same city-pair route family is bounded now too: - `simulation_try_select_and_publish_company_start_or_city_connection_news` `0x00404ce0` + `simulation_try_publish_startup_company_or_city_connection_news` `0x00404ce0` filters and scores candidate city entries, re-enters the same shared heavy builder through `city_connection_try_build_route_between_region_entry_pair` `0x00404c60` for the dense pair sweep and the final retry, and then publishes `2889` `%1 has started a new company - the %2` diff --git a/docs/function-map.md b/docs/function-map.md index 634686d..f4d445f 100644 --- a/docs/function-map.md +++ b/docs/function-map.md @@ -34,9 +34,24 @@ Field meanings: ## Update Rules - New rows must always include `address`, `name`, `subsystem`, `source_tool`, and `confidence`. +- Prefer names in the shape `owner_verb_object[_qualifier]`. +- Prefer one primary verb, one primary object, and at most one qualifier. - If a rename is speculative, state that directly in `notes`. - When two tools disagree on function boundaries, preserve the ambiguity in `notes` instead of hiding it. - Prefer one row per concrete function, not per guessed feature. +- Prefer `try_` for best-effort helpers that may fall through without mutation or publication. +- Prefer `apply_` when a helper commits one selected policy or state transition. +- Reserve `evaluate_` for read-heavy helpers that classify or score state without committing the later action themselves. +- Prefer one stable family noun once a transient runtime structure is grounded. +- Use `queue_node` for transient linked-list allocations, and reserve `record` for persisted rows or document-style payloads. +- Prefer `startup_company` over `company_start` when the object is the newly started company. +- Prefer participial qualifiers such as `_ignoring_territories` over `_with_*_ignored` once the side condition is grounded. +- Drop filler tails such as `_lanes` once a broader owner is grounded well enough to carry the family directly. +- Prefer `_and_optionally_` over `_with_optional_` when a helper may take one secondary path but the main owner is still singular. +- Treat `_and_`, `_with_`, `_if_`, and `_via_` as fallback tools for still-uncertain seams, not the + default naming style. +- Raw offset tails such as `field_0xNN` are acceptable for direct accessors and low-confidence rows, + but should be replaced once a grounded semantic field name exists. ## Starter Subsystems diff --git a/docs/history/progress-history.md b/docs/history/progress-history.md new file mode 100644 index 0000000..6ee6879 --- /dev/null +++ b/docs/history/progress-history.md @@ -0,0 +1,311 @@ +Preserved progress narrative from the former root README. + +Analysis and reimplementation of Railroad Tycoon 3 + + +The old executable is at ./rt3_wineprefix/drive_c/rt3/RT3.exe + +Our first task is to understand the executable's high-level control loops and subsystem boundaries well +enough to choose good rewrite targets. As we go, we document evidence, keep a curated function map, +and stand up Rust tooling that can validate artifacts and later host replacement code. + +The long-term direction is still a DLL we can inject into the original executable, patching in +individual functions as we build them out. The active implementation milestone is now a headless +runtime rehost layer that can execute deterministic world work, compare normalized state, and grow +subsystem breadth without depending on the shell or presentation path. The current packed-event +frontier is broader real grouped-descriptor coverage on top of the existing save-slice, snapshot, +overlay-import, compact-control, and symbolic company-target workflows. The runtime already carries +selected-company and controller-role context through overlay imports, and real descriptors `2` +`Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and +execute through the ordinary runtime path, and descriptors `1` `Player Cash` and `14` +`Deactivate Player` now join that batch through the same service engine. Synthetic packed records +still exercise the same runtime without a parallel packed executor. The first grounded +chairman-profile runtime slice now exists too: save-slice or overlay-backed chairman/company +context plus the hidden grouped target-subject lane let those same real descriptors `1` and `14` +execute on the grounded chairman scope ordinals `0..3` (`condition_true`, `selected`, `human`, +`ai`), while wider chairman ordinals remain explicit parity. The first grounded +chairman and governance condition batch is broader now: selected-chairman cash / holdings / net +worth / purchasing-power thresholds and company book-value-per-share / investor-confidence / +management-attitude thresholds now import through the normal event-service path, while wider +chairman ordinals remain explicit frontier. Checked-in save-slice +documents can now also carry explicit company rosters and chairman-profile tables, so the current +company-targeted and chairman-targeted descriptor and condition batches can execute from standalone +save-slice fixtures without overlay snapshots when that context is present; raw `.gms` inspection +now reconstructs both collections automatically: the fixed save-side `0x32c8` world block still +supplies selected company/chairman ids plus the campaign override byte, the grounded issue-`0x37` +value/multiplier pair, and chairman slot/role-gate analysis bytes, and the tagged company / +chairman-profile direct-record families now populate +save-native roster entries for real `.gms` imports and exports. The current raw-save boundary is +narrower now: company/chairman identity, active flags, links, chairman cash, chairman holdings, +chairman purchasing power, company debt, and company track-laying capacity are grounded directly +from save records, while +broader company finance/governance scalars and controller-kind reconstruction still remain +conservative defaults until their raw lanes are pinned more strongly. The offline runtime analysis +surface also now exposes `runtime inspect-save-company-chairman ` 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 `, +`runtime inspect-region-service-trace `, and +`runtime inspect-infrastructure-asset-trace `, so the next city-connection / +linked-transit slices can start from explicit owner-seam blockers instead of another generic save +scan. A checked-in +`EventEffects` export now exists too in +`artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now +exists beside it in `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`. Recovered +descriptor rows now land on explicit semantic frontier buckets such as +`blocked_shell_owned_descriptor`, `blocked_evidence_blocked_descriptor`, and +`blocked_variant_or_scope_blocked_descriptor` instead of generic anonymous descriptor residue. The +first recovered governance descriptor tranche now imports through the generic +company-governance scalar effect surface: +descriptor `56` `Credit Rating` and descriptor `57` `Prime Rate` execute from ordinary real packed +rows, while adjacent recovered finance/control-transfer descriptors such as `55` `Stock Prices` +and `58` `Merger Premium` now land on explicit shell-owned parity instead of anonymous unmapped +descriptor residue, and tracked shell-owned fixtures now pin finance, scenario-outcome, and +control-transfer shell rows explicitly. The +recovered whole-game scalar economy/performance strip `59..104` now has a +bounded runtime landing surface too: representative descriptors import into +`RuntimeState.world_scalar_overrides` through stable normalized keys such as +`world.build_stations_cost`, `world.track_maintenance_cost`, `world.all_engine_speeds`, and +`world.hotel_revenue`. The runtime-variable strip `39..54` now executes too through bounded +event-owned scalar maps on world/company/player/territory state, and the matching ordinary +condition strip now gates records through those same maps too, without widening save-native +reconstruction or adding a second packed executor. The grounded aggregate cargo-economics +descriptors now have bounded +runtime landing surfaces too: descriptor `105` `All Cargo Prices` plus descriptors `177..179` +`All Cargo Production` / `All Factory Production` / `All Farm/Mine Production` import into +event-owned cargo override state, and the grounded named cargo-production strip `180..229` now +imports into named cargo production overrides too, and the named cargo-price strip `106..176` now +imports into named cargo price overrides as well. The checked-in static selector reconstruction is +now explicit: the broader 1.06 CargoTypes corpus has `51` names, the Cargo106 `cargoSkin` corpus +has `70`, and the rehosted offline selector builder now closes the `71`-row named price strip as +`cargoSkin` plus the core `Rock` carry-over. The checked-in +`artifacts/exports/rt3-1.06/economy-cargo-sources.json` report parses both `CargoTypes` and the +`Cargo106.PK4` `cargoSkin` descriptors, normalizes localized `~####Name` tokens into visible +names, builds a merged live cargo registry, and now derives exact named cargo-production and named +cargo-price selectors from the checked-in bindings. Dedicated CLI inspector commands now expose +both grounded selectors directly, while the same report still makes the residual live-registry gap +explicit by showing the nine excluded CargoTypes-only industrial names outside the 71-row price +strip. The +add-building strip `503..519` is now explicitly classified as recovered +shell-owned descriptor parity rather than generic unresolved residue. The first grounded +condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and +the first ordinary nonnegative condition batch now executes too: numeric-threshold company +finance, company track, aggregate territory track, and company-territory track rows can import +through overlay-backed runtime context. Exact named-territory binding now executes, and the runtime +now also carries the minimal event-owned train roster and opaque economic-status lane needed for +real descriptors `8` `Economic Status`, `9` `Confiscate All`, and `15` `Retire Train` to execute +through the same path. Descriptor `3` +`Territory - Allow All` now executes too, reinterpreted as company-to-territory access rights +rather than a territory-owned policy bit. Whole-game ordinary-condition execution now exists too: +special-condition thresholds, candidate-availability thresholds, and economic-status-code +thresholds now gate imported runtime records through the same service path, and that world-side +condition batch now decodes from checked-in metadata instead of fixture-only ids: real +special-condition label ids, real economic-status ids, and the recovered `%1 Avail.` candidate +template plus candidate-name side strings all lower into the runtime condition model. Checked-in +whole-game descriptor metadata now drives the first real world-side effect batch too: +special-condition and candidate-availability setters import natively, and descriptor `110` +`Disable Stock Buying and Selling` now lowers into the keyed runtime flag +`world.disable_stock_buying_and_selling`. The recovered whole-game toggle batch is broader now +too: descriptors `111..138`, with descriptor `122` `Limited Track Building Amount` now landing in +the bounded `world_restore.limited_track_building_amount` scalar and the remaining boolean lanes +lowering into keyed `world_flags`, cover finance/trading, construction, and governance +restrictions. Explicit the late recovered special-condition toggles now execute too where current +evidence is equally +strong: `Use Bio-Accelerator Cars`, `Disable Cargo Economy`, `Disable Train Crashes`, `Disable +Train Crashes AND Breakdowns`, and `AI Ignore Territories At Startup`. Whole-game condition decode +is broader now too: checked-in world-flag condition ids can lower into `world_flag_equals` gates +for boolean equality/inequality forms, so real packed records can gate whole-game effects on +existing `world_flags` without fixture-authored placeholder ids. The tracked parity save-slice no +longer depends on a raw `unsupported_framing` placeholder either: its remaining residue is now one +recovered locomotives-page `real_packed_v1` record that now lands on explicit descriptor parity +instead of a generic unmapped bucket. The next recovered descriptor band is now partially +executable too: descriptors `454..456` (`All Steam/Diesel/Electric Locos Avail.`) now lower +through checked-in metadata into keyed `world_flags`, while the wider locomotive availability/cost +scalar bands are now save-native too. Raw `.smp` inspection/export reconstructs the persisted +`[world+0x66b6]` locomotive name table and derives a minimal `RuntimeState.locomotive_catalog`, so +standalone save-slice imports can now lower the grounded lower locomotive availability and +locomotive-cost rows directly into `RuntimeState.named_locomotive_availability` and +`RuntimeState.named_locomotive_cost` without needing overlay snapshots when the save carries enough +catalog context, and the grounded executable lower prefix now extends through save-backed +locomotive id `61` (`Zephyr`); the unresolved lower tail and upper locomotive bands now stay on +explicit parity instead of synthetic execution. The remaining recovered scalar world families +execute too: +cargo-production slots `230..240` lower into `cargo_production_overrides`, and descriptor `453` +lowers into +`world_restore.territory_access_cost`. Whole-game ordinary-condition breadth now aligns with those +same world-scalar runtime surfaces too: named locomotive availability thresholds, named +locomotive cost thresholds, named cargo-production slot thresholds, aggregate cargo-production +thresholds, factory/farm-mine/other cargo-production thresholds, limited-track-building-amount +thresholds, and territory-access-cost thresholds all gate imported runtime records through the +same service path. Explicit unmapped world-condition frontier buckets still remain where current +checked-in metadata stops, and +`blocked_missing_locomotive_catalog_context` is now reserved for intentionally incomplete save-side +catalog context instead of the normal save-slice path. Cargo slot identity and class metadata are +now save-native too: the recipe-book probe lowers into `RuntimeState.cargo_catalog`, so save-slice +documents can carry slot labels, class tags, and token-stem evidence alongside the executable +`cargo_production_overrides` surface without introducing a live cargo-economy model. Shell +purchase-flow, Trainbuy refresh, +cached locomotive-rating recomputation, and selected-profile parity remain out of scope. Mixed +supported/unsupported real rows still stay parity-only. The PE32 hook remains useful as capture and +integration tooling, but it is no longer the main execution milestone. + +## Project Docs + +Bootstrap design and workflow documents live in `docs/`. + +- `docs/README.md`: handbook index and target hashes +- `docs/control-loop-atlas.md`: compatibility index for the split atlas +- `docs/control-loop-atlas/`: canonical atlas section files +- `docs/setup-workstation.md`: toolchain baseline and local setup +- `docs/re-workflow.md`: repeatable reverse-engineering workflow +- `docs/function-map.md`: canonical function-map schema and conventions + +The first committed exports for the canonical 1.06 executable live in `artifacts/exports/rt3-1.06/`. + +## Rust Workspace + +The Rust workspace is split into focused crates: + +- `rrt-model`: shared types for addresses, function-map rows, and control-loop concepts +- `rrt-runtime`: headless runtime state, stepping, normalized event service, and persistence-facing + runtime types +- `rrt-fixtures`: fixture schemas, loading, normalization, and diff helpers for rehost validation +- `rrt-cli`: validation, runtime fixture execution, state-diff tools, and repo-health checks +- `rrt-hook`: minimal Windows DLL scaffold for low-risk in-process loading, capture, and later + integration experiments under Wine + +For the current headless runtime smoke path, use `cargo run -p rrt-cli -- runtime summarize-fixture +fixtures/runtime/minimal-world-step-smoke.json` or one of the broader runtime fixtures under +`fixtures/runtime/`. + +For the current hook smoke test, run `tools/run_hook_smoke_test.sh`. It builds the PE32 proxy, +copies it into the local RT3 install, launches the game briefly under Wine with +`WINEDLLOVERRIDES=dinput8=n,b`, and expects `rrt_hook_attach.log` to appear. diff --git a/docs/re-workflow.md b/docs/re-workflow.md index 389f89d..41a2073 100644 --- a/docs/re-workflow.md +++ b/docs/re-workflow.md @@ -172,11 +172,26 @@ directory included. ## Naming Rules - Names should prefer behavior over implementation detail when behavior is known. +- Prefer the shape `owner_verb_object[_qualifier]`. +- Prefer one primary verb, one primary object, and at most one qualifier. - If behavior is only partly known, keep a neutral prefix such as `subsystem_` or `unk_`. - Address-derived placeholder names are acceptable, but only as temporary rows. - Every renamed function should keep a short note explaining why the name is justified. - For high-level passes, prioritize names that clarify loop role, ownership, or handoff semantics over names that only describe a local helper's mechanics. +- Prefer `try_` for best-effort helpers that may fall through without mutation or publication. +- Prefer `apply_` when a helper commits one selected policy or state transition. +- Reserve `evaluate_` for read-heavy helpers that classify or score state without committing the later action themselves. +- Prefer one stable family noun once a transient runtime structure is grounded. +- Use `queue_node` for transient linked-list allocations, and reserve `record` for persisted rows or document-style payloads. +- Prefer `startup_company` over `company_start` when the object is the newly started company. +- Prefer participial qualifiers such as `_ignoring_territories` over `_with_*_ignored` once the side condition is grounded. +- Drop filler tails such as `_lanes` once a broader owner is grounded well enough to carry the family directly. +- Prefer `_and_optionally_` over `_with_optional_` when a helper may take one secondary path but the main owner is still singular. +- Treat `_and_`, `_with_`, `_if_`, and `_via_` as fallback tools for still-uncertain seams, not as + the default naming shape. +- Raw offset tails such as `field_0xNN` are acceptable for accessors and low-confidence rows, but + should be dropped once a stable semantic field meaning is grounded. ## Confidence Rules diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index bf8d393..8c90b5f 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -1,2120 +1,20 @@ # Rehost Queue -Working rule: +This file is the short active queue for the current runtime and reverse-engineering frontier. -- Do not stop after commits. -- After each commit, check this queue and continue. -- Only stop if the queue is empty, the remaining work cannot be advanced by any further non-hook - work without guessing, or you need approval. -- Before any final response, state which stop condition is true. If none is true, continue. -- `final` responses are stop-only. -- Do not send placeholder or status-only `final` responses such as `Continuing.` or `Working on - it.`. -- If no stop condition is true, keep working and use `commentary` updates only. +## Working Rule -## Next +- Continue after each commit unless the queue is empty, a real blocker remains, or approval is needed. +- Keep detailed branch notes, long evidence dumps, and retired queue state in `docs/rehost-queue/`. +- Prefer linking to preserved notes instead of growing this file into another worklog. -- Make the current Tier-2 head explicit: the `00`-name mystery is closed, and the next source - pass is the stock style-family loader strip above the later banked clone pass. Direct recovery - now shows: - - `0x00416ce0` is the stock `gpdBuildingTypeDB` `%1*.bty` load callback and already rewrites - bare `port` / `warehouse` to `Port00` / `Warehouse00` - - `0x004196c0` is the stock import owner that walks `%1*.bty` matches, routes them through - `0x00414490`, calls the same `0x00416ce0` load callback, and only then tails into - `0x00419230`; the current recovered source names are - `StationSml`, `StationMed`, `StationLrg`, `ServiceTower`, `Maintenance`, `ClpBrd`, `Kyoto`, - `Persian`, `SoWest`, `Tudor`, and `Victorian` - - `0x00419230` is then the later two-bank, twelve-ordinal clone-and-rename pass that stamps - `Warehouse%02d` / `Port%02d` - - so the next honest question is which source rows out of that stock style-family strip first - satisfy each later `(bank, ordinal)` pair before `0x00419230` clones them - - that means the queue should keep tracing `0x004196c0 -> 0x00414490 -> 0x00416ce0 -> 0x00419230` - and its - callsites, not return to the older generic hidden-writer hypothesis +## Current Active Items -- Treat the periodic-company trace as the main shellless simulation frontier now that the - infrastructure footer-bit residue is layout/presentation-owned. The checked-in - `runtime inspect-periodic-company-service-trace ` report now exposes concrete branch - owners instead of generic blockers: - - `industry_acquisition_side_branch` carries - `0x004019e0 -> 0x004014b0` with the city-connection sibling `0x00406050` - - `city_connection_announcement` carries - `0x004019e0 -> 0x00406050` plus peer helpers - `0x00420030 / 0x00420280 / 0x0047efe0` - - `linked_transit_roster_maintenance` carries - `0x004019e0 -> 0x00409720 -> 0x004093d0 / 0x00407bd0 -> 0x00408f70 -> 0x00409950` - - the linked-transit timing seam is now grounded save-side too: - `[company+0x0d3e]` is the shorter peer-cache refresh counter, - `[company+0x0d3a]` is the heavier autoroute site-score refresh counter, and - the route-anchor tuple `[company+0x0d35] / [company+0x7664/+0x7668/+0x766c]` remains save-native - - the train-side follow-ons are bounded too: - `0x00408280 / 0x00408380` are the ranked-site chooser and staged autoroute-entry builder above - the rebuilt site caches, and `0x00409770 / 0x00409830 / 0x00409950` are the append/add/balance - strip above that - - the chooser-local cache words are bounded too: - `[site+0x5c1]` is a live occupancy/count lane reset by `0x00481910` and adjusted by - `0x004819b0`, with counts sourced from current-site-id resolver `0x004a9340`; meanwhile - `[site+0x5c5]` is a world-counter age lane stamped at `0x004aee2b` - - the per-company cache root is bounded too: - `[site+0x5bd]` is allocated by `0x00407780` as a 0x20-entry table of 0x1a-byte per-company - cache cells and freed by `0x004077e0` - - the per-company cache-cell layout is bounded too: - bytes `+0x00/+0x01` gate participation, dwords `+0x02/+0x06/+0x0a` hold peer count, peer - pointer, and peer-cache refresh stamp, and floats `+0x0e/+0x12/+0x16` are the - weighted/raw/final score lanes - - the persisted-vs-live split is tighter now too: - the minimal save-backed identity set is `[site+0x276]`, `[site+0x04]`, `[site+0x2a4]`, - `[site+0x2a8]`, `[peer+0x04/+0x08]`, `[company+0x0d35/+0x0d56/+0x7664/+0x7668/+0x766c]`, - and world calendar lanes `[world+0x15/+0x0d]`, while the actual cache contents at - `[site+0x5bd]`, `[site+0x5c1/+0x5c5]`, and `[site+0x0e/+0x12/+0x16]` are live rebuilt scratch - lanes under `0x004093d0 / 0x00407bd0 / 0x00481910 / 0x004819b0 / 0x004aee2b` - - the upstream live-table owners are tighter now too: - active-company refresh owner `0x00429c10` walks the live company roster and re-enters - `0x004093d0`; candidate table root `0x0062ba8c` is world-load owned by - `0x0041f4e0 -> 0x0041ede0 -> 0x0041e970`; and route-entry tracker compatibility / chooser - helpers `0x004a6360 / 0x004a6630` already sit under owner-notify refresh `0x00494fb0` - - the placed-structure replay strip is tighter now too: - `0x00444690 -> 0x004133b0 -> 0x0040ee10 -> 0x0040edf6 -> 0x00480710` already republishes - anchor-side linked-peer ids, route-entry anchors, and world-cell owner chains, and later - runtime path `0x004160aa -> 0x0040ee10` re-enters the same family outside bring-up - - the subtype-`4` follow-on is tighter now too: - `0x0040eba0` already republishes `[site+0x2a4]` through `0x004814c0 / 0x00481480` plus - world-cell chain helpers `0x0042c9f0 / 0x0042c9a0`, so self-id replay is no longer the open - linked-transit blocker - - the owner-company branch is tighter now too: - direct inspection of `0x0040ea96..0x0040eb65` shows that it consumes `[site+0x276]` and - branches through owner/company helpers, but does not itself rehydrate `[site+0x276]` - - the nearby selected-company and live-site helper strip is tighter now too: - `0x004337a0` is the raw selected-company getter over `[world+0x21]`, and the adjacent - world-side helpers `0x00452d80 / 0x00452db0 / 0x00452fa0` are live selected-site or active - service-state setters/dispatchers over `[world+0x217d/+0x2181]` gated by mode byte - `[world+0x2175]`, not restore-time republishers for `[site+0x276]` - - the base placed-structure load callback is narrower now too: - local `.rdata` at `0x005cb4c0` shows the shared base table, not the `0x005c8c50` - specialization table, owns the `0x0045c150 / 0x0045b560 / 0x00455870 / 0x00455930` - load-save quartet; direct disassembly of `0x0045c150 -> 0x00455fc0` shows that callback only - reloads the generic `0x55f1/0x55f2/0x55f3` triplet/scalar bands and then re-enters the base - triplet/scalar slots `0x00455870 / 0x00455930`, so it does not repopulate `[site+0x276]` - - that makes the next linked-transit question narrower: - identify which earlier restore or service owner feeds `[site+0x276]` and the live linked-peer - rows before replay continuation `0x0040e360..0x0040edf6`, beyond the already-grounded - `0x00480710` anchor-side refresh, before `0x004093d0 / 0x00407bd0 / 0x004a6630`, because the - candidate table, route-entry-tracker owners, replay-strip framing, subtype-`4` self-id replay, - bounded train-side strip - `0x00409770 / 0x00409830 / 0x00409950`, and cache-cell semantics are no longer the blocker -- Make the next static/rehost slice the near-city industry acquisition owner seam under - `0x004014b0`, not another generic infrastructure pass. The concrete questions are: - - which minimum persisted peer-site fields on the already-grounded `0x006cec20` placed-structure - collection feed near-city unowned-industry candidates - - which placed-structure, city-or-region, and company linkage survives save/load strongly enough - to drive the proximity scan - - whether the acquisition branch can be rehosted as a shellless sibling beside the already - grounded annual-finance helper - - the save-side `0x36b1/0x36b2/0x36b3` triplet seam is now also loaded into the checked-in - save-slice model as first-class `placed_structure_collection` context, carrying stem pairs plus - grounded footer/policy status lanes instead of remaining inspection-only evidence - - the separate `0x38a5/0x38a6/0x38a7` side-buffer seam is now also loaded into the save-slice - model as first-class `placed_structure_dynamic_side_buffer_summary` context, carrying the - grounded owner-shared dword, compact-prefix summaries, name-pair summaries, and overlap counts - against the triplet corpus instead of remaining trace-only evidence - - the save-side `0x55f1/0x55f2/0x55f3` region triplet seam is now also loaded into the - save-slice model as first-class `region_collection` context, carrying region names, the - grounded policy lanes, reserved-policy dwords, and embedded profile rows instead of leaving - region triplets inspection-only - - the save-side fixed-row run candidate family is now also the next save-slice seam to keep - first-class once grounded, because it is the closest save-native candidate family to the - still-missing cached tri-lane inputs behind `[site+0x310/+0x338/+0x360]` - - cross-save fixed-row comparison is tighter now too: - `runtime compare-region-fixed-row-runs p.gms q.gms` shows shared shape-family matches even - though the best raw `rows_offset` drifts between saves, so the tri-lane-adjacent row family - should be treated as a stable shape-family seam rather than a fixed-offset seam - - the periodic-company trace now carries that save-native seam directly too: - it exposes the current top tri-lane-adjacent fixed-row shape-family candidates with row-count, - stride, rows-offset, and probable-density-lane hints, so the tri-lane frontier is now - structured as “save shape-family candidates present, fixed offset ruled down” instead of only - a prose note - - the `0x5dc1/0x5dc2` serializer bundle is tighter now too: - atlas-backed recovery bounds `0x0040c980 -> 0x0045b560` as emitting the derived payload over - `[site+0x23e/+0x242/+0x246/+0x24e/+0x252]`, so the remaining restore-owner question should - treat that persisted selector/child/runtime bundle as one seam rather than only - `[site+0x23e/+0x242]` - - the loader-side counterpart narrows the minimum shellless identity subset too: - `0x0045c150` restores `[owner+0x23e/+0x242]`, clears the transient roots, and then hands off - to `0x0045c310 / 0x0045b5f0 / 0x0045b6f0` to rebuild the primary child handle plus the larger - ambient/animation/light/random-sound family, so current shellless planning can keep the - minimum persisted subset at cached ids `[site+0x3cc/+0x3d0]`, restored name-pair - `[owner+0x23e/+0x242]`, and the post-secondary discriminator byte while treating - `[owner+0x246/+0x24e/+0x252]` as part of the broader saved bundle that still flows through the - rebuild side - - the periodic-company trace now surfaces the strongest non-transport owner-company candidate - family directly too: - ordinary loaded runtime-effect lane - `0x00444d92 -> 0x00432f40(kind 8) -> 0x004323a0 -> 0x00431b20` above the non-direct - `0x4e99/0x4e9a/0x4e9b` bundle, with the remaining gap narrowed to the control-lane mapping - from loaded rows into trigger kind `8` and then into the placed-structure mutation opcodes -- Direct disassembly now narrows that acquisition strip further: - - `0x004014b0` scans the live placed-structure collection at `0x0062b26c` - - `0x0041f6e0 -> 0x0042b2d0` is the center-cell token gate over the current region - - `0x0047de00 -> 0x0040c990` is the linked-region status branch reused from the city-connection - helper strip - - `0x004801a0` is the route-anchor reachability gate for one candidate site through - `0x00401860 -> 0x0048e3c0` - - the company-side half of that gate is now explicit too: `0x00401860` validates or rebuilds the - cached linked-transit route-anchor entry id `[company+0x0d35]` from the live route-entry - collection using fallback count lanes `[company+0x7664/+0x7668/+0x766c]` - - those four company lanes are now threaded into save-native company market state, so the - route-anchor side of the acquisition gate is no longer just a trace-only blocker - - `0x0040d360` is the subtype-`4` predicate over the current placed-structure subject's - candidate byte `[candidate+0x32]` - - `0x0040d540` scores site/company proximity with pending-bonus context - - `0x0040cac0` samples the cached site tri-lane at `[site+0x310/+0x338/+0x360]` - - `0x00405920` walks same-company linked site peers above the live placed-structure / peer-site - collection seam - - `0x00420030 / 0x00420280` is the boolean/selector peer-site pair over `0x006cec20`, combining - `0x0042b2d0`, the optional company filter through `0x0047efe0`, the station-or-transit gate - `0x0047fd50`, and the status branch `0x0047de00 -> 0x0040c990` - - `0x0047efe0` and `0x0047fd50` both consume `[site+0x04]` as the live backing-record selector - - `0x00480210` writes linked-peer row `[peer+0x04]` from the anchor-site id argument - - `0x0040f6d0 -> 0x00481390` writes the anchor-site linked peer id back into `[site+0x2a8]` - - `0x0047dda0` consumes `[peer+0x08]` as the linked route-entry anchor id - - `0x0041f7e0 / 0x0041f810 / 0x0041f850` already ground `[site+0x2a4]` as the record's own - placed-structure id lane beneath the peer-chain helpers - - `0x0040d210` is the owner-side placed-structure resolver from `[site+0x276]` through - `0x0062be10` - - `0x00480710 -> 0x0048abc0 / 0x00493cf0` is the linked-site refresh and route-entry rebind or - synthesis strip above that anchor lane - - the same `0x00480710` replay strip now also republishes the concrete world-cell owner chains: - `0x0042bbf0 / 0x0042bbb0` remove or prepend the current site in the owner chain rooted at - `[cell+0xd4]`, while `0x0042c9f0 / 0x0042c9a0` remove or prepend it in the linked-site chain - rooted at `[cell+0xd6]` - - late world bring-up `0x00444690` is the current caller of - `0x004133b0 placed_structure_collection_refresh_local_runtime_records_and_position_scalars` - - `0x004133b0` drains queued site ids through `0x0040e450` and then sweeps all live sites - through `0x0040ee10` - - `0x0040ee10` reaches `0x0040edf6 -> 0x00480710` for linked-peer refresh and then the later - `0x0040e360` follow-on - - `0x004160aa` is a separate non-bring-up runtime caller of `0x0040ee10` - - the direct `0x36b1` per-record callbacks serialize base scalar triplets - `[this+0x206/+0x20a/+0x20e]` plus the subordinate payload callback strip, and the - `0x4a9d/0x4a3a/0x4a3b` side-buffer owner only persists route-entry lists, three byte arrays, - five proximity buckets, and the sampled-cell list - - `0x004269b0` consumes the chosen site's own placed-structure id lane `[site+0x2a4]` -- That leaves the acquisition blocker set tighter than before: - - peer-site and linked-site replay seams are grounded enough for planning - - the live owner-company meaning of `[site+0x276]` is already grounded through `0x0047efe0`, - with the direct owner-side resolver bounded at `0x0040d210` - - `[site+0x2a4]` is already grounded as the record's own placed-structure id lane through the - peer-chain helpers `0x0041f7e0 / 0x0041f810 / 0x0041f850`, and constructor-side `0x00480210` - already seeds that lane for new linked-site rows - - direct disassembly now also shows `0x004269b0` resolving one chosen site id through live - placed-structure collection `0x0062b26c` before mutating `[site+0x276]`, so the - `[site+0x2a4]` self-id lane is reconstructible from collection identity rather than needing a - separate serializer-owned projection - - the subtype byte consumed as `[candidate+0x32] == 4` is already bounded under the - aux-candidate load/stem-policy chain - `0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0` - - remaining non-hook gaps are the save or replay projection of `[site+0x276]` and the cached - tri-lane `[site+0x310/+0x338/+0x360]` - - the checked-in periodic-company trace now exposes those gaps as structured statuses instead of - only prose: - - site owner-company lane = `live_meaning_grounded_projection_missing` - - site self-id lane = `live_meaning_grounded_reconstructible_from_collection_identity` - - site cached tri-lane = `delta_reader_grounded_projection_missing` - - candidate subtype lane = `cached_candidate_id_bridge_grounded_via_stream_load` - - backing-record selector bridge = `stream_load_callback_grounded_via_0x40ce60` - - the same trace now also carries three explicit projection hypotheses for the next pass: - - `site_owner_replay_from_post_load_refresh_self_id_reconstructible` - - `site_cached_tri_lane_payload_or_restore_owner` - - `cached_source_candidate_id_to_subtype_projection` - - the first of those is now bounded more tightly: - `0x004133b0 -> 0x0040e450 / 0x0040ee10` rebuilds cloned local-runtime records and local - position/scalar triplets, but direct constructor and caller recovery now shows the - owner-company lane also living under the create-side allocator/finalize family - `0x004134d0 -> 0x0040f6d0 -> 0x0040ef10`, plus data-driven loader callers - `0x0046f073 / 0x004707ff -> 0x0040ef10` - - the checked-in replay strip is narrower than before now: - direct local inspection now splits it more precisely: - `0x0040ee10` itself only reads cached source lane `[site+0x3cc]` in the checked range, and - the bounded `0x00480710` neighborhood is working from `[site+0x04]`, `[site+0x08]`, and - `[site+0x3cc]`; but the broader immediate continuation `0x0040e360..0x0040edf6` still - consumes `[site+0x2a8]`, `[site+0x2a4]`, and `[site+0x276]` around - `0x0040d230 / 0x0040d1f0 / 0x00480710 / 0x00426b10 / 0x00455860`, so the replay family is - narrowed rather than ruled out for owner-company rehydration; in the checked range those - `[site+0x276]` uses are still reads/queries rather than a direct rehydrating store - - the create-side owner family is grounded too now: - `0x004134d0` allocates a new row through `0x00518900`, `0x0040f6d0` seeds `[site+0x2a4]`, - copied name bytes, `[site+0x276]`, `[site+0x3d4/+0x3d5]`, and cleared local caches, and the - shared finalize helper `0x0040ef10` has both create-side callers `0x00403ef3 / 0x00404489` - and data-driven loader callers `0x0046f073 / 0x004707ff` - - one persisted tuple path is grounded too now: - the data-driven loader callers `0x0046f073 / 0x004707ff` push tuple fields - `[+0x00/+0x04/+0x0c]` into `0x0040ef10`, and inside that helper arg3 becomes `ebx` and then - `[site+0x276]` at `0x0040f5d4` - - that tuple path is classified further now too: - the checked-in atlas ties `0x004707ff` to multiplayer transport selector-`0x13` body - `0x004706b0`, which attempts the placed-structure apply path through - `0x004197e0 / 0x004134d0 / 0x0040eba0 / 0x0052eb90 / 0x0040ef10` - - the neighboring batch builder is classified too: - `0x00472b40` is the multiplayer transport selector-`0x72` counted live-world apply path, and - its inner builders `0x00472bef / 0x00472d03` reach `0x004134d0` from counted transport - records rather than ordinary save-load restore - - one surviving non-transport `0x004134d0` caller is bounded away from persisted restore too: - `0x00422bb4` pushes one live `0x0062b2fc` record plus local args and literal flags `1/0` - into `0x004134d0`, then returns the created row id through an out-param instead of feeding - the tuple-backed finalize path - - the remaining `0x00508fd1 / 0x005098eb` family is bounded away too: - it caches the created site id in `[this+0x7c]`, re-enters `0x0040eba0` with immediate coords, - and later calls `0x0040ef10` with a hard zero third arg, so it reads as another live - controller path rather than the missing persisted owner seam - - the adjacent `0x00473c20` family is bounded away too: - it drains queued site ids and coordinate pairs from scratch band `0x006ce808..0x006ce988`, - re-enters `0x0040eba0` at `0x00473c98`, and clears each queued id slot, so it is a local - post-create refresh path rather than a persisted replay owner - - the remaining direct `[site+0x276]` store census is bounded away too: - `0x0042128d` is broad zero-init in the `0x00421430` constructor neighborhood, - `0x00422305` computes a live score/category lane before publishing event `0x7`, - `0x004269c9/0x00426a2a` are acquisition commit/clear helpers, and - `0x004282a9/0x004300d6` are bulk owner-transfer writes - - the paired tagged triplet serializer is bounded away too: - `0x00413440` is the save-side `0x36b1/0x36b2/0x36b3` serializer, dispatches each live record - through vtable slot `+0x44`, and keeps that seam on the already-grounded triplet payload - rather than the missing `[site+0x276]` replay owner - - the ordinary bring-up strip is narrower too: - `0x00444690 -> 0x004133b0` is still the checked ordinary restore-side replay owner above live - placed structures, but it only drains queued local-runtime ids through `0x0040e450` and then - sweeps live rows through `0x0040ee10`; after that, bring-up proceeds into later route-entry, - grid, and tagged refresh owners rather than re-entering the constructor/finalize family - `0x004134d0 / 0x0040f6d0 / 0x0040ef10` - - the ordinary restore staging order is explicit now too: - world bring-up calls the tagged `0x36b1/0x36b2/0x36b3` stream-load owner `0x00413280` at - `0x00444467`, refreshes the placed-structure dynamic side buffers through `0x00481210` at - `0x004444d8`, and only later enters the queued local-runtime replay owner - `0x00444690 -> 0x004133b0 -> 0x0040ee10` - - the broader load-side stream owner is separate too: - `0x00413280` is the actual tagged `0x36b1/0x36b2/0x36b3` stream-load owner, dispatching - per-entry vtable slot `+0x40`; current local recovery still only grounds that seam through the - cached-source bridge `0x0040ce60 -> 0x0040cd70 / 0x0045c150`, not through a direct - `[site+0x276]` republisher - - the grouped-opcode family is narrower rather than fully ruled down: - `0x00431b20` is still only reached through scenario runtime-effect service - `0x004323a0 -> 0x00432f40` via direct call `0x00432317`, but that service loop is itself - called from world bring-up at `0x00444d92` with trigger kind `8` under shell-profile latch - `[0x006cec7c+0x97]`; so `0x0061039c` currently reads as a startup-time live runtime-effect - application lane rather than an ordinary tagged restore owner - - that `kind 8` branch is ordinary in one important way now too: - restore-side loader `0x00433130` repopulates the live event collection `0x0062be18` from packed - chunk family `0x4e21/0x4e22`, and the event-detail editor strip - `0x004d90ba..0x004d91ed` writes trigger field `[event+0x7ef]` across the full `0x00..0x0a` - range through controls `0x4e98..0x4ea2`, including kind `8` at `0x004d91b3`; so the remaining - startup compact-effect question is no longer whether kind `8` is a special synthetic class, but - which loaded kind-`8` rows in `0x0062be18` can actually reach the placed-structure mutation - opcode families under `0x00431b20` - - the event-detail editor family tightens that one step further too: - selected-event control root `0x4e84` and refresh strip `0x004db02a / 0x004db1b8..0x004db309` - mirror current trigger field `[event+0x7ef]` back into those same `0x4e98..0x4ea2` controls, - while editor-side builder `0x004db9e5..0x004db9f1` allocates an ordinary runtime-effect row - into `0x0062be18` through `0x00432ea0`; so the remaining startup compact-effect question is no - longer whether kind `8` lives on a separate editor/build class either, but which loaded - kind-`8` rows actually carry the mutation-capable compact payloads - - bundle-side inspection now grounds the startup collection itself: - sampled maps such as `War Effort.gmp`, `British Isles.gmp`, `Germany.gmp`, and - `Texas Tea.gmp` expose non-direct `0x4e99/0x4e9a/0x4e9b` runtime-event collections, and the - compact `0x526f/0x4eb8/0x4eb9` row family is now decoded into actual condition/grouped row - summaries rather than opaque slices - - the rehosted collection summary now makes the remaining control-lane gap explicit too: - it reports `records_with_trigger_kind`, `records_missing_trigger_kind`, - `nondirect_compact_record_count`, `nondirect_compact_records_missing_trigger_kind`, and - `control_lane_notes`; real `War Effort.gmp` output currently shows `24/24` compact non-direct - rows still missing decoded trigger kind, which narrows the next owner question to the - non-row-body control lane rather than the compact row framing itself - - the same per-map summary now surfaces the add-building subset directly too: - it reports `add_building_dispatch_strip_record_indexes`, - `add_building_dispatch_strip_descriptor_labels`, and the matching - `add_building_dispatch_strip_records_with_trigger_kind` / - `add_building_dispatch_strip_records_missing_trigger_kind` counts, so inspected maps can now - show whether `Add Building` rows are present and still null-trigger without a separate cluster - pass - - that add-building subset is structured one layer deeper now too: - the per-map summary and the compact-dispatch cluster/counts reports both surface - add-building signature families, condition-tuple families, and combined - signature/condition clusters, so the remaining trigger-kind/source question can compare exact - compact subfamilies instead of only raw descriptor hits - - the first concrete add-building cluster split is now visible too: - targeted map inspections show `Texas Tea.gmp` carrying a one-row `Port01` cluster on - signature family `nondirect-ge1e-h0001-0007-0000-6d00-0200-p0000-0000-0000-ffff` with - condition family `[7:0]`, while `Alternate USA.gmp` carries repeated three-row - `FarmGrain`/`Logging Camp` clusters on `nondirect-ge34-h0002-0007-0004-73|75|7b|85...` with - condition family `[7:4,42:0]`; that narrows the remaining trigger-kind/source question to a - small set of compact subfamilies rather than a single undifferentiated add-building carrier - - the first exclusivity pass is positive too: - the widened cluster-counts report now surfaces, for each add-building - signature/condition cluster, the descriptor keys that share it plus the - non-add-building subset. Current targeted checks on `Texas Tea.gmp` and `Alternate USA.gmp` - show those observed add-building clusters have empty non-add-building companions, which biases - the next source/trigger-kind pass toward a distinct add-building compact subfamily rather than - a generic dispatch carrier reused by variable-band rows - - the descriptor-independent row-shape split is explicit now too: - the same cluster-counts surface reports normalized add-building row shapes. - Current targeted checks show `Texas Tea.gmp` on the one-row shape `[0:8:0]`, while - `Alternate USA.gmp` sits on the repeated three-row shape - `[0:8:-25,0:8:0,0:8:0]`; that gives the next pass a simpler shape-level discriminator even - when descriptor labels differ - - the shipped-map corpus is checked in now too: - `artifacts/exports/rt3-1.05/add-building-compact-dispatch-corpus.json` records the full - six-map add-building carrier set in bundled RT3 1.05 maps: - `Alternate USA`, `Chicago to New York`, `Louisiana`, `Pacific Coastal`, - `Rhodes Unfinished`, and `Texas Tea`. Across that corpus the normalized row-shape totals are - only four families: - `[0:8:-25,0:8:0,0:8:0] = 4`, - `[0:8:0,0:8:0,0:8:0,0:8:0] = 1`, - `[0:8:0,0:8:0,0:8:0] = 1`, - `[0:8:0] = 4`. - The accompanying signature/condition cluster totals now show the add-building carrier set is - broader than the first two sampled maps but still small enough to treat as a bounded compact - subfamily frontier rather than a diffuse generic event carrier. - - the same probe now narrows the candidate runtime-effect set too: - it reports which decoded records already carry grouped opcodes in the grounded - `0x00431b20` dispatch strip; real `War Effort.gmp` currently narrows that to record indexes - `[1, 9, 12, 13, 14, 15, 16, 17, 22]`, all through opcode `4`, with descriptor labels - `Game Variable 1`, `Company Variable 1..4`, and `Player Variable 1`, so the next pass can - focus on that smaller subset instead of the full 24-row compact collection while avoiding the - earlier overclaim that opcode `4` alone already proves a placed-structure mutation row - - the sampled-map framing split is narrower now too: - targeted real-map inspections of `Texas Tea.gmp`, `British Isles.gmp`, and `Germany.gmp` - still show their entire event-runtime collections as nondirect compact - `0x4e99/0x4e9a/0x4e9b` rows with `records_with_trigger_kind = 0`, even when grouped rows - already reach the grounded `0x00431b20` dispatch strip. That means the direct full-record - `0x4e21/0x4e22` parser path is not currently bridging `[event+0x7ef]` for the ordinary - mutation-capable rows in those sampled maps. - - the widened compact-cluster probe now makes that same gap first-class too: - `runtime inspect-compact-event-dispatch-cluster ` now reports all dispatch-strip - descriptor occurrences, their `trigger_kind`, payload family, and the aggregated - `dispatch_descriptor_occurrence_counts` / `dispatch_descriptor_map_counts` summaries instead of - only unknown descriptor ids. On `Texas Tea.gmp`, the widened report now shows - `548 Add Building Port01`, `8 Economic Status`, and `43 Company Variable 1` all arriving on - `real_packed_nondirect_compact_v1` rows with `trigger_kind = null`, while the count summary - reports `Company Variable 1 = 4` occurrences and the other two once each, so the next pass can - work from a checked scalable probe rather than ad hoc `inspect-smp` scrapes. - - the scalable summary sibling is grounded too: - `runtime inspect-compact-event-dispatch-cluster-counts ` reuses the same analysis - but emits only the corpus-level count fields, so the next broader map-install pass no longer - needs to wade through every occurrence payload just to compare descriptor or trigger-kind - coverage; the same counts output now also carries an explicit add-building subset so the - acquisition-side branch can compare `Add Building` records and their still-missing trigger-kind - coverage without grepping the full occurrence dump. - - the installed-map totals are grounded now too: - current `rt3_105/maps` corpus gives `41` bundled maps, `38` maps with dispatch-strip rows, and - `318` dispatch-strip records total, all on `real_packed_nondirect_compact_v1` with - `dispatch_strip_records_with_trigger_kind = 0`; the add-building subset inside that corpus is - only `10` grouped occurrences across `7` recovered descriptor keys - (`Barracks`, `Bauxite Mine`, `FarmGrain`, `Furniture Factory`, `Logging Camp`, `Port01`, - `Warehouse05`), again all still missing trigger kind - - the per-record trigger gate is explicit now too: - direct disassembly of `0x004323a0` shows the service returns before dispatch unless - one-shot latch `[event+0x81f]` is clear, mode byte `[event+0x7ef]` matches the selected - trigger-kind argument from `0x00432f40`, and the compact chain root `[event+0x00]` is - nonzero. The kind-`8` side path at `0x00432ca1..0x00432cb0` only calls `0x00438710` on - already-live records carrying `[event+0x7ef] == 8`. So the remaining ordinary bundle question - is no longer “does the service loop itself bypass the per-record trigger byte?”; it is which - later owner, if any, materializes or retags `[event+0x7ef]` for the nondirect compact rows. - - the post-load retagger is narrower than that missing owner too: - direct disassembly of `0x00442c30` (called from `0x00443a50` at `0x00444b50`) shows a - hardcoded scenario-name patch table over already-live records in `0x0062be18/0x0062bae0`. - The checked cases mostly tweak modifier bytes `[event+0x7f9/+0x7fa]` or nested payload scalars - on records that already carry concrete kinds such as `7` (`Open Aus`, `The American`), `6` - (`Test connections`), `5` (`Win - Gold`), and `1` (`Win - Silver` / `Win - Bronze`). So the - remaining trigger-kind frontier is no longer “maybe the name sweep bulk-materializes null - - the startup kind-`8` owner strip has a dedicated checked artifact now too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.{dot,md}` seeds - `0x00444d92`, `0x00432f40`, `0x004323a0`, `0x00431b20`, and `0x00433130` at depth `5`, - giving the current ordinary bring-up path a reusable static-analysis surface instead of only - scattered queue prose - - that artifact narrows the immediate owner one step higher too: - the ordinary restore/service pair `0x00433130` and `0x00432f40` now sit directly under - `world_entry_transition_and_runtime_bringup` `0x00443a50` in the checked subgraph, alongside - `0x004133b0` placed-structure refresh, `0x00421510` region refresh, and the - `0x00433bd0/0x00434130/0x004354a0/0x00435603` year-band/state refresh strip. That means the - next static recovery pass can focus on `0x00443a50` ordering and preconditions instead of - treating `0x00444d92` as a floating standalone caller. - - there is now a smaller checked artifact for that owner too: - `artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.{dot,md}` seeds only - `0x00443a50` at depth `3`, giving a 343-node / 989-edge bringup-refresh surface that is - easier to work from than the broader kind-`8` startup export when the immediate question is - just ordering and neighboring refresh owners. - - the saved-runtime-state loader is boxed in one step lower now too: - `artifacts/exports/rt3-1.06/world-load-saved-runtime-state-subgraph.{dot,md}` seeds - `0x00446d40` at depth `3` and shows the loader directly reaches - `0x00433130 scenario_event_collection_refresh_runtime_records_from_packed_state`, but not - `0x00432f40`. That shifts the remaining ordinary kind-`8` question upward: the unresolved - service-vs-restore ordering is now specifically inside `0x00443a50`, not inside the loader - itself. - - the bringup owner note now supplies the high-level ordering too: - current `function-map.csv` notes for `world_entry_transition_and_runtime_bringup` - `0x00443a50` already state that the tagged load phase reloads event runtime records through - `0x00433130`, while the one-shot kind-`8` runtime-effect service through `0x00432f40` only - runs much later in the final reactivation tail, after the candidate/region/year-band refresh - strip and before shell-profile latch `[0x006cec7c+0x97]` is cleared. That means the remaining - unknown is no longer restore-vs-service ordering in the abstract; it is which late bringup - branch or retagger between those two points materializes the live kind-`8` records that carry - the mutation-capable compact payloads. - - the late retagger remains a prose-first seam: - a direct `0x00442c30` subgraph export currently collapses to the seed node only, because the - grounded function-map note carries the useful scenario-title and collection-mutation detail but - not many mapped-address backrefs. So the post-load scenario-fixup branch should currently be - treated as a manual-note recovery seam rather than one the generic subgraph exporter can answer - by itself. - - the relevant prose is checked in separately now too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-late-bringup-note.md` extracts the current - late-bringup facts for `0x00446d40`, `0x00443a50`, `0x00442c30`, and the explicit - `SP - GOLD` / `Labor` trigger-kind rewrites, so the next pass can work from one bounded note - instead of stitching the same ordering back together from the queue plus function-map prose. - - the shipped add-building carrier corpus no longer supports the older filename-mismatch bias: - the checked report - `artifacts/exports/rt3-1.05/add-building-map-title-hints.json` - now scans the six bundled carrier maps in - `artifacts/exports/rt3-1.05/add-building-compact-dispatch-corpus.json` - against the grounded `0x00442c30` title set (`Go West!`, `Germany`, `France`, - `State of Germany`, `New Beginnings`, `Dutchlantis`, `Britain`, `New Zealand`, - `South East Australia`, `Tex-Mex`, `Germantown`, `The American`, `Central Pacific`, - `Orient Express`). - - the new title-hint probe narrows that evidence precisely: - five of the six shipped carrier maps now show at least one grounded retagger-title hit, - but only one map currently shows an adjacent embedded `.gmp` reference plus grounded title and - only one shows a same-stem pair. `Louisiana.gmp` carries - `Dutchlantis.gmp` / `Dutchlantis` at offset `0x73d0` with zero byte distance, while the other - current carrier-map hits stay weaker (`Alternate USA.gmp` late `Germany` / `France` / - `Britain`, `Chicago to New York.gmp` late `Germany` / `France`, - `Pacific Coastal.gmp` later `Central Pacific`, `Texas Tea.gmp` later `Germany`, - `Rhodes Unfinished.gmp` no current hit). - - that keeps the title-fixup branch alive but no longer as a broad filename-level explanation: - the evidence now supports a narrow “one strong `Louisiana -> Dutchlantis` overlap plus several - weaker prose-only or late-string overlaps” reading rather than a clean one-to-one mapping from - the shipped add-building carrier filenames to the grounded `0x00442c30` scenario-title set. - - the direct runtime-event comparison narrows it further too: - the checked note - `artifacts/exports/rt3-1.06/runtime-effect-kind8-title-overlap-note.md` - shows that `Louisiana.gmp` is the only carrier with a same-stem - `Dutchlantis.gmp` / `Dutchlantis` pair, but `Dutchlantis.gmp` itself still has no current - add-building dispatch rows while `Louisiana.gmp` keeps the one-row - `Add Building Warehouse05` cluster on - `nondirect-ge1e-h0001-0007-0000-5200-0200-p0000-0000-0000-ffff :: [7:0]`. So the strongest - current title overlap still does not reproduce the actual shipped add-building row family. - - the post-reload candidate set is checked in now too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-post-reload-candidates.md` extracts the - currently plausible late `0x00443a50` branches between ordinary reload and final kind-`8` - service, with the current bias explicitly shifted away from the known title-fixup branch and - toward the Tier 2 candidate/world-state rebuild owners rather than the Tier 3 - shell-progress/year-scalar refresh strip. - - the Tier 2 owner strip is checked in separately now too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-candidate-rebuild-subgraph.md` - shows the current bounded rebuild band rooted at `0x00412c10 / 0x00412bd0 / 0x00437737` - reaching candidate runtime-record rebuild `0x00412d70 / 0x00412fb0`, - named-availability query/upsert `0x00434ea0 / 0x00434f20`, - cargo-economy filter refresh `0x0041eac0`, - port/warehouse recipe rebuild `0x00435630`, - and only then the later world bringup / event-service neighborhood - `0x004384d0 / 0x00443a50 / 0x00432f40`. - - the strongest same-stem title pair has a Tier 2 availability note now too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-named-availability-note.md` - shows `Louisiana.gmp` and `Dutchlantis.gmp` share the direct named-availability bit for - `Warehouse05` (`1/1`), as well as `Port01`, `Furniture Factory`, `FarmGrain`, and - `Logging Camp`; their current `18` named-availability differences fall elsewhere - (`Bauxite Mine`, `AluminumMill`, `Farm Corn`, `FarmCotton`, `FarmRice`, `FarmSugar`, etc.). - The same note now folds in two deeper grounded constraints too: - `0x00412d70` does not consult the scenario-side recipe-book name at `[state+0x0fe8]`, and - `0x00434f20` writes only a boolean availability override bit. So the current Tier 2 question - is no longer “is `Warehouse05` simply toggled differently?” or “is a recipe-book display name - leaking through?” but - “which broader candidate-state rebuild or latch sequencing difference above - `0x00437737 / 0x00412c10 / 0x00412bd0 / 0x00412d70 / 0x00412fb0` leads to the shipped - `Add Building Warehouse05` row in `Louisiana.gmp`?” - The broader `compare-candidate-table` surface now reinforces the same point too: - `Louisiana.gmp` versus `Dutchlantis.gmp` stays on the same - `scenario-named-candidate-availability-table` semantic family, still keeps `Warehouse05 = 1/1`, - and moves the actual `difference_count = 43` into the wider industry mix and zero-trailer-name - set rather than a unique `Warehouse05` gate. - - that sequencing question is checked in directly now too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-sequencing-note.md` - extracts the current late order around the explicit `0x197` checkpoint: - the earlier `0x00443ebc` recipe-runtime rebuild still sits before - `0x00412c10 / 0x00412bd0 / 0x00434130 / 0x00436af0`, while the later `0x00444ac1` - checkpoint runs after `0x004354a0 / 0x00487de0` and then falls through into - `0x00437737 -> 0x00434f20 -> 0x00412c10`, with the separate recipe-runtime side re-entering - `0x00435630 -> 0x00412d70`. That keeps the next recovery pass focused on Tier 2 sequencing - interactions instead of title strings or direct `Warehouse05` availability bits. - - the strongest title-overlap pair now has a recipe-book note too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-recipe-book-note.md` - shows `Louisiana.gmp` and `Dutchlantis.gmp` diverge heavily on the recipe-book surface even - though `Warehouse05` stays `1 / 1` in the named-availability table. `Louisiana.gmp` is sparse - there (currently only `book00.line02` stays nonzero), while `Dutchlantis.gmp` keeps multiple - later mixed lines and mode words. That shifts the current Tier 2 bias further toward the - `0x00435630 -> 0x00412d70` rebuild side rather than the direct `0x00437737 -> 0x00412c10` - availability side. - The same note now also boxes in the same-condition-family counterexample: - `Louisiana.gmp` and `Texas Tea.gmp` both sit on `[7:0]`, but `Louisiana.gmp` keeps the one-row - `Warehouse05` cluster with only `book00.line02 = 0x00080000`, while `Texas Tea.gmp` keeps the - one-row `Port01` cluster with a broader four-book nonzero mode strip. So the current evidence - no longer supports “shared `[7:0]` family implies one shared Tier 2 recipe/runtime shape.” - - the bundled add-building carrier set now has a checked recipe-book corpus too: - `artifacts/exports/rt3-1.05/add-building-carriers-recipe-book-scan.json` - shows the six carrier maps split into two recipe families: - `Alternate USA.gmp` stands alone with the richer five-book nonzero mode strip, while the other - five carrier maps fall into one broader family but still diverge sharply inside it. - Among that five-map family, `Louisiana.gmp` is currently the sparsest nonzero profile - (only `book00.line02 = 0x00080000` plus the paired `book00` token lanes), while - `Chicago to New York.gmp`, `Rhodes Unfinished.gmp`, and `Texas Tea.gmp` each keep broader - multi-book nonzero mode strips and `Pacific Coastal.gmp` keeps supplied-token-only activity. - That makes `Louisiana.gmp` look less like a generic carrier-map recipe pattern and more like a - genuinely narrow Tier 2 recipe/runtime-record case. - - the recipe/runtime owner strip is checked in separately now too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-recipe-runtime-note.md` - summarizes the new subgraph rooted at `0x00435630 / 0x00412d70 / 0x00412fb0`. - The current bounded shape is a coupled rebuild family rather than a one-way ladder: - `0x00435630` re-enters `0x00412d70`, `0x00412d70` re-enters `0x00435630` plus - `0x00411ce0 / 0x00411ee0`, and `0x00412fb0` re-enters - `0x004120b0 -> 0x00412d70 -> 0x00412ab0 -> 0x00412c10`. - That keeps the next pass focused on the internal sequencing and handoff across the coupled - recipe/runtime/availability rebuild strip, not on one isolated helper. - The same note now carries the first upstream feed-in difference too: - `compare-setup-payload-core Louisiana.gmp Dutchlantis.gmp` already differs on - `payload_word_0x14` (`1870` vs `2025`), `payload_byte_0x20` (`0x3a` vs `0xfd`), - `payload_word_0x3b2` (`2` vs `1`), and the candidate-header words - (`0xcdcdcdcd` vs `0x00000000`). So the next pass can now work on both sides of the coupled - Tier 2 strip: the upstream setup payload core and the downstream recipe/runtime rebuild loop. - - the carrier-set setup-core comparison is checked in now too: - `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-setup-core-note.md` - widens that upstream comparison across all six bundled add-building carriers. It shows - `Louisiana.gmp` is not unique on `payload_word_0x3b2` (`Chicago to New York.gmp` also has `2`) - and shows `Louisiana.gmp` is unique among that six-map carrier subset on the candidate-header - sentinel pair `0xcdcdcdcd / 0xcdcdcdcd`, while `Alternate USA.gmp` stays separately unique on - the recognized `rt3-105-map-container-v1` header pair. The same note now widens further across - all 41 bundled `rt3_105` maps and shows the candidate-header pair is not actually - `Louisiana`-specific in the full corpus at all: nine maps share `0xcdcdcdcd / 0xcdcdcdcd`, - matching the older atlas read that this looks like coarse scenario-family framing instead of a - direct Tier 2 trigger. That trims the next upstream Tier 2 question again: focus on the more - specific remaining setup-core combination (`payload_word_0x14`, `payload_byte_0x20`, plus the - sparse recipe/runtime profile), not on the candidate-header class by itself. The same widened - note now records that the full current `Louisiana.gmp` tuple - (`payload_word_0x14 = 1870`, `payload_byte_0x20 = 0x3a`, `payload_word_0x3b2 = 2`, - `candidate_header = 0xcdcdcdcd / 0xcdcdcdcd`) is still unique across the 41-map corpus even - after the coarse header class is demoted, so the next pass should compare that tighter tuple to - the sparse recipe/runtime family rather than reopening the broad header-pair question. The same - note now also carries the grounded owner bridge for those compared setup-core fields. The early - copy path is still `0x00442400 -> 0x00502220 -> 0x0047be50`, but the later reset or - reactivation owners are now bounded too: - `0x00436d10` and `0x00443a50` both reimport the staged subset - `[profile+0x77/+0xc5]` before rerunning the same rebuild family that includes - `0x00435603`, `0x00435630`, `0x0041e970`, `0x00412bd0`, `0x00434130`, and `0x00436af0`. - That narrows the open question again: - `+0x14/+0x20` now have one real bridge into the Tier 2 strip through - `[profile+0x77/+0xc5]`, while `+0x3b2/+0x3ba` still stop on the setup-panel threshold or - scroll path. The next pass should therefore test whether the unique `Louisiana.gmp` - `+0x14/+0x20` pair is enough to explain its Tier 2 runtime shape through that grounded bridge, - or whether the sparse recipe/runtime side is still the more plausible differentiator. One - adjacent `compare-setup-launch-payload` check now sharpens that too: `Louisiana.gmp` does not - collapse into the nearest setup-core peers on the launch-token side either, but that band is - still not part of the currently grounded Tier 2 bridge. So the next pass should keep launch - tokens as supporting context while testing the already-grounded `+0x14/+0x20 -> [profile+0x77/+0xc5]` - bridge against the sparse recipe/runtime family. The same widened setup-core note now shows the - split inside that bridge too: `payload_byte_0x20 = 0x3a` is unique to `Louisiana.gmp` across - all 41 bundled `rt3_105` maps, while `payload_word_0x14 = 1870` has only one peer (`Mexico`). - That makes the campaign/setup-byte side of the bridge the strongest remaining setup-core - differentiator, with the year-word side a secondary companion rather than a full peer-group - collapse. The same note now also carries the direct bridge-peer check: - `compare-recipe-book-lines Louisiana.gmp Mexico.gmp` shows that the only `+0x14` peer still - stays zero across the checked recipe-book surface while `Louisiana.gmp` keeps the sparse - nonzero `book00` profile. So the next pass should keep the setup bridge narrowed to the - unique `+0x20` campaign/setup-byte side while still treating the sparse recipe/runtime family - as the stronger remaining differentiator. The same note now also sharpens the `+0x20` bridge - itself: current grounded downstream consumers of mirrored `[world+0x66de]` are mostly the - editor metadata checkbox and campaign-gated branches inside `0x00442c30`, not the core - Tier 2 helpers directly. So the likely Tier 2 relevance of `+0x20` is the branch gate inside - `0x00436d10 / 0x00443a50` before the rebuild family runs, not a later direct - `[world+0x66de] -> 0x00435630/0x00412bd0/0x00412c10` data path. The same note now weakens that - bridge a step further: current grounded profile-side consumers treat `[profile+0xc5]` as a - nonzero gate, and both `0x004425d0` and the campaign-side launch branch force it to `1`. - So the raw `Louisiana.gmp` setup payload byte `+0x20 = 0x3a` is not yet a grounded unique - numeric Tier 2 input; on the current evidence it collapses to the ordinary nonzero campaign or - setup branch before `0x00436d10 / 0x00443a50` run. That shifts even more weight back onto the - sparse recipe/runtime family, with `+0x14` the only currently grounded preserved setup-side - scalar still crossing the bridge. The same note now also adds a same-header-class recipe check: - `compare-recipe-book-lines Louisiana.gmp "Argentina Opens Up.gmp"` shows that even another - `0xcdcdcdcd / 0xcdcdcdcd` peer keeps a broader mixed `book01/book02` profile while - `Louisiana.gmp` stays sparse. So the coarse setup-header class is no longer a plausible - predictor of the Tier 2 runtime shape either; the recipe/runtime family remains the dominant - differentiator. The checked Tier 2 recipe-runtime note now carries the stronger current read: - `0x00435630` only materializes nonzero-mode rows, `Mexico.gmp` is the only `+0x14` peer and - still stays zero across the checked recipe-book surface, and the same-header peer - `Argentina Opens Up.gmp` keeps additional nonzero `book01/book02` content. So the next pass - should bias toward finding other maps that share Louisiana's minimal imported nonzero - recipe-runtime set, rather than reopening broader setup-core or header-class hypotheses. That - scan is now checked in at - `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-recipe-signature-note.md`: - across all 41 bundled `rt3_105` maps there are 23 nonzero-mode path signatures, and - `Louisiana.gmp` sits in a small three-map `book00.line02`-only mode family with - `Britain.gmp` and `South East USA.gmp`. But `Louisiana.gmp` becomes unique again once the - supplied and demanded token lanes are included. That makes the next concrete Tier 2 recipe pass - smaller: compare `Louisiana.gmp` directly against `Britain.gmp` and `South East USA.gmp` on - the token-bearing minimal imported set and then on any downstream runtime-facing differences. - The note now carries that first peer split too: `Britain.gmp` diverges strongly with different - mode and extra supplied rows, while `South East USA.gmp` stays structurally closer but still - differs on the exact `book00.line01` demanded token and `book00.line02` mode/supplied-token - pair. So the current queue head is no longer “find a peer”; it is “find the downstream - runtime-facing consequence of Louisiana’s distinct exact `book00` token-bearing signature.” - That downstream split is now partially grounded too: direct `inspect-smp` checks show both - `Britain.gmp` and `South East USA.gmp` still carry `Port01` and `Warehouse05` names in-file - but neither has any add-building dispatch-strip trigger rows at all. So `Louisiana.gmp` is now - the first checked member of that `book00.line02` mode-shape family whose exact token-bearing - signature also lines up with the shipped add-building runtime path. - - the next corpus split is tighter now too: - the apparent `0x00010000 / 0x6c430000 / 0x00080000+0x00004080` three-row cluster is no longer - just a local `Louisiana.gmp` curiosity; `Ireland.gmp` and `Eastern China.gmp` both reuse parts - of it, but only inside broader multi-book imported profiles, and neither currently reaches any - add-building dispatch-strip rows. `Louisiana.gmp` is the only bundled map whose *entire* - nonzero imported recipe surface collapses to that exact three-row cluster. So the current - queue head is now narrower again: - test whether the shipped `5200 :: [7:0]` `Add Building Warehouse05` strip is tied to that - minimal imported-cluster shape specifically, rather than to the mere presence of one reused - `0x00080000 / 0x00004080` triplet inside a broader recipe profile. - - the same split is tighter even inside the broader “single imported row” family: - eight bundled maps currently materialize only one nonzero recipe row, but `Louisiana.gmp` is - the only one whose supporting token surface is just two additional demand rows instead of three - or four extra token-bearing rows. So the next checked discriminator is now narrower than - “single imported row” too: - test whether the `5200 :: [7:0]` add-building strip tracks that exact two-demand-plus-one- - imported minimal cluster, not just the existence of one imported row or one reused - `0x00080000 / 0x00004080` triplet. - - the importer-side boundary is tighter now too: - direct `inspect-smp` checks show `Louisiana.gmp`, `Britain.gmp`, and `South East USA.gmp` all - collapse to the same first importer shape of exactly one - `imports_to_runtime_descriptor = true` line on `nonzero-supply-branch`, while the broader - reuse case `Ireland.gmp` imports four such rows. So the next queue head is no longer - “prove the first importer branch differs”; it is: - recover the later runtime-record / named-availability consequence under - `0x00412d70 / 0x00412fb0 / 0x00412c10` that makes the minimal cluster land on - `5200 :: [7:0]` only in `Louisiana.gmp`. - - the direct named-availability table is still not that consequence: - the nearest single-import peer `South East USA.gmp` keeps `Warehouse05 = 1` and `Port01 = 1` - just like `Louisiana.gmp`, matching the earlier `Dutchlantis.gmp` read. So the next later - Tier 2 question is now even narrower: - recover the runtime-record bank/template consequences under `0x00412d70` - (plus dependent `0x00411ce0 / 0x00411ee0`) before `0x00412c10` mirrors anything into - `[candidate+0x7ac]`. - - the `0x00412d70` runtime-record roots are grounded now too: - direct `objdump` over `RT3.exe` shows `0x005c93d8 = "Warehouse%02d"` and - `0x005c93e8 = "Port%02d"`, with the rebuild choosing the `Port%02d` root only when - `[candidate+0xba] != 0` and the `Warehouse%02d` root otherwise. That turns the next queue head - into a concrete port-versus-warehouse runtime-record question: - recover how the bank/template pass and live availability bytes `[candidate+0xba/+0xbb]` - make `Louisiana.gmp` land on the `Warehouse%02d` side that later lines up with - `5200 :: [7:0]`. - - the writer-side split is narrower now too: - direct disassembly of `0x004120b0` shows that the per-record stream-load helper clears - `[candidate+0x79c/+0x7a0/+0x78c/+0x790/+0x794/+0x7b0]`, restores the fixed fields through - `[candidate+0x33]`, restores `[candidate+0xb9/+0xba/+0xbb]`, and streams the packed `0xbc` - descriptor array into `[candidate+0x37]`. Direct disassembly of upstream source-record import - `0x00414490` also restores `[record+0xb8/+0xb9/+0xba/+0xbb]`. The new checked-in building - source inspector now shows the stock `Data/BuildingTypes/*.bca` corpus keeps those four bytes - zero across every observed file, including `Warehouse.bca` and `Port.bca`. So the next - concrete recovery target is no longer the lower tagged-record reader itself: - recover which later owner or alternate content path makes the live - `[candidate+0xba/+0xbb]` bank/template state diverge from that all-zero shipped BCA corpus - before `0x00411ee0 / 0x00411ce0 / 0x00412c10` run. - - the broader consumer strip above that split is narrower now too: - direct disassembly now rules three more neighbors onto the read-only side of the bank bytes. - `0x00419230` only scans already-linked owner candidates and runs two rebank-or-clone passes - keyed by `candidate[+0xba]` and `candidate[+0xbb]` before stamping `Port%02d` / - `Warehouse%02d` labels into the auxiliary pool. `0x00418610` only feeds `candidate[+0xba]` - plus subtype/class predicates into the projected-rectangle sample-band helper. And the broader - projected-offset lane at `0x0041a5fd..0x0041a944` resolves the same owner candidate and gates a - world-cell occupancy sweep on `candidate[+0xba]`, but still does not reconstruct the byte. - The `.smp` restore-side auxiliary branch is negative in the same way: `0x00413f80` only - restores queued temporary aux-record images without the fixed selector-byte body - `[+0xb8..+0xbb]`, and `0x0041a950` only releases the live aux collection before re-entering - the same `0x004196c0 -> 0x00419230` import-plus-follow-on strip when the restore flags demand - it. - The outer world-entry load branch is fixed in the same way: `0x00438c70` allocates the live - candidate pool through `0x004131f0` and the auxiliary/source pool through `0x0041aa50`, and - both constructors tail directly into the same fixed tagged-import families rather than taking a - title-specific source-selection branch in between. - The startup-side availability preseed is negative in the same way now too: direct disassembly - of `0x00437737` and its sibling callsite `0x00436ad7` shows both branches only *reading* - live candidate subtype `[candidate+0x32]` plus bank bytes `[candidate+0xba/+0xbb]` and then - feeding those predicates into the scenario-side availability table at `[state+0x66b2]` through - `0x00434f20`; the later bring-up caller `0x00444acc` simply re-enters that same - `0x00437737 -> 0x00434f20 -> 0x00412c10` strip. So even the visible startup preseed still - consumes already-materialized bank/template bytes rather than explaining how they became - nonzero in the first place. - The constructor/import seam is fixed in the same negative way now too: `0x00438c70` allocates - the live candidate pool through `0x004131f0`, calls `0x004411d90` (currently a no-op stub), - and only then allocates the aux/source pool through `0x0041aa50`, so there is no hidden branch - between those two constructors. The only checked caller of source-record importer `0x00414490` - is `0x00419788`, and the surrounding `.rdata` proves that strip is the stock - `"%1*.bca"` / `".\\Data\\BuildingTypes\\"` scan rather than a map-specific alternate - package path. So the remaining Tier-2 mystery is not “which hidden caller invokes the BCA - parser?”; it is “which later non-stock writer or projection seam makes live - `[candidate+0xba/+0xbb]` diverge after the fixed stock BCA import has already run?” - The stock asset-side negative is no longer completely uniform either. The checked-in - `inspect-building-type-sources` report now shows the shipped `Data/BuildingTypes` corpus is - zero at `0xb8..0xbb` for every current `.bca` except `MachineShop.bca`, which carries the lone - selector-window exception `(0x00, 0x80, 0x3f, 0x00)` on a `788`-byte row. The separate - `backup/Bldg/*.bca` corpus stays fully zero in that same window. So the queue head is narrower - again: explain whether the Machine Shop exception is part of the seeded Tier-2 family at all, - or recover the later projection seam that still has to account for the broader live divergence. - Direct map-payload inspection now sharpens that split further. The current Tier-2 maps - `Louisiana.gmp`, `Dutchlantis.gmp`, and `South East USA.gmp` all already carry the literal - `Port00` / `Warehouse00` / `Port01` / `Warehouse01` names inside the shared setup payload at - stable offsets `0x6f77`, `0x7087`, `0x70cb`, and `0x7241` respectively, while the same map - corpus shows no `MachineShop` string at all. That moves the queue head again: the live - `PortNN` / `WarehouseNN` family is now more plausibly a setup-payload candidate-table / source - row projection problem than a hidden stock-BCA stem problem. - The fixed candidate-table rows behind that hint are now checked directly too: - `runtime inspect-candidate-table` shows the same contiguous block in all three maps with - `Port00` at row `35` / file offset `28535`, `Warehouse00` at row `43` / offset `28807`, - `Port01` at row `45` / offset `28875`, and `Warehouse01` at row `56` / offset `29249`, with - the surrounding `Port02..11` / `Warehouse02..11` rows staying in the same contiguous cluster - and each carrying availability trailer `0x00000001`. So the remaining Tier-2 source question - is no longer whether those names exist as stable scenario rows; it is how that stable - candidate-table cluster is projected into the later aux-record bank and then into the live - clone families. - The new root scan sharpens that boundary further. `runtime scan-candidate-table-headers - rt3_wineprefix/drive_c/rt3/maps` shows `37` probe-bearing shipped maps and `4` skips, while - the narrower `runtime scan-candidate-table-named-runs` command confirms that - the shipped probe-bearing maps split into two stable `00`-row families rather than one: - `30` maps keep `Port00` / `Warehouse00` at rows `35` / `43`, while `7` maps move just those - two `00` rows earlier to `10` / `18`; the `Port01..11` and `Warehouse01..11` runs stay fixed - at `45..55` and `56..66` in all `37` probe-bearing maps. Trailer families split separately - too: `28` maps keep the numbered rows on trailer `0x00000001`, while `9` maps keep the same - row layout but zero those trailers. Raw map-string presence is broader than that actual - candidate-table seam too: - `Port00` appears in all `41` shipped `.gmp` files, but `Central Pacific.gmp`, `Italy.gmp`, - `Tex-Mex.gmp`, and `Texas Tea.gmp` do not expose the fixed candidate-table header at all. So - the next Tier-2 source pass should target the `37` probe-bearing maps rather than the noisier - full string-bearing map corpus. - The earlier `+0x173` writer census now trims several dead ends from that pass. Direct - inspection shows `0x00416ded` is only a collection-local row-id seed via `0x00518380` before - `0x00518900`; `0x004274d8` and `0x0042872d` are object-local rollover/allocator copies; and - `0x005b7a6f` is just a helper-side child allocation. The only candidate-family `+0x173` store - in this strip that still matters to Tier-2 is `0x00419559`, and it is simply the later clone - pass stamping the chosen seed-row id onto the new row. So the queue no longer needs another - generic `+0x173` writer sweep before returning to the actual bank-byte owner. - The `Port00` / `Warehouse00` name mystery is narrower than that bank-byte owner now too. - Direct recovery shows `0x00419680` constructing `gpdBuildingTypeDB` at `0x0062b2fc` by - storing vtable `0x005c9718`, and the surrounding `.rdata` descriptor at `0x005c9718` is the - `%1*.bty` table `{ 0x00416950, 0x00416ce0, 0x004168e0, "%1*.bty", ... }`. That same vtable is - used by the ordinary DB load path at `0x0044431c`, which calls slot `+0x04 = 0x00416ce0`, - while `0x00416950` iterates live rows and calls slot `+0x08 = 0x004168e0` as the per-entry - cleanup path. Inside `0x00416ce0`, the stock building-type loader rewrites only the bare - source names `port` (`0x005c8f94`) and `warehouse` (`0x005c8e4c`) to `Port00` - (`0x005c96a0`) and `Warehouse00` (`0x005c9694`) before scanning the live candidate collection - `0x0062b268`; when it finds the matching candidate row, it seeds `[row+0x173]` from the live - candidate row id at `0x00416ded` and then re-enters `0x00518900`. So the queue no longer - needs a generic hidden late writer for the `00` names themselves: that remap is already owned - by the stock `.bty` load callback. The remaining Tier-2 source question is now the row-family - choice and later bank selection above that remap, especially the shifted `00` families - (`35/43` vs `10/18`) and the still-fixed `01..11` run families (`45..55` / `56..66`). - The checked-in stock source report agrees with that split too: - `artifacts/exports/rt3-1.06/building-type-sources.json` still shows only bare source stems - `port` and `warehouse`, while `port00..11` and `warehouse00..11` stay in - `named_binding_comparison.binding_only_canonical_stems`. So the queue no longer needs more - stock `BuildingTypes/*.bty` discovery for the numbered families either; the remaining `01..11` - source work belongs under scenario/live candidate-table projection and row-family choice above - the already-grounded bare-name remap. - The shipped asset directory makes that remap boundary explicit too: `Data/BuildingTypes` - contains bare `Port.bty/.bca` and `Warehouse.bty/.bca`, but no stock `Port00` or - `Warehouse00` assets on disk. So the checked-in queue no longer needs to speculate about a - hidden on-disk `00` family; the loader-side `port -> Port00` and `warehouse -> Warehouse00` - rewrite is the actual stock bridge. - The stock source-family strip above that remap is grounded now too. `0x004196c0` walks the - `%1*.bty` file set, routes each match through `0x00414490`, calls the stock load callback - `0x00416ce0`, and only then tails into `0x00419230`. The recovered source-name table is - `StationSml`, `StationMed`, `StationLrg`, `ServiceTower`, `Maintenance`, `ClpBrd`, `Kyoto`, - `Persian`, `SoWest`, `Tudor`, and `Victorian`, so the next Tier-2 source question is no longer - “where do numbered names come from?” but “which style-family rows first survive into the later - two-bank, twelve-ordinal clone pass?” - The stock asset corpus now agrees with that table directly too. The shipped - `Data/BuildingTypes` directory contains `VictorianStationSml/Med/Lrg.bty`, - `TudorStationSml/Med/Lrg.bty`, `SoWestStationSml/Med/Lrg.bty`, - `PersianStationSml/Med/Lrg.bty`, `KyotoStationSml/Med/Lrg.bty`, - `ClpBrdStationSml/Med/Lrg.bty`, plus standalone `Maintenance.bty` and - `ServiceTower.bty`, alongside the style-specific house families. So the - `0x005f3c6c/0x005f3c80` tables are no longer just a recovered naming hypothesis; they line up - with real stock filename families on disk. - The exact-match resolver beneath that style-family strip is grounded now too. `0x00419590` - copies one source-kind name from `0x005f3c6c`, combines it with one style/theme entry from the - smaller subset table `0x005f3c80` through format `"%1%2"` at `0x005c8730`, and then scans the - live auxiliary pool `0x0062b2fc` for the first exact `[entry+0x04]` match. So the - `0x005f3c6c/0x005f3c80` pair is no longer an anonymous bank byte table; it is the stock - style-family-to-combined-name resolver that sits immediately below the remaining Tier-2 source - selection frontier. - The recovered `.bty` header probes tighten that source split further too. The checked-in - `inspect-building-type-sources` report now shows `Port.bty` and `Warehouse.bty` are ordinary - `type_id = 0x000003ec` rows with direct bare-name headers (`name_0x22` / `name_0x7c` = - `Port` or `Warehouse`) and shared `dword_0xbb = 0x000001f4`, while the style-station rows such - as `VictorianStationSml/Med/Lrg.bty` stay on the same `0x000003ec` family but keep - `name_0x7c = VictorianStations` and `dword_0xbb = 0x00000000`. The standalone - `Maintenance.bty` / `ServiceTower.bty` rows also stay in the same stock family, but expose - display names `Maintenance Facility` and `Service Tower` with zero `dword_0xbb`. The stock - nonzero family is explicit now too: only one recovered `.bty` header lane is nonzero, - `dword_0xbb = 0x000001f4`, and it spans exactly `22` files: - `Brewery`, `ConcretePlant`, `ConstructionFirm`, `DairyProcessor`, `Distillery`, - `ElectronicsPlant`, `Furnace`, `FurnitureFactory`, `Hospital`, `Lumbermill`, `MachineShop`, - `MeatPackingPlant`, `Museum`, `PaperMill`, `PharmaceuticalPlant`, `Port`, `Recycling Plant`, - `Steel Mill`, `Textile Mill`, `Tire Factory`, `Tool and Die`, and `Warehouse`. So the - remaining Tier-2 source question is no longer whether the numbered `Port%02d` / - `Warehouse%02d` banks are hidden station-style aliases; it is why the later clone path prefers - this narrower `0x000001f4` stock family over the zero-valued station and - maintenance/service families when it seeds those numbered banks. - The stock rebuild handoff above that seed question is tighter now too. Direct disassembly of - `0x004196c0` shows the broader stock `*.bca` rebuild loop formatting the wildcard path rooted - at `0x005c93fc`, iterating the file enumerator through the `0x005c8190/0x005c8194/0x005c819c` - find-first/find-next strip, calling the per-file stock loader `0x00414490` for each hit, and - only then tail-calling `0x00419230`. So the remaining Tier-2 source problem is increasingly - “which stock rows that rebuild admits or seeds with nonzero bank bytes” rather than “which - unrelated later scheduler invokes the banked clone pass.” - The direct `+0xba/+0xbb` writer census is narrower now too. The obvious newly surfaced stores - at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side portrait/string - refresh helpers: they walk a separate id-keyed collection through `0x0053f830`, free and - replace heap strings at dword `[entry+0xba]`, and mirror shell text from `[0x006cec74+0x1ef]`. - The other new dword writers at `0x00540251/0x0054034d`, `0x0055fd40`, `0x0055bdc4/0x0055bf01`, - `0x0055ca78`, `0x0055f290`, `0x005b5168`, and `0x005b6718` likewise belong to wider - non-candidate heap objects with their own vtables and field layouts, not to the live - `0x005c93cc` candidate rows. - The actual candidate import strip now has a tighter positive bound instead: `0x004120b0` - explicitly declares `[candidate+0xba]` and `[candidate+0xbb]` as one-byte parser fields through - `0x00531150`, while `0x00412d70` can later clone a whole already-materialized candidate row - through `rep movsl`, including those byte fields, before `0x00412f02` chooses the - `Port%02d`/`Warehouse%02d` naming branch from the cloned `[candidate+0xba]` bit. So the live - divergence frontier is narrower again: not generic direct stores into candidate rows, but the - earlier seed or projection seam that first makes some source/live rows reach that clone path - with nonzero bank bytes. - That owner chain is explicit now too: the stock candidate collection root `0x0062b268` is - constructed at `0x00438c8e -> 0x004131f0 -> 0x00412fb0 -> 0x004120b0`, and the adjacent - `.rdata` strings at `0x005c93f4..0x005c940e` prove that `0x00412fb0` is the - `Data\\BuildingTypes\\%1*.bca` / `%1*.bty` loader, not a map-side overlay reader. The - immediate follow-on call at `0x00438ca0 -> 0x00411d90` is a no-op, so there is no hidden - world-load mutation directly after the stock constructor. That narrows the next Tier-2 source - question again: either a later candidate-class serializer/import/export family such as - `0x00414500..0x00414b14` is still replaying non-stock bank bytes into live rows, or the - qualifying source rows already live in the stock `BuildingTypes` corpus and the remaining work - is to explain which rows the later clone path chooses. - The clone-group lane is tighter now too. Candidate dword `[candidate+0x794]` is not a broader - owner-state field written elsewhere; the current direct write set shows it is initialized to - zero in `0x004120b0` and then assigned only in `0x00412d70` as the chosen seed-row index that - later clones should follow. The known consumers now line up with that meaning: - `0x0041eba8` compares region-side row key `[row+0x2f2]` against `[candidate+0x794]`, - `0x004cccdc` excludes candidates whose `[candidate+0x794]` already matches one active row key, - and `0x0050a1cd` keeps the same seeded rows out of one later availability branch. So the open - question is not “who writes the mysterious group id?” but “which source/live rows become the - first seeded rows that let `0x00412d70` propagate nonzero bank bytes within that family?” - The later rebank-or-clone owner is narrower now too. Direct disassembly of `0x00419230` shows - one outer pass over bank selector `0` then `1`, with an inner `12`-ordinal sweep each time. - For each `(bank, ordinal)` pair it scans already-linked aux/source rows by owner candidate id - `[entry+0x173]`, requires nonzero bank byte `[candidate+0xba]` or `[candidate+0xbb]` - accordingly, prefers a previously materialized target whose ordinal field `[entry+0x187]` - already matches, otherwise clones one qualifying source row, copies the optional heap planes by - size `([entry+0xb8] * [entry+0xb9] << 5)`, stamps `[entry+0x187] = ordinal+1`, writes the - visible stem at `[entry+0x22]` from `Warehouse%02d` or `Port%02d`, mirrors that stem into the - exact-match key at `[entry+0x04]`, and finally rebinds `[entry+0x173]` by exact stem match - back into the live candidate pool. That moves the queue head one step earlier again: identify - which qualifying source rows first satisfy each `(bank, ordinal)` pair before `0x00419230` - ever clones or renames them. - So the honest next queue head is now one step earlier again: - recover the setup-payload or restore-time projection owner that carries those static - `PortNN` / `WarehouseNN` rows into the live candidate clone families with nonzero - `[candidate+0xba/+0xbb]`, beyond the separate `MachineShop.bca` selector-window exception in - the shipped `BuildingTypes/*.bca` corpus. - kinds”; it is the smaller set of scenario-specific records where that sweep explicitly writes - `[event+0x7ef]` itself or a still-later owner does. - - two explicit trigger-kind materializations are now grounded inside that retagger: - the `SP - GOLD` branch at `0x00443526` rewrites `[event+0x7ef]` from `1 -> 5` on live - runtime-event id `1` when `[world+0x66de]` is set and the checked root payload is kind `7` - with subtype byte `5`, while the `Labor` branch at `0x00443601` rewrites `[event+0x7ef]` - from `0 -> 2` on live runtime-event id `0x0d` when the same scenario flag is set and the - checked `0x3c -> 0x3d` child payload pair carries the matching negative scalar sentinel. - - cross-map probing now gives a better static-analysis lead too: - `British Isles.gmp` shows no current `0x00431b20` dispatch-strip rows, `Germany.gmp` stays on - `Game Variable 1` plus `Company Variable 3..4`, while `Texas Tea.gmp` adds `Economic Status` - and one still-unlabeled grouped descriptor id `548`; that makes descriptor `548` plus the - `opcode 8` branch on record `7` the next concrete non-hook analysis target above the compact - runtime-event loader - - the checked-in function map now sharpens that `Texas Tea.gmp` branch too: - opcode `0x08` in the grounded `0x00431b20` dispatch strip lands on - `0x00426d60 company_deactivate_and_clear_chairman_share_links`, so the open question is - whether grouped descriptor id `548` is the missing compact-event label for that destructive - company-clear path or a neighboring unmapped id-space entry in the same branch family - - the broader installed-map sweep narrows that question further: - across all `41` bundled `.gmp` files in the current `rt3_105/maps` install, grouped descriptor - id `548` currently appears only once, in `Texas Tea.gmp` record `7`, with `opcode 8`, - `scalar 0`, standalone condition tuple `(7, subtype 0)`, and compact signature family - `nondirect-ge1e-h0001-0007-0000-6d00-0200-p0000-0000-0000-ffff` - - the same wider sweep also rules out the simplest alias theory: - the ordinary checked-in `Deactivate Company` descriptor `13` does appear in real map bundles, - but only on `opcode 1` with scalar `1` in `British Isles.gmp`, `Chicago to New York.gmp`, - `East Coast, USA.gmp`, `Japan Trembles.gmp`, and `State of Germany.gmp`; it does not appear on - the `opcode 8` deactivation branch, so grouped descriptor id `548` is not just the obvious - compact stand-in for ordinary descriptor `13` - - that compact opcode-`8` cluster is now grounded as an artifact-boundary problem rather than a - mysterious compact-only id family: - direct binary inspection of the `0x00610398` EventEffects table shows the contiguous table does - not stop at row `519`; it continues cleanly through row `613`, with the extractor-side - sequential descriptor invariant still holding. The checked-in extractor and semantic catalog now - cover the full `614`-row export instead of the old truncated `520`-row slice - - that closes the earlier unlabeled cluster: - grouped descriptor ids `521`, `526`, `528`, `548`, and `563` are now recovered as - `Add Building FarmGrain`, `Add Building Furniture Factory`, `Add Building Logging Camp`, - `Add Building Port01`, and `Add Building Warehouse05` respectively. The checked-in - `event-effects-building-bindings.json` now carries the full descriptor-side candidate bridge - across all `111` add-building rows (`503..613`) through the direct `descriptor_id - 503` - mapping in `0x00430270`, while concrete candidate names remain grounded only for the stable - RT3 1.05 live-catalog run `0..66` exposed by `0x0041ede0` and the checked candidate-table - corpus - - the earlier `label_id - 2000` bridge for `548` and `563` is now known to be a false lead: - those numeric collisions hit the special-condition label table - (`Disable Building Stations`, `Completely Disable Money-Related Things`), but the extended - EventEffects table proves the actual grouped descriptors are add-building slots, not - special-condition verbs - - the compact opcode-`8` frontier therefore shifts: - direct disassembly of `0x00430270` now shows that the add-building consumer does not branch on - grouped opcode at all for descriptor strip `503..613`; it consumes the descriptor-derived - candidate id, placement count byte `0x11`, center words `0x12/0x14`, and radius word `0x16`, - clamps that radius to at least `1`, and retries randomized placements up to `200` times. - The next static-analysis pass should therefore target the remaining span-field meaning and - shell-owned placement-flow ownership on that strip, not more missing-label recovery: the - descriptor-side candidate bridge is now checked in across `503..613`, and the honest remaining - boundary is the missing non-hook name catalog for candidate ids `67..110` - - the new offline `BuildingTypes` source report sharpens that missing name-catalog boundary too: - `runtime inspect-building-type-sources rt3_wineprefix/drive_c/rt3/Data/BuildingTypes artifacts/exports/rt3-1.06/event-effects-building-bindings.json` - now reports `77` `.bca` files, `200` `.bty` files, and `208` canonical asset stems. Against - the checked-in named add-building bindings it finds exactly `43` shared canonical stems, - `24` binding-only stems (`Port00..11` and `Warehouse00..11`), and `165` broader asset-only - stems. The numbered live `Port00..11` and `Warehouse00..11` names therefore collapse to - generic asset stems `Port` and `Warehouse` on disk, so the `BuildingTypes` directory is now - grounded as a wider offline source catalog, but not yet as a direct second live candidate-name - owner for descriptor-side ids `67..110` - - the local-runtime builder strip now reinforces that same boundary: - direct disassembly of `0x00418be0` shows the broader placed-structure rebuild lane resolving - its caller-supplied stem only through `0x00416e20 indexed_collection_resolve_live_entry_id_by_stem_string` - against the current live candidate collection, then projecting runtime scratch through - `0x00416ec0` and `0x00418610`. So the broader `BuildingTypes` asset pool is not yet a proven - alternate live owner for descriptor-side add-building candidate ids; current non-hook evidence - still routes stem resolution back through the live candidate collection that tops out at the - contiguous named run `0..66` - - the shipped-map compact-dispatch corpus is narrower than the descriptor strip too: - `runtime inspect-compact-event-dispatch-cluster-counts rt3_wineprefix/drive_c/rt3_105/maps` - scans all `41` bundled maps and finds add-building dispatch occurrences only for descriptors - `506`, `507`, `521`, `526`, `528`, `548`, and `563`. No bundled-map dispatch rows currently - exercise descriptor ids `570..613`, so the back half of the widened add-building strip is now - bounded as descriptor-grounded but latent in the shipped compact-event corpus - - the concrete owner strip above that bundle is grounded now too: - `0x00433060` is the direct non-direct serializer loop that writes `0x4e99/0x4e9a/0x4e9b`, - calls `0x00430d70` per live collection row, and sits beside the sibling `0x00433130` size/load - family rather than behind an unknown blob writer - - those non-direct rows still carry stable structural family ids in the inspection notes: - the row probe emits `compact signature family = nondirect-...` keys alongside decoded grouped - descriptors, so repeated compact families can still be compared across maps without scraping - raw bytes - - that moves the startup compact-effect blocker again: - the remaining question is no longer compact row framing, but which serialized/live rows in this - now-decoded non-direct bundle correlate to loaded trigger-kind `8` rows and which of those can - reach the placed-structure mutation opcodes under `0x00431b20` - - the `[site+0x27a]` companion lane is grounded now too: - it is a live signed scalar accumulator rather than a second owner-identity seam, with zero-init - at `0x0042125d` and `0x0040f793`, accumulation at `0x0040dfec` and `0x00426ad8`, direct set on - acquisition commit at `0x004269e4`, and negated clear at `0x00426a44..0x00426a90` - - the remaining owner-company question is therefore narrower than “find any replay seam”: - identify which non-transport persisted source family outside the currently bounded direct - allocator/finalize/store families, the save-side `0x00413440` serializer, the load-side - `0x00413280` cached-source bridge, the checked ordinary replay strip - `0x00444690 -> 0x004133b0 -> 0x0040ee10`, and the already-bounded loaded runtime-effect lane - `0x00444d92 -> 0x00432f40(kind 8) -> 0x004323a0 -> 0x00431b20` feed the tuple or companion - restore calls that are sufficient to repopulate `[site+0x276]` for shellless acquisition; the - remaining runtime-effect subquestion is now which loaded kind-`8` rows can carry the - placed-structure mutation opcodes rather than whether kind `8` is synthetic - - the ordinary restore-vs-finalize split is tighter now too: - direct disassembly shows `0x00413280` stream-loading `0x36b1/0x36b2/0x36b3` through per-entry - vtable slot `+0x40`, and `0x00444690` simply enters `0x004133b0` before continuing into later - grid/world refresh owners; neither ordinary bring-up owner re-enters - `0x004134d0 / 0x0040f6d0 / 0x0040ef10` for already-restored rows. The positive - `[site+0x276]` store at `0x0040f5d4` therefore remains grounded only on explicit tuple/create - paths, so the next non-hook pass should look for a non-transport persisted tuple family or a - later ordinary restore owner rather than another generic replay scan. - - the loaded runtime-effect lane is narrower now too: - real map inspections already show decoded `0x00431b20` grouped-descriptor content inside the - ordinary non-direct bundle without any recovered trigger-kind metadata in the current surface: - `Texas Tea.gmp` reports `Add Building Port01`, `Alternate USA.gmp` reports - `Add Building FarmGrain` and `Add Building Logging Camp`, and `War Effort.gmp` reports only - variable bands, while all three currently summarize `trigger_kinds_present = []` and - `records_with_trigger_kind = 0`. That means the next non-hook pass on this branch is no longer - “do ordinary loaded rows reach `0x00431b20`?”; it is “where is trigger-kind represented or - bypassed for those already-decoded ordinary compact rows?” - - the adjacent control-lane inheritance path is narrower now too: - direct disassembly shows `0x0042db20` loading only the linked `0x1e`/`0x28` row bodies from - `0x4e9a`, while the separate deep-copy helper `0x0042e050` copies text bands plus the full - `[event+0x7ee..0x7fa]` block. Current caller census shows that deep-copy seam reached from the - event-editor duplication path at `0x004dba23`, not from the ordinary `0x00433130` restore - strip. So the next non-hook question is not “maybe load inherits trigger kind through - 0x0042e050”; it is which other ordinary owner, if any, seeds `[event+0x7ef]` for these - already-decoded non-direct rows. - - the first non-editor positive control-lane writer is bounded away from restore too: - direct disassembly now shows `0x00430b50` allocating a fresh live runtime-effect row through - `0x00432ea0 -> 0x0042d5a0`, zero-initializing the full `[event+0x7ee..0x7fa]` block, then - seeding `[event+0x7ef]` to `2` or `3` plus adjacent control bytes. That builder is reached - from the `0x004323a0` follow-on service strip at `0x0043232e`, not from `0x00433130` restore. - So the remaining ordinary-owner question is narrower again: neither deep-copy inheritance nor - the first positive control-lane builder currently belongs to the non-direct `0x4e9a` load path. - - the remaining non-editor control-lane mutators are bounded away from restore too: - direct disassembly of `0x00443200..0x004436e3` shows a name-driven live retagger sweep over - the already-materialized event collection `0x0062be18`, matching text bands through - `0x005a57cf` against scenario strings such as `New Beginnings`, `Chicago to New York`, - `The American`, and `Labor`, then mutating `[event+0x7ef/+0x7f9/+0x7fa]` on those live - records. That is now a grounded post-load live-maintenance seam, not the missing ordinary - `0x4e9a` control-lane owner. - - the positive-path caller census is effectively boxed in now too: - direct `0x0040ef10` callers are the create-side pair `0x00403ef3 / 0x00404489`, the transport - tuple pair `0x0046f073 / 0x004707ff`, and the already-ruled-down live controller - `0x005098eb`; direct `0x004134d0` callers are the create-side pair `0x00403ed5 / 0x0040446b`, - the transport-side pair `0x0046efbf / 0x0047074b`, the counted transport builders - `0x00472bef / 0x00472d03`, plus live/controller callers `0x00422bb4` and `0x00508fd1`. - That means the still-missing owner seam is no longer “find another obvious constructor/finalize - caller”; it is specifically a non-transport persisted tuple family or a later ordinary restore - owner outside this boxed caller set. - - the second is narrower in the same way: - the checked-in `0x36b1/0x36b2/0x36b3` triplet seam and the - `0x4a9d/0x4a3a/0x4a3b` side-buffer seam still do not serialize `[site+0x310/+0x338/+0x360]` - directly, so the known save seams are ruled down even though a later restore family is still open - - the dynamic side-buffer load seam is ruled down too: - `0x00481430 -> 0x0047d8e0` repopulates the route-entry list, three byte arrays, five - proximity buckets, and the trailing scratch band from stream, but still does not claim the - cached tri-lane - - the tri-lane now has one real runtime accumulator too: - direct local binary inspection shows `0x0040c9a0` folding `[site+0x310/+0x338/+0x360]` into - `[site+0x2b4/+0x2b8/+0x2bc]`, mirroring the nine-dword side array rooted at `[site+0x2e4]`, - and then clearing the tri-lane - - caller census keeps that tri-lane role narrow: - `0x0040c9a0` only appears under the broad live-collection sweep - `0x0040a3a1..0x0040a4d3`, while `0x0040cac0` stays under weighted scoring or evaluation - families such as `0x0040fcc0..0x0040fe28` and `0x00422c62..0x00422d3c` - - direct local binary inspection now rules out the old “no live writer” hypothesis too: - `0x0040d4aa/0x0040d4b0` add into `[site+0x310]`, - `0x0041114a7/0x004111572` add into `[site+0x310]`, - `0x0041114b7/0x004111582` add into `[site+0x338]`, and - `0x0041118aa/0x0041118f4` add into `[site+0x360]` - - the writer family is now bounded one level higher: - `0x0040d450` is a small owner-company-aware producer over `[site+0x276]`, - `0x00455810/0x00455800/0x0044ad60`, and `0x00436590` ids `0x66/0x68`, while - `0x00410b30..0x004118f4` is a broader candidate-processing loop walking `0xbc`-stride rows, - gating them through `0x00412560`, and then accumulating both stack temporaries and direct - writes into `[site+0x310/+0x338/+0x360]` - - `0x00412560` is now bounded as the shared candidate/admissibility gate above that loop: - it checks candidate-row fields `+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44`, world date/flags via - `0x006cec78`, and the candidate table `0x0062ba8c` - - the cached source/candidate bridge is now grounded on stream load too: - direct local binary inspection shows `0x00413280` dispatching per-entry vtable slot `+0x40` - on the `0x005c8c50` specialization table, that slot resolving to `0x0040ce60`, and - `0x0040ce60` immediately re-entering `0x0040cd70` plus `0x0045c150` - - the third hypothesis is now a cached source/candidate bridge question, not just a raw - `[site+0x04]` selector question: - `0x0040cd70` seeds `[site+0x3cc/+0x3d0]` from `0x62b2fc / 0x62b268`, - `0x0040cee0` resolves cached candidate id `[site+0x3d0]` back into the live candidate pool, - and `0x004138f0` already counts live placed structures by that cached candidate id - - the checked-in consumer side is tighter too: - `0x0040dc40` already consumes live owner company `[site+0x276]`, company stat-family - `0x2329/0x0d`, candidate field `[candidate+0x22]`, and the projected-cell validation strip - `0x00417840 -> 0x004197e0`, then commits the linked-site mutation through - `0x0040d1f0 / 0x00480710 / 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70` - - the periodic-company trace now carries the tri-lane live-service family as structured fields, - not just prose: - - site cached tri-lane status = - `live_writer_family_grounded_semantics_and_persisted_inputs_missing` - - tri-lane live service status = - `candidate_gate_and_live_writer_family_grounded_exact_formula_and_persisted_inputs_missing` - - tri-lane live owners include: - `0x0040d450`, `0x00410b30..0x004118f4`, `0x00412560`, `0x0040c9a0`, - and the downstream `0x0040fcc0..0x0040fe28 / 0x00422c62..0x00422d3c` consumers - - tri-lane gate fields include: - candidate-row fields `+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44`, - world date/flags via `0x006cec78`, - candidate table `0x0062ba8c`, - and caller-provided subject vtable slot `+0x80` plus owner-present flag `[site+0x246]` - - tri-lane writer roles are now split explicitly between: - one owner-company-aware local scorer `0x0040d450`, - the broader `0x00410b30..0x004118f4` candidate loop, - and the later `0x0040c9a0` accumulator/reset - - direct caller families are now split explicitly too: - `0x0040fb70` is the small wrapper into `0x00412560`, - `0x004b4052 / 0x004b46ec` are collection-wide `0x0040fb70` census callers over - `0x0062b26c`, - `0x00401633` is an acquisition-adjacent `0x0040d540` caller that immediately feeds company - stat-family `0x2329/0x0d`, - `0x0044b81a` is an owner-company-aware `0x0040d540` caller that also reaches - `0x0040cb70` and news/event id `0x65`, - and `0x004b70f5 / 0x004b7979` are broader sibling `0x0040d540` callers routing through - `0x004337a0` and downstream `0x00540120 / 0x00518140` - - formula input lanes are now structured too: - `0x00412560` uses candidate-row time window `+0x20/+0x22`, - owner/absence booleans `+0x24/+0x28`, - list count `+0x2c`, - and membership list `+0x44`, - while the wider `0x00410b30..0x004118f4` loop consumes candidate-row - `+0x18/+0x1c/+0x2a/+0x2c/+0x44`, - subject latch `[site+0x78c]`, - personality byte `[site+0x391]`, - world lanes `[world+0x0d]`, `[world+0x4afb]`, `[world+0x4caa]`, - owner-company scalar `[company+0x0d5d]`, - and the local cache bands `[site+0x2e8]`, `[site+0x310]`, `[site+0x338]`, `[site+0x360]` - - the direct writer census now narrows the remaining owner-company question too: - grounded `[site+0x276]` writes cluster under create-side and live mutation families such as - `0x004269b0 / 0x00426a10`, the create-side `0x0040ef10 / 0x0040f6d0` strip, and the bulk - reassignment families `0x00426dce..0x00426ea1` and `0x00430040..0x004300d6`, not under the - known `0x00444690 -> 0x004133b0 -> 0x0040ee10` replay strip - - the direct writer census is split more cleanly now too: - `0x00421200` is the broad late-field constructor/reset zero-fill over the same - `0x23a/0x23e/0x25a/0x25e/...` row family and clears `[+0x276]` as part of that initialization, - `0x00428270` is a collection-wide live owner remap over `0x0062b26c` that rewrites - `[site+0x276]` only for rows matching one old owner id, and `0x00422280` is a subtype-local - synthetic scalar writer that stores one `100000 * rand(bucket)` result into `[+0x276]` before - publishing localized-id `7`, so none of those three writes is the missing replay owner seam - - the same write side is now grounded one level higher too: - direct local control-flow reconstruction shows those families hanging under the grouped opcode - dispatcher `0x00431b20` over `0x0061039c`, with opcodes `0x04..0x07` dispatching to - `0x00430040`, opcodes `0x08/0x10..0x13` dispatching to `0x00426d60`, and opcodes - `0x0d/0x16` dispatching to `0x0042fc90` - - `0x0042fc90` itself is now ruled onto the live mutation side too: - it iterates the live placed-structure collection `0x0062b26c`, filters rows through - `0x0040c990`, optional owner-company match `[site+0x276]`, and row vtable slot `+0x70`, - which keeps that branch on the live application side rather than replay - - the focused save-side triplet probe is now available directly too: - `runtime inspect-save-placed-structure-triplets ` dumps the grounded - `0x36b1/0x55f1/0x55f2/0x55f3` rows without the wider periodic trace wrapper - - real-save output from that focused probe rules the checked-in triplet seam down further: - on grounded `p.gms`, all 2026 rows keep policy trailing word `0x0101`, the profile side is - dominated by companion byte `0x00` with status `unset` (`1885` rows) plus farm-growth buckets - (`138` rows), and the only nonzero companion-byte residue is `3` `TextileMill` rows - - the create-side family is grounded separately too: - city-connection direct placement already reaches - `0x00402cb0 -> 0x00403ed5/0x0040446b -> 0x004134d0 -> 0x0040ef10` - as the shared constructor/finalize strip for newly created site rows - - so the acquisition blocker is no longer “is there any real consumer family above these - lanes?”, “how do new placed structures finalize?”, “is `[site+0x2a4]` still missing?”, or - “does the tri-lane even have live writers?”; it is now specifically the persisted inputs and - exact shellless service semantics above the grounded live tri-lane scorer family, plus the - save-native or replay owner that populates `[site+0x276]` for already-restored rows before - shellless acquisition runs - - direct disassembly now shows the generic base constructor `0x0052edf0` clearing base state - through `0x0052ecd0` and then writing `[this+0x04]` from caller arg `1` - - `0x00455b70` is the concrete placed-structure specialization constructor feeding - `0x0052edf0`, with arg `3` as the primary selector and arg `1` as fallback - - `0x00455c62` is the direct in-body call from that specialization constructor into - `0x0052edf0` - - `0x00456100` is a local wrapper that duplicates its first incoming arg across the - selector/fallback bundle before calling `0x00455b70` - - `0x00456072` is a fixed `0x55f2` callback that forwards three local dwords plus unit scalars - into `0x00455b70` - - `0x0045c36e / 0x0045da65 / 0x0045e0fc` are concrete callers of `0x00456100`, repeatedly - allocating `0x23a` rows, forwarding stack-backed buffers, and using the same default scalar - lanes - - the `0x00456100 -> 0x00455b70` wrapper mapping is now grounded far enough to say the sampled - selector-source lanes are `[owner+0x23e]` at `0x0045c36e`, literal zero at `0x0045da65`, and - `[ebp+0x08]` at `0x0045e0fc` - - direct disassembly now shows `0x0045c150` as a save-backed loader for `[owner+0x23e/+0x242]`: - it zeroes those fields, runs the shared tagged loader `0x00455fc0`, reads tagged payload - `0x5dc1`, and copies the two recovered lanes into `[owner+0x23e/+0x242]` before - `0x0045c310 -> 0x0045c36e` later feeds `[owner+0x23e]` into `0x00456100` - - the local linked-site helper neighborhood now reaches that same owner strip directly: - `0x0040ceab` calls `0x0045c150`, and `0x0040d1a1` jumps straight into `0x0045c310` - - `0x00485819` is one typed placed-structure caller of `0x0052edf0` through the generic - three-arg wrapper `0x00530640` - - `0x00490a79` is one chooser-side caller of `0x00455b70`, feeding literal selector - `0x005cfd74` with fallback seed `0x005c87a8` - - the periodic-company trace now also surfaces the save-side `0x5dc1` payload/status summaries - already parsed from the `0x36b1` triplet seam; on grounded `p.gms` the payload dword lane is - almost entirely unique while the status kind stays `unset`, and the dominant adjacent payload - delta is `0x00000780` across `1908` steps; grounded `q.gms` shows the same dominant adjacent - delta `0x00000780` across `1868` steps - - the same trace now also promotes the one-byte `0x5dc1` post-secondary discriminator explicitly: - grounded - `p.gms` shows dominant companion byte `0x00` on `2023` rows with only `3` `0x01` rows, and - grounded `q.gms` shows dominant companion byte `0x00` on `2043` rows with only `14` `0x01` - rows; the old “pre-footer padding” hypothesis is now better understood as a separate - post-secondary discriminator byte after the repeated secondary payload string, not as the - `[owner+0x242]` field itself - So the next owner question is no longer “what does the acquisition branch do?” or “which post- - load owner replays linked-site refresh?” but “which concrete `0x00455b70` caller family applies - to the live site rows, and which persisted lane becomes the selector bundle that ultimately - seeds `[site+0x04]`?” The current strongest restore-family hypothesis is now the save-backed - `0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70` strip. -- Make the next static/rehost slice the peer-site rebuild seam above persistence, not another save - scan: - - treat `0x00444690 -> 0x004133b0 -> 0x0040e450 / 0x0040ee10 -> 0x0040edf6 -> 0x00480710` as - the checked-in bring-up replay path for existing saves - - treat `0x004160aa -> 0x0040ee10` as the checked-in recurring runtime maintenance entry into the - same linked-peer refresh strip - - treat `0x0052edf0` as the checked-in field owner for `[site+0x04]` - - treat `0x00455b70` as the checked-in specialization constructor that maps selector/fallback - bundle lanes into that field owner - - distinguish which `0x00455b70` caller family actually seeds the live site rows before - `0x00420030 / 0x00420280 / 0x0047efe0 / 0x0047fd50` consume the resulting selector, with the - current first target being the save-backed - `0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100` family - - use the structured periodic-company trace selector fields now checked into - `inspect-periodic-company-service-trace`: owner strip - `0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70`, persisted tag - `0x5dc1`, selector lane `[owner+0x23e]`, class-identity status - `grounded_direct_local_helper_strip`, and helper linkage - `0x0040ceab -> 0x0045c150` / `0x0040d1a1 -> 0x0045c310` / - `0x0040cd70 seeds [site+0x3cc/+0x3d0] from 0x62b2fc / 0x62b268` - - use the new `0x5dc1` payload/status summary in the same trace as negative evidence too: - the current `profile_payload_dword` lane behaves like a save-invariant monotone ladder - (`dominant adjacent delta 0x780` on both `p.gms` and `q.gms`) rather than a compact selector - family, so the next peer-site slice should treat that raw dword as a likely allocator/offset - lane until a stronger selector interpretation appears - - use the new `0x5dc1` post-secondary-byte summary in the same trace as positive evidence: - that byte is overwhelmingly `0x00` with a tiny `0x01` residue on both grounded saves, so the - next peer-site slice should treat it as a real typed discriminator after the restored - `[owner+0x23e]` / `[owner+0x242]` payload strings and ask which later `0x004014b0` / - `0x00406050` predicates actually consume it - - use the new nonzero-companion name-pair summary in the same trace as a narrower acquisition - clue too: grounded `p.gms` exposes only `TextileMill/TextileMill x3`, while grounded `q.gms` - exposes `TextileMill x9`, `Toolndie x2`, and singleton `Brewery`, `MeatPackingPlant`, and - `MunitionsFactory` rows, so the next peer-site slice should treat nonzero post-secondary-byte - rows as a likely industry-like subset rather than a generic placed-structure mode split - - that bridge is explicit now too: the periodic trace can classify those nonzero companion - names against the recovered stock Tier-2 building family, and the current grounded overlap set - is `TextileMill`, `Toolndie`, `Brewery`, and `MeatPackingPlant` while - `MunitionsFactory` remains the clear current residue outside the recovered - `0x000001f4` stock header family - - the checked-in building-source summary now says which stock header lane is actually carrying - the shared alias roots too: within that recovered nonzero `0x000001f4` family, - `name_0x40` / `name_0x7c` mostly stay on direct file/display roots (`Warehouse x7` plus - singletons such as `Brewery`, `Port`, and `Toolndie`), while `name_0x5e` is the real - clustered alias-root lane (`TextileMill x10`, `LumberMill x4`, `MeatPackingPlant x4`, - `Distillery x2`, `Toolndie x2`). So the next Tier-2 source-selection pass should treat - `0x5e`-style alias roots as the stronger stock-family clue than the direct-name lanes - - the trace now keeps the explicit non-overlap residue first-class too: the current list outside - that recovered nonzero family is just `MunitionsFactory/MunitionsFactory x1`, so the next - chooser-side/source-selection slice can focus on whether that residue belongs to a zero-valued - stock-header family or to a later live projection seam rather than treating the whole nonzero - post-secondary set as one undifferentiated mystery - - that broader stock-header check is now grounded too: the checked-in `name_0x5e` dword-family - summary shows `MunitionsFactory` is not outside stock assets at all. It sits in a zero-valued - `WeaponsFactory` alias cluster (`Electric Plant`, `Fertilizer Factory`, `Munitions Factory`, - `Nuclear Power Plant`, `Oil Well`, `Weapons Factory`) while the recovered nonzero family keeps - its own `TextileMill`, `LumberMill`, `MeatPackingPlant`, `Distillery`, and `Toolndie` - clusters. So the next Tier-2 source-selection question is no longer “stock vs non-stock”; it - is which stock alias-root cluster gets selected, and why some later clone/replay paths prefer - the nonzero `0x000001f4` cluster while the peer-site residue can still surface a zero-family - `WeaponsFactory`-side root - - the stock-cluster-to-selector join is explicit now too: the checked-in `name_0x5e` + - `.bca` selector summary shows every grounded alias cluster is zero-selector by default, - including the nonzero `0x000001f4` clusters (`TextileMill x9`, `LumberMill x4`, - `MeatPackingPlant x4`, `Distillery x2`, `Toolndie x2`) and the zero-family - `WeaponsFactory x6` cluster. The only surfaced nonzero joined outlier is - `MachineShop` inside the nonzero `TextileMill` cluster (`byte_0xba = 0x3f`, `byte_0xbb = 0x00`). - So the next Tier-2 source-selection pass should no longer ask whether whole alias clusters map - to nonzero bank bytes; it should ask why one specific stock row inside the `TextileMill` - cluster surfaces a nonzero selector while its peer rows stay zero - - the direct-name plus alias plus selector join narrows that one step further: - inside the same nonzero `0x000001f4` family, the direct - `Warehouse/TextileMill/Warehouse` shape splits into six all-zero selector peers - (`ConcretePlant`, `ConstructionFirm`, `ElectronicsPlant`, `Hospital`, - `PharmaceuticalPlant`, and `Warehouse`) plus one unique selector outlier, - `MachineShop = 0x00/0x80/0x3f/0x00`. The sibling bare `Port/TextileMill/Port` row stays - all-zero. So the remaining Tier-2 question is no longer “does the bare `Port` or - `Warehouse` row carry the seeded selector?”; it is “why does one warehouse-shaped industrial - peer in that alias family carry the lone seeded selector while the bare rows do not?” - - the exact stock resolver-family strip is boxed in now too: - the checked-in `recovered_source_family_summaries` report shows every `0x00419590` - source-family row (`VictorianStation*`, `TudorStation*`, `SoWestStation*`, - `PersianStation*`, `KyotoStation*`, `ClpBrdStation*`, `Maintenance`, and `ServiceTower`) - stays on `type_id = 0x000003ec` with `dword_0xbb = 0`, and almost all of them have no `.bca` - pair at all; the only paired standalone row is `ServiceTower`, and it still carries - `byte_0xba = 0x00`, `byte_0xbb = 0x00`. So the remaining Tier-2 question is no longer - whether the exact `0x00419590` source-family strip itself carries the seeded nonzero bank - bytes; current evidence says it does not. - - the `0x00419590` caller surface is boxed in further too: - current direct caller recovery still keeps the real load-side use under the already-grounded - `0x00419230` rebank-or-clone strip, while the only newly surfaced non-local caller is - `0x00506424`, which reaches `0x00419590` from a live placed-structure consumer path that - immediately flows through `0x00402c90` and `0x0040dc40`. So the remaining Tier-2 question is - no longer whether `0x00419590` itself is a hidden load-side owner; it is still the earlier - seed-row or projection seam that makes later clone/consumer paths see nonzero bank bytes. - - the global stock `.bca` selector report narrows that one step further still: the exact - `MachineShop.bca` signature (`byte_0xb8 = 0x00`, `byte_0xb9 = 0x80`, `byte_0xba = 0x3f`, - `byte_0xbb = 0x00`) is unique across the checked-in stock corpus. So the current Tier-2 - frontier is not a broad hidden family of nonzero stock rows; it is a single surfaced stock-file - outlier plus whatever later clone/replay logic amplifies it into the numbered banked rows - - keep the already-grounded `0x0047fd50` class gate separate from that byte: direct disassembly - now says `0x0047fd50` resolves the linked peer through `[site+0x04]`, reads candidate class - byte `[candidate+0x8c]`, and returns true only for `0/1/2` while rejecting `3/4` and above, - so the next slice should not conflate the post-secondary byte with the existing - station-or-transit gate - - treat the peer-site selector seam itself as grounded enough for planning purposes - - use the new structured restore/runtime field split in the same trace: - restore subset - `[site+0x3cc/+0x3d0]` plus `0x5dc1`-backed `[owner+0x23e/+0x242]`, - and runtime subset - `[site+0x04]`, `[site+0x2a8]`, `[peer+0x08]` - - use the new structured reconstruction status in the same trace: - `restore_subset_and_bring_up_reconstruct_runtime_subset` - - treat the runtime subset as reconstructible from the restore subset plus the already-grounded - bring-up path for planning purposes - - use the new structured acquisition input families in the same trace: - region subset - `[region+0x276]`, region vtable `+0x80` byte-`0x32`, `[region+0x3d5]`, - `[region+0x310/+0x338/+0x360]`, `[region+0x2a4]`; - peer subset - center-cell token gate, `[site+0x04]`, `[site+0x2a8]`, `[peer+0x08]`, linked-region status; - company subset - stat-family reader `0x2329/0x0d`, chairman byte `[profile+0x291]`, company byte `[company+0x5b]` - plus indexed lane `[company+0x67 + 12*0x0042a0e0()]`, and the company-root argument passed into - `0x0040d540 / 0x00455f60` - - use the new shellless-readiness split in the same trace: - runtime-backed families - peer-site restore subset plus bring-up reconstruction, company stat-family `0x2329/0x0d`, - chairman byte `[profile+0x291]`, and save-native company/chairman identity; - remaining owner gaps - `[region+0x276]`, `[region+0x2a4]`, `[region+0x310/+0x338/+0x360]`, and the stable region - class/type discriminator consumed through `0x0040d360` - - use the new per-lane region status split in the same trace: - `[region+0x276]` already has a grounded runtime producer at `0x00422100` and is now only an - ordinary-save restore gap; - `[region+0x2a4]` currently has no region-class runtime writer in the binary scan and now looks - payload/restore-owned; - `[region+0x310/+0x338/+0x360]` has an exact raw delta reader at `0x0040cac0` and likewise no - direct region-class runtime writer in the current binary scan, so it now also looks - payload/restore-owned; - `0x0040d360` is now exact as `[owner_vtable+0x80+0x32] == 4`, so the remaining gap there is - only the save-native projection of that byte - - make the next periodic-company slice about the smaller shellless-simulation question instead: - which later save payload or restore owner rehydrates the remaining region-side `0x004014b0` - inputs `[region+0x2a4]` and `[region+0x310/+0x338/+0x360]` once the peer/company inputs are - treated as grounded and `[region+0x276]` is treated as a producer-known ordinary-save restore - gap -- Use the higher-layer probes as the standard entry point for the current blocked frontier instead - of generic save scans: - `runtime inspect-periodic-company-service-trace `, - `runtime inspect-region-service-trace `, and - `runtime inspect-infrastructure-asset-trace `. -- Follow the new higher-layer probe outputs instead of another blind save scan: - `runtime inspect-infrastructure-asset-trace ` now shows that the `0x38a5` - infrastructure-asset seam is grounded and the old alias hypothesis is disproved on `q.gms`, so - the next placed-structure slice should target the consumer mapping above that seam rather than - more collection discovery; the same trace now also carries atlas-backed candidate consumers - (`0x0048a1e0`, `0x0048dd50`, `0x00490a3c`, `0x004559d0`, `0x00455870`, `0x00455930`, - `0x00448a70/0x00493660/0x0048b660`, `0x004133b0`) plus bridge/tunnel/track-cap name-family - counts, so the next pass can start at those concrete owners instead of the whole - placed-structure family. -- Rehost or bound the next concrete `Infrastructure` consumer above `0x38a5` instead of treating - “consumer mapping missing” as a stop: start with the checked-in candidate strip - `0x0048a1e0`, `0x0048dd50`, `0x00490a3c`, `0x004559d0`, `0x00455870`, `0x00455930`, - `0x00448a70/0x00493660/0x0048b660`, `0x004133b0`, and narrow that list to the first true - shellless owner that consumes the side-buffer seam. The infrastructure trace now ranks the - current best hypothesis as the child attach/rebuild strip - (`0x0048a1e0`, `0x0048dd50`, `0x00490a3c`), with the serializer/load companions next and the - route/local-runtime follow-on family explicitly secondary. -- For that top-ranked infrastructure strip, treat the next pass as three exact owner questions - rather than a general “map the consumer” task: whether the `0x38a5` compact-prefix/name-pair - groups feed the first-child triplet clone lane, the caller-supplied payload-stem lane, or only a - later route/local-runtime refresh lane; which child fields or grouped rows absorb the - side-buffer payload before `0x00448a70/0x00493660/0x0048b660` become relevant; and, now that the - direct route-entry bridge helpers over `[this+0x206/+0x20a/+0x20e]` are grounded, which later - route/local-runtime owner still carries the remaining mixed exact classes once cached - primary-child slot `[this+0x248]` is demoted to child-list cache/cleanup state. -- Targeted disassembly now tightens that strip further: `0x0048a1e0` clones the first child through - `0x0052e880/0x0052e720`, destroys the prior child, seeds a literal `Infrastructure` child - through `0x00455b70` with payload seed `0x005c87a8`, and republishes the two sampled bands - through `0x0052e8b0/0x00530720` after attaching through `0x005395d0`; the non-clone branch - attaches through `0x0053a5d0`. So the next unknown is no longer whether this strip owns the - child/rebuild seam, but which `0x38a5` compact-prefix groups drive the clone-vs-payload choice. -- The outer rebuild owner is tighter now too: `0x0048dcf0` reads a child count plus optional - primary-child ordinal from the tagged stream through `0x00531150`, zeroes `[this+0x08]`, - dispatches each fresh child through `0x00455a50 -> vtable slot +0x40`, culls ordinals above `5`, - and restores cached primary-child slot `[this+0x248]` from the saved ordinal. That means the - child/rebuild loop is consuming an already-materialized child stream rather than parsing the - `0x38a5` compact-prefix seam directly. -- The upstream handoff is grounded now too: `0x00493be0` is the tagged collection load owner over - `0x38a5/0x38a6/0x38a7`, and it feeds each live infrastructure record straight into - `0x0048dcf0` after restoring one shared owner-local dword into the `0x90/0x94` lane. So the - remaining infrastructure question is no longer whether `0x38a5` reaches the child-stream restore - path at all. Direct disassembly now also shows `0x00518140` resolving a non-direct live entry by - tombstone bitset and then returning the first dword of a `12`-byte row from `[collection+0x3c]`, - while `0x00518680` loads that non-direct table family before `0x00493be0` starts iterating, and - `0x00493be0` itself now reads as an ordinal-to-live-id-to-payload-pointer walk through - `0x00518380(ordinal, 0)` then `0x00518140(live_id)`. So the next infrastructure question is no - longer “which row owns the payload pointer?”. Direct disassembly of `0x005181f0/0x00518260` now - also treats those `12`-byte rows as a live-entry directory with - `(payload pointer, previous live id, next live id)`, so the next infrastructure question is only - how those payload streams align with the embedded `0x55f1` name-pair groups and compact-prefix - regimes, and which tagged values inside each payload stream become the child count, optional - primary-child ordinal, and per-child callback sequence that `0x0048dcf0` consumes. Direct - disassembly now also shows the shared child payload callback `0x00455fc0` opening - `0x55f1 -> 0x55f2 -> 0x55f3`, parsing three `0x55f1` strings through `0x00531380`, seeding the - child through `0x00455b70`, and then dispatching slot `+0x48`; the widened save-side probe - currently sees `0` third `0x55f1` strings on grounded `q.gms`. That now looks less like a probe - failure and more like an ordinary fallback path, because direct disassembly of `0x00455b70` - stores the three payload strings into `[this+0x206/+0x20a/+0x20e]`, defaulting the second lane - through a fixed literal when absent and defaulting the third lane back to the first string when - absent. So the next pass should stay focused on payload-stream grouping and tagged value roles, - not on rediscovering a missing third-string encoding. -- The child loader identity is tighter now too: local `.rdata` at `0x005cfd00` proves the - `Infrastructure` child vtable uses the shared tagged callback strip directly, with - `+0x40 = 0x00455fc0`, `+0x44 = 0x004559d0`, `+0x48 = 0x00455870`, and `+0x4c = 0x00455930`. - Direct disassembly of `0x004559d0` then shows the concrete write-side chain for the child - payload: write `0x55f1`, serialize string lanes `[this+0x206/+0x20a/+0x20e]`, write `0x55f2`, - dispatch slot `+0x4c`, run `0x0052ec50`, and close `0x55f3`. So the remaining infrastructure - frontier is no longer “which slot does `0x00455a40` jump to?”; it is which chooser/seed values - reach those string lanes and the trailing footer path. -- That source side is narrower now too: direct disassembly shows the paired chooser siblings - calling `0x00490960` directly beside `0x0048a340/0x0048f4c0/0x00490200`, and `0x00490960` - copies selector fields into the child object (`[this+0x219]`, `[this+0x251]`, bit `0x20` in - `[this+0x24c]`, and `[this+0x226]`), allocates a fresh `0x23a` `Infrastructure` child, seeds it - through `0x00455b70` with caller-supplied stem input plus fixed literal `Infrastructure` at - `0x005cfd74`, attaches it through `0x005395d0`, seeds position lanes through - `0x00539530/0x0053a5b0`, and can cache it as primary child in `[this+0x248]`. The remaining - problem is no longer “where do the child payload lanes come from?” but “which chooser branches - feed `0x00490960` which caller stem and selector tuple for each grounded save-side class?”. -- One direct branch is grounded now too: the repeated chooser calls at - `0x004a2eba/0x004a30f9/0x004a339c` all feed `0x00490960` with mode arg `0x0a` and stem arg - `0x005cb138 = BallastCapDT_Cap.3dp`, which means they bypass the selector-copy block at - `0x004909e2` and go straight into fresh child allocation/seeding. So the remaining source-side - mapping problem is no longer generic BallastCap coverage; it is the other constructor branches, - especially the ones with mode `< 4` that actually populate the selector-byte copy block. -- The broader mode family is grounded now too. A wider static callsite sweep shows: - - mode `0x0b` with fixed `TrackCapDT_Cap.3dp` / `TrackCapST_Cap.3dp` - - mode `0x03` with `OverpassST_section.3dp` - - mode `0x02` with decoded tunnel table stems plus zero-stem fallbacks - - mode `0x01` with decoded bridge table stems plus zero-stem fallbacks +- Keep the periodic-company trace as the main shellless simulation frontier. +- Push the next static/rehost slice through the near-city industry acquisition owner seam under `0x004014b0`. +- Keep the building style-family source pass focused on `0x004196c0 -> 0x00414490 -> 0x00416ce0 -> 0x00419230`. - The current grounded `q.gms` name corpus now also maps directly onto most of those families: - `BridgeSTWood_Section.3dp -> mode 0x01`, `TunnelSTBrick_* -> mode 0x02`, - `BallastCapST_Cap.3dp -> mode 0x0a`, and `TrackCapST_Cap.3dp -> mode 0x0b`, with only - `Overpass` still static-only in the current save corpus. +## Preserved Detail - So the remaining infrastructure question is no longer “what does `0x00490960` build?” or even - “which family is this name row?” but “how do the surviving compact-prefix regimes subdivide - those already-mapped families, especially inside bridge mode `0x01` and track-cap mode `0x0b`?”. -- The direct route-side bridge is grounded now too: `0x0048e140/0x0048e160/0x0048e180` simply - resolve `[this+0x206/+0x20a/+0x20e]` through live route collection `0x006cfca8`, and - `0x0048e1a0` compares those resolved peers against `[this+0x202]`. The neighboring - `0x0048ed30` path is now also narrower: it only tears down child list `[this+0x08]`, clearing - cached primary-child slot `[this+0x248]` when needed, so `[this+0x248]` is no longer the first - route bridge to chase. -- The later route/local-runtime follow-on family is tighter now too: `0x00448a70` is a - world-overlay helper over `[world+0x15e1/+0x162d]`, `0x00493660` is a counter-plus-companion- - region follow-on keyed by `[child+0x218]`, `[child+0x226]`, `[child+0x44]`, and `0x0048dcb0`, - `0x0048b660` is a presentation-color/style owner over `[child+0x216/+0x218/+0x226/+0x44]` and - bit `0x40` in `[child+0x201]`, and `0x0048e2c0/0x0048e330/0x0048e3c0` now read as flag / route- - tracker / region-test helpers rather than hidden payload decoders. So the next infrastructure - slice should stay focused on the remaining mixed exact compact-prefix classes and earlier - child-stream semantics, not on rediscovering the already-bounded presentation owners. -- The new probe correlation now makes that residual even more concrete: on grounded `q.gms`, the - dominant mixed `0x0001/0xff` class splits as `bridge:62 / track_cap:21 / tunnel:19`, while the - pure `0x0002/0xff` class is all bridge and the pure `0x0055/0x00` class is all ballast-cap. - So the next infrastructure slice should focus on subdividing the mixed one-child `0x0001/0xff` - class rather than revisiting the already-grounded pure classes. -- The sibling `0x00490200` is tighter now too: it reads the seeded lanes - `[this+0x206/+0x20a/+0x20e]` back through the live route collection at `0x006cfca8`, compares - them against the current owner using `[this+0x216/+0x218/+0x201/+0x202]`, and behaves like a - route/link comparator layered above the same child payload lanes that `0x004559d0` later - serializes. So the next infrastructure pass should treat `0x00490960` as the source owner and - `0x00490200` as a consumer of the same seeded lanes, not as separate unexplained seams. -- The smaller helper `0x00490a3c` is narrower now too: it allocates one literal `Infrastructure` - child, seeds it through `0x00455b70` with caller-provided stem input, attaches it through - `0x005395d0`, seeds position lanes through `0x00539530/0x0053a5b0`, and optionally caches it as - the primary child. So the next concrete infrastructure question is which upstream owner - maps the direct `0x38a5` rows into the child count, primary-child ordinal, and per-child payload - callbacks consumed by `0x0048dcf0`, and which restored child fields still retain those embedded - name-pair semantics before route/local-runtime follow-ons take over. -- The save-side `0x38a5` probe is now tighter at the payload-envelope level too: grounded - `q.gms` shows all `138` embedded `0x55f1` rows already live inside complete - `0x55f1 -> 0x55f2 -> 0x55f3` envelopes before the next name row, every embedded `0x55f2` chunk - is the fixed `0x1a` bytes that `0x00455fc0` expects, and the dominant embedded `0x55f3` - payload-to-next-name span is the short `0x06`-byte form across `72` rows. So the next - infrastructure pass should stop asking whether the shared tagged callback sequence is present at - all and instead decode the short `0x55f3` payload role and its relation to the compact-prefix - regimes and primary-child restore path. -- That short trailing lane is tighter now too: direct disassembly of `0x0052ebd0/0x0052ec50` - shows the post-`+0x48` helper pair loading and serializing two single-byte lanes that fold into - bits `0x20` and `0x40` of `[this+0x20]`, and the save-side probe now shows the dominant - `0x06`-byte rows all carrying the same grounded flag pair `0x00/0x00` on `q.gms`. So the next - concrete infrastructure question is no longer “is there a short trailing flag lane?”; it is how - the compact-prefix regimes and those flag-byte pairs feed the child-count / primary-child restore - state above `0x0048dcf0`. -- The fixed `0x55f2` lane is tighter now too: direct disassembly of `0x00455870/0x00455930` shows - the `+0x48/+0x4c` strip loading and serializing six `u32` lanes from the fixed `0x1a` chunk, - forwarding them through `0x00530720` and `0x0052e8b0`. Grounded `q.gms` probes now show every - embedded `0x55f2` row using the same trailing word `0x0101` while those six dword lanes vary by - asset row. So the next infrastructure question is no longer whether `0x55f2` is a fixed-format - child lane; it is which of those two dword triplets correspond to child-count / primary-child - restore state and which only seed published anchor or position bands. -- That split is tighter now too: direct disassembly of `0x00530720/0x0052e8b0` shows the first - fixed `0x55f2` triplet landing in `[this+0x1e2/+0x1e6/+0x1ea]` and the second in - `[this+0x4b/+0x4f/+0x53]`, with the companion setter also forcing bit `0x02`. So the next - infrastructure question is no longer whether the fixed `0x55f2` row hides the child count or - primary-child ordinal at all; those outer-header values now have to live outside the fixed row, - most likely in the surrounding payload-stream header or compact-prefix regime above - `0x0048dcf0`. -- The outer prelude itself is tighter now too: direct disassembly of `0x0048dcf0` shows it reading - one `u16` child count through `0x00531150`, zeroing `[this+0x08]`, and conditionally reading one - saved primary-child byte before the per-child callback loop runs. Grounded `q.gms` bytes now also - show the first `0x38a6` record starting immediately after the shared owner-local dword with - `child_count = 1`, `saved_primary_child_byte = 0xff`, and the first child `0x55f1` opening at - offset `+0x3`. So the next infrastructure question is no longer “what kind of values are we - looking for above the fixed rows?”; it is the narrower partitioning problem of how the observed - `0x55f3`-to-next-`0x55f1` gaps divide between the two `0x52ebd0` flag bytes and the next - record’s `u16 + byte` prelude. -- The widened prelude correlation closes part of that partitioning too: grounded `q.gms` rows with - a `0x03` post-profile gap now collapse cleanly to the next-record prelude pattern - `0x0001 / 0xff` across `17/17` rows, while the zero-length class is a separate grounded outlier - with dominant pattern `0x0055 / 0x00` across `18/18` rows and the `0x06` class remains the only - large mixed frontier. So the next infrastructure slice should focus on classifying the mixed - `0x06` rows, not on rediscovering the already-grounded pure-prelude `0x03` rows. -- That `0x06` class is now narrower too: grounded `q.gms` shows the dominant short-span class as - `BridgeSTWood_Section.3dp / Infrastructure` with compact prefix `0xff000000 / 0x0001 / 0xff` - across `62/72` rows and dominant prelude candidate `0x0001 / 0xff` across `63/72` rows. So the - next infrastructure slice should stop treating the `0x06` class as uniformly ambiguous and focus - on the smaller outlier families inside that class, especially the zero-like `BallastCap`-style - rows and any remaining non-`0x0001 / 0xff` prelude candidates. -- The exact compact-prefix classes are explicit across the whole prelude now too: - `0xff0000ff / 0x0002 / 0xff` is a pure bridge class, `0xff000000 / {0x0001,0x0002} / 0xff` - are pure bridge classes, `0xf3010100 / 0x0055 / 0x00` is a pure `BallastCap` class, and - `0x0005d368 / 0x0001 / 0xff` is a pure one-row `TrackCap` class. -- That sharpens the remaining infrastructure unknowns considerably: the only mixed exact - compact-prefix classes left on grounded `q.gms` are `0x000055f3 / 0x0001 / 0xff` and - `0xff0000ff / 0x0001 / 0xff`. -- The current `0x000055f3 / 0x0001 / 0xff` class is tunnel-dominant: - `TunnelSTBrick_Section.3dp / Infrastructure:13`, `TunnelSTBrick_Cap.3dp / Infrastructure:4`, - `TrackCapST_Cap.3dp / Infrastructure:0` in the exact-prefix correlation, with all `17` rows - staying on prior profile span `0x03`. -- The current `0xff0000ff / 0x0001 / 0xff` class is `TrackCap`-dominant but still carries `4` - tunnel rows: - `TrackCapST_Cap.3dp / Infrastructure:18`, - `TunnelSTBrick_Cap.3dp / Infrastructure:2`, - `TunnelSTBrick_Section.3dp / Infrastructure:2`. - Its rows are spread across many spans rather than one dominant restore span. -- Cross-save `q.gms` / `p.gms` traces sharpen that split further without changing it: - `0x000055f3 / 0x0001 / 0xff` stays on prelude `0x0001 / 0xff`, fixed short-flag pair - `0x01 / 0x00`, and fixed prior profile span `0x03` in both saves, while - `0xff0000ff / 0x0001 / 0xff` stays on prelude `0x0001 / 0xff`, fixed short-flag pair - `0x00 / 0x00`, and widely scattered prior profile spans in both saves. -- Direct consumers of those footer bits are grounded now too: `0x00528d90` only admits the child - when the explicit caller override is set, the surrounding global override byte - `[owner+0x3692]` is set, or bit `0x20` in `[child+0x20]` is set; the sibling loop - `0x00529730` only takes the later `0x530280` follow-on when bit `0x40` in `[child+0x20]` is - set. -- That footer-bit consumer strip is tied to a broader higher-layer owner family now too: - `0x005295f0..0x005297b7` repopulates candidate cells through `0x00533ba0`, walks candidate - child lists through `0x00556ef0/0x00556f00`, and honors the same controller mode byte - `[owner+0x3692]` that the atlas already places under the world-window presentation dispatcher. -- The neighboring helpers tighten that owner family further: atlas-backed `0x00533ba0` is the - nearby-presentation cell-table helper under the layout/presenter strip, direct disassembly shows - `0x00548da0` walking layout list root `[layout+0x2593]`, and direct disassembly of `0x0054bab0` - mutates layout slots `[layout+0x2637/+0x263b/+0x2643]`. -- That means the remaining infrastructure question is no longer both footer bytes. It is - specifically why the stable `0x000055f3 / 0x0001 / 0xff` tunnel family sets the first footer - byte / bit-`0x20` admission gate while the sparse `0xff0000ff / 0x0001 / 0xff` outlier class - clears it, inside that layout/presentation owner family rather than at the serializer layer. -- Source-side constructor analysis is narrower now too. `0x00490960` takes: - - mode at stack arg 1 - - stem at stack arg 2 - - args 3/4 into `0x539530` - - arg 5 into `0x53a5b0` - - arg 10 as the primary-child cache gate for `[this+0x248]` - - args 7/8/9 into the selector-copy block for `[this+0x219]`, `[this+0x251]`, and bit `0x20` - in `[this+0x24c]` when `mode < 4` -- That already separates the remaining mixed classes: - - fixed `TrackCap` mode `0x0b` callers at `0x0048ed01/0x0048ed20` push arg7/arg8/arg9 as - `-1 / -1 / 0` and bypass selector-copy entirely because `mode >= 4` - - tunnel mode `0x02` callers at - `0x004a17eb / 0x004a1995 / 0x004a1b44 / 0x004a1b7d / 0x004a1b95` - necessarily flow through selector-copy because `mode < 4`, with arg8 fixed at `1`, arg9 - fixed at `0`, and only arg7 varying through a branch-local one-bit register -- So the next infrastructure slice should stop treating the remaining frontier as a generic - “mixed 0x06/outlier” problem and instead target the owning constructor/restore semantics for - those two exact mixed compact-prefix classes, especially how tunnel arg7 and the fixed - `TrackCap` no-selector bundle both still collapse into the observed mixed save-side prefixes. -- The candidate-pattern classes are now explicit across the whole stream too: `0x0055 / 0x00` - is a pure `BallastCapST_Cap.3dp / Infrastructure` class across `18` rows, always preceded by a - zero-length prior profile span, while `0x0002 / 0xff` is a pure - `BridgeSTWood_Section.3dp / Infrastructure` class across `18` rows with dominant prior profile - span `0x06` (`10` rows). So the next infrastructure pass should split its owner questions: - treat `0x0055 / 0x00` as a `BallastCap`-specific boundary artifact class, and treat - `0x0002 / 0xff` as the grounded save-side bridge-specific two-child candidate class above - `0x0048a1e0/0x0048dcf0`, with the remaining unknown narrowed to the upstream chooser that emits - that class before the attach/rebuild path runs. -- That upstream chooser is grounded now too as paired siblings: direct disassembly shows - `0x004a2c80` routing the `DT` family and `0x004a34e0` routing the `ST` family, with both - repeatedly calling `0x0048a1e0`, branching on `[this+0x226]`, selector bytes - `[this+0x219]/[this+0x251]`, bit `0x20` in `[this+0x24c]`, and lookup tables `0x621a44..0x621a9c`, - then routing follow-on through `0x0048a340/0x0048f4c0/0x00490200/0x00490960`. So the remaining - infrastructure question is no longer “is there an upstream chooser?” but “how do the save-side - classes select the `DT` versus `ST` chooser sibling, and then which lookup-table families inside - that sibling map to the grounded `0x0002 / 0xff` bridge class and the `0x0055 / 0x00` - BallastCap class?”. -- Those lookup tables are decoded now too: `0x621a44/0x621a54` feed `BridgeST` caps/sections, - `0x621a64` feeds `TunnelST` cap/section variants, `0x621a74/0x621a84` feed `BridgeDT` - caps/sections, and `0x621a94` feeds `TunnelDT` variants, while fixed literals - `0x5cb138/0x5cb150` are `BallastCapDT/ST` and `0x5cb168/0x5cb180` are `OverpassDT/ST`. So the - remaining infrastructure question is no longer table discovery; it is the selector-byte mapping - from `[this+0x219]/[this+0x251]/[this+0x252]` onto those decoded families and then onto the - grounded `0x38a5` prefix classes. -- The top-level chooser meaning is grounded now too: within those paired DT/ST siblings, - `[this+0x226]==1` routes the bridge families, `[this+0x226]==2` routes the tunnel families, and - `[this+0x226]==3` routes the overpass/ballast family, while bit `0x20` in `[this+0x24c]` - selects the cap-oriented side over the section-oriented side. So the remaining infrastructure - selector problem is below that top-level split: the exact `[this+0x219]/[this+0x251]` values - that choose the decoded family entries and how those values surface in the save-side `0x38a5` - classes. -- Those material selectors are grounded now too: within the bridge branch, `[this+0x219]` - selects `steel`, `stone`, `suspension`, or `wood`, with value `2` taking the special - suspension-cap path through `[this+0x252]`; within the tunnel branch, `[this+0x251]` selects - `brick` versus `concrete`, while bit `0x20` chooses cap versus section by switching between the - base and `+0x8` table entry families. So the remaining infrastructure selector problem is no - longer “what do these bytes mean?” but “how do those already-grounded selector values surface in - the save-side `0x38a5` classes, especially the `0x0002 / 0xff` bridge class and the - `0x0055 / 0x00` BallastCap class?”. -- The exact setter seam is grounded now too: direct disassembly of `0x0048a340` shows its dword - argument writing `[this+0x226]`, its next two byte arguments writing `[this+0x219]` and - `[this+0x251]`, and its final byte argument toggling bit `0x20` in `[this+0x24c]`. So the - remaining infrastructure selector problem is no longer about hidden intermediate state; it is - specifically how those already-grounded setter values are serialized or rebuilt into the - save-side `0x38a5` prefix classes. -- One selector byte is partly grounded now too: when `[this+0x219]==2`, the chooser jump tables - stop using the general bridge families and instead route `[this+0x252]` through fixed - `BridgeDT/BridgeST` suspension-cap literals for `R10`, `L10`, `12`, `14`, `16`, and `18`. - So the remaining infrastructure selector problem is mostly `[this+0x219]/[this+0x251]` family - choice plus the exact save-side class mapping for the BallastCap branch. -- The current real-save corpus also narrows the active side further: grounded `q.gms`, `p.gms`, - `g.gms`, and `nom.gms` only expose `ST`-family side-buffer names, while classic `rt3/` saves in - this workspace currently expose no `0x38a5` side-buffer seam at all. So the save-driven part of - the next infrastructure slice should assume it is exercising the `ST` chooser sibling directly, - with `DT` still grounded statically but not yet exercised by the current save corpus. -- Reconstruct the save-side region record body on top of the newly corrected non-direct tagged - region seam (`0x5209/0x520a/0x520b`, stride hint `0x06`, `Marker09` record stems) now that the - `0x55f3` payload is known to be fully consumed by the embedded profile collection on grounded - real saves: the remaining blocker is no longer a hidden trailing payload tail, but finding the - separate save-owner seam for the pending bonus lane `[region+0x276]`, completion latch - `[region+0x302]`, one-shot notice latch `[region+0x316]`, severity/source lane `[region+0x25e]`, - and any stable region-id or class discriminator that can drive shellless city-connection - service. The newly grounded queue-node probe for the atlas-backed kind-`7` notice records is a - negative result on `q.gms`, `p.gms`, and `Autosave.gms`, so the next region pass should not - assume that the transient `[world+0x66a6]` queue family is persisted in ordinary saves; the - region trace now also carries the concrete queued/service owners (`0x00422100`, `0x004337c0`, - `0x00437c00`, `0x004c7520`, `0x004358d0`, `0x00438710`, `0x00420030/0x00420280`, - `0x0047efe0`) so the next pass can focus on the missing saved latches and stable region id/class - rather than on rediscovering the outer service family. -- Rehost or bound the next concrete region owner above the missing latches instead of treating the - absent persisted queue as a stop: start with the checked-in owner strip `0x00422100`, - `0x004337c0`, `0x00437c00`, `0x004c7520`, `0x004358d0`, `0x00438710`, - `0x00420030/0x00420280`, `0x0047efe0`, and reduce it to the first true save-owned or rebuild - owner that can explain `[region+0x25e/+0x276/+0x302/+0x316]` plus a stable region id/class. The - region trace now ranks the current best hypothesis as the pending bonus service owner - (`0x004358d0`) plus the peer/linkage strip (`0x00420030/0x00420280`, `0x0047efe0`), with the - transient producer/queue family explicitly secondary and the queued kind-`7` modal dispatch kept - as shell-adjacent reference only. -- For that top-ranked region strip, treat the next pass as three exact owner questions too: which - restore seam re-seeds `[region+0x25e]` and clears `[region+0x302/+0x316]` before the grounded - `0x00422100 -> 0x004358d0` producer/consumer cycle runs again, which stable region id or class - discriminator survives save/load strongly enough to drive `0x004358d0`, and how far the grounded city-connection peer/linkage helpers - (`0x00420030/0x00420280`, `0x0047efe0`) can be reused directly before the transient queued-notice - family matters again. -- Targeted disassembly now tightens that strip too: `0x004358d0` calls `0x00420030` twice plus - `0x00420280`, then resolves the linked company through `0x0047efe0`, posts company stat slot `4` - on success, and stamps `[region+0x302]` or `[region+0x316]` while clearing `[region+0x276]`. - `0x00420030` itself now reads as the real peer gate over collection `0x006cec20`, combining - `0x0042b2d0`, the optional company filter through `0x0047efe0`, the station-or-transit gate - `0x0047fd50`, and the status branch `0x0047de00 -> 0x0040c990`; `0x00420280` is the same scan - returning the first matching site id. So the remaining unknown is the persisted latch/id seam, - not the live peer/service logic. -- The producer half is grounded now too: `0x00422100` filters for class-`0` regions with - `[region+0x276]==0` and `[region+0x302]==0`, rejects already-connected pairs through - `0x00420030(1,1,0,0)`, chooses one eligible candidate, buckets severity/source lane - `[region+0x25e]` against the three checked thresholds, writes the resulting amount to - `[region+0x276]`, and appends the kind-`7` queued notice through `0x004337c0`. That means the - remaining region gap is now explicitly the upstream restore seam for `[region+0x25e]` and the - completion/fallback latch clear, not either side of the producer/consumer service pair. -- The severity/source lane itself is narrower now too: `0x004cc930` is a selected-region editor - helper that writes `[region+0x25a]` and `[region+0x25e]` together from one integer input, while - `0x00438150` and `0x00442cc0` are fixed-region global reseed/clamp owners over collection - `0x0062bae0` that adjust the same mirrored pair for hardcoded region ids. So the remaining - region restore question is no longer “what does `[region+0x25e]` mean?” but “which load/reseed - seam restores the mirrored severity pair before the producer runs?” -- Two more direct-hit writer bands are now explicitly ruled out too: `0x0043a5a0` is a separate - constructor under vtable root `0x005ca078` that zeroes its own `[this+0x302/+0x316]` fields - during local object setup, and `0x0045c460/0x0045c8xx` is a separate vtable-`0x005cb5e8` helper - family whose `[this+0x316]` is a child-array pointer serialized through `0x61a9/0x61aa/0x61ab`. - So those offset-collision classes should stay out of the remaining region restore search. -- The direct writer census is tighter now too: the other apparent `0x302/0x316` writer bands - (`0x0043dd45`, `0x0043de19`, `0x0043e0a7`, `0x0043f5bc`) all hang off that same non-region - `0x005ca078` family through helpers `0x0043af60` and `0x0043b030`. So the only grounded - region-owned literal writes left are the constructor `0x00421200` plus the producer/consumer - pair `0x00422100` and `0x004358d0`, which means the remaining region seam should now be treated - as an indirect restore/rebuild path rather than another direct offset writer hunt. -- The later post-load per-region sweep is narrowed too: in the broader `0x00444887` restore strip, - the follow-on loop at `0x00444b90` dispatches `0x00420560` over each live region, but that - helper only zeroes and recomputes `[region+0x312]` from the embedded profile collection - `[region+0x37f]/[region+0x383]` and lazily seeds the year-driven `[region+0x317/+0x31b]` band - through `0x00420350`. It still does not touch `[region+0x276/+0x302/+0x316]`, so that whole - follow-on branch should stay out of the remaining latch-restore search too. -- The checked-in constructor owner `0x00421200` - `world_region_construct_entry_with_id_class_and_default_marker09_profile_seed` now also grounds - the initialization side of this family: it clears `[region+0x276]`, `[region+0x302]`, - `[region+0x316]`, and neighboring cached bands at construction time while seeding - `[region+0x25a/+0x25e] = 100.0f` and `[region+0x31b] = 1.0f`. That means the remaining queue item - is specifically post-construction restore or rebuild of the same latches, not their basic field - identity. -- The next restore-side target is explicit now too: the checked-in function map already grounds - `0x00421510` as the tagged region-collection load owner that dispatches each live region through - vtable slot `+0x40`, and `0x0041f5c0` as the per-record load slot that reloads the tagged payload - through `0x00455fc0` before rebuilding profile collection `[region+0x37f]`. So the next region - pass should ask whether `[region+0x276/+0x302/+0x316]` are restored directly inside that payload - load or rebuilt immediately after it, rather than treating “restore seam” as a generic unknown. -- Direct disassembly now closes that callback identity too: `0x0041f590/0x0041f5b0` prove the - world-region vtable root is `0x005c9a28`, so the `0x00455fc0` dispatch at slot `+0x48` lands on - `0x00455870` and the serializer sibling at `+0x4c` lands on `0x00455930`. Those two callbacks - only restore and serialize two helper-local three-lane scalar bands: `0x00455870` reads six - dwords through `0x00531150` and forwards them to `0x00530720 -> [helper+0x1e2/+0x1e6/+0x1ea]` - and `0x0052e8b0 -> [helper+0x4b/+0x4f/+0x53]`, while `0x00455930` writes that same pair back - through `0x00531030`; they still do not touch acquisition-side lanes - `[region+0x2a4]` or `[region+0x310/+0x338/+0x360]`, and they still do not touch - `[region+0x276/+0x302/+0x316]`. That means the remaining region restore target is now the later - owner that rebuilds those latches or the separate tagged body seam that persists them. -- The save-side region payload probe is wider now too: the checked-in `region_record_triplets` - surface no longer stops at raw pre-name prefix bytes and now also emits structured prefix dword - candidates per record, and the fixed `0x55f2` policy chunk now also carries structured reserved - dword candidates instead of raw integers only. That gives the next region payload pass a direct - way to compare both opaque payload bands against the remaining acquisition-side lane shapes - instead of redoing raw hex inspection by hand. -- Grounded real-save output now narrows that new probe two steps further: on both `p.gms` and - `q.gms`, every decoded region triplet currently still has `pre_name_prefix_len = 0`, an empty - `pre_name_prefix_dword_candidates` vector, and `fixed 0x55f2 policy reserved dwords are nonzero - on 0 of 145 decoded region records`. So the remaining acquisition-side payload target does not - appear to live in either the pre-`0x55f1` prefix band or the fixed `0x55f2` reserved dword band - on grounded ordinary saves. That shifts the next region payload-comparison pass onto later body - seams, not back onto the prefix or fixed-policy chunk. -- The new fixed-row run candidate probe pushes that same payload search one seam later, but it is - not grounded yet: on both `p.gms` and `q.gms` it finds high-signal counted runs keyed to the - live region count `145` with fixed row stride `0x29` before the tagged `0x5209/0x520a/0x520b` - region collection, yet the top candidate offset is not stable (`p.gms = 0xd13239`, - `q.gms = 0xd2d7d7`). So the next region payload pass should compare candidate lane-shape - fingerprints across saves rather than promoting any one absolute pre-header offset as the fixed - restore seam. -- The new two-save `runtime compare-region-fixed-row-runs ` report now does - that comparison directly. Current result: `p.gms` vs `q.gms` has `0` exact shape overlaps, and - the only coarse family overlaps are lower-ranked fully mixed candidates where every dword lane is - still simultaneously small-nonzero and partially-zero. That means the fixed-row scan remains - useful negative evidence, but it is still not honest to promote as the missing region restore - seam; the next region pass should stay focused on later restore owners or a more selective row - family discriminator above this mixed pre-header corpus. -- The rest of `0x00455fc0` is ruled down further now too: after the `+0x48` callback it only runs - `0x0052ebd0`, which reads two one-byte generic flags through `0x531150` into base object bytes - `[this+0x20]`, `[this+0x8d]`, `[this+0x5c..+0x61]`, `[this+0x1ee]`, `[this+0x1fa]`, and - `[this+0x3e]`, and then it opens `0x55f3` only for span accounting before returning. So the - missing region latches are not hiding in the remainder of `0x00455fc0` either. -- The next restore-handoff strip is explicit now too: the region trace now carries a dedicated - later-global-restore hypothesis for `0x00444887`, because that continuation is the first caller - checkpoint above the ruled-down `0x00421510 -> 0x0041f5c0 -> 0x00455fc0` path. It immediately - advances into `0x00487c20` territory refresh and `0x0040b5d0` support refresh, then later - re-enters the per-region follow-on loop at `0x00444b90 -> 0x00420560`. Current disassembly keeps - `0x00420560` on the profile/class-mix scalar side only: it recomputes `[region+0x312]` from the - embedded profile collection and linked placed-structure class mix, then seeds the year-driven - `[region+0x317/+0x31b]` band through `0x00420350`. So the next region pass should treat the - broader `0x00444887` continuation as the live handoff seam when chasing - `[region+0x2a4]` and `[region+0x310/+0x338/+0x360]`, not as just another generic restore note. -- That same continuation is less ambiguous now too: the loader immediately after territory/support - refresh, `0x00433130`, is only - `scenario_event_collection_refresh_runtime_records_from_packed_state`, opening `0x4e99/0x4e9a`, - repopulating live event collection `0x0062be18` through `0x0042db20`, and closing `0x4e9b`. - So the event-side branch under `0x00444887` is ruled out as the missing later region restore - handoff too. -- That same continuation is slightly less symmetric now too: the atlas-backed territory side at - `0x00487c20` currently restores only collection metadata/live ids and still uses no-op per-entry - load/save callbacks `0x00487670/0x00487680`, so the next pass should bias more heavily toward - support refresh `0x0040b5d0` or the later region-local rebuild than toward territory payload as - the hidden source of `[region+0x2a4]` and `[region+0x310/+0x338/+0x360]`. -- The support side is less opaque now too: the same atlas already bounds `0x0040b5d0` above - support collection `0x0062b244`, whose grounded live owners maintain goose-entry counters, - neighboring world support lanes `[world+0x4c9a/+0x4c9e/+0x4ca6/+0x4caa]`, and selected - support-entry state rather than an obvious per-region acquisition latch family. So the next pass - should now bias even more toward the later region-local rebuild beneath the `0x00444887` - continuation, while still keeping `0x0040b5d0` as a weaker adjacent prerequisite rather than - treating it as the primary hidden owner. -- The next owner family is narrower now too: the checked-in shell-load subgraph and function map - place `world_load_saved_runtime_state_bundle` `0x00446d40` directly ahead of the post-load - generation pipeline `0x004384d0`, which is now the first explicit non-hook owner family above - the ruled-down `0x00444887` continuation. The current grounded stage order is concrete enough to - split the next static pass: `319` refreshes route entries, auxiliary route trackers, and then the - placed-structure replay strip `0x004133b0`; `320` runs the region-owned building setup strip - `0x00421c20 -> 0x004235c0`; and `321` runs the economy-seeding burst `0x00437b20` plus the - cached region summary refresher `0x00423d30`. That means the next region closure pass should - chase this `0x004384d0` handoff family directly instead of treating the remaining - `[region+0x2a4]` / `[region+0x310/+0x338/+0x360]` gap as a generic continuation below - `0x00444887`. -- The early `0x004384d0` setup strip is tighter now too: before the conditional `320/321` gates it - always runs `0x0044fb70` transport/pricing-grid setup and `0x0041ea50` - candidate-local-service setup, and the extra arg-guarded `0x00421b60 -> 0x004882e0` - default-region pair sits beside them as the last pre-`320/321` setup branch. Those helpers are - already grounded as world-grid, candidate-table, and border-refresh owners rather than the - missing `[region+0x2a4]` / `[region+0x310/+0x338/+0x360]` republisher, so they drop out of the - remaining region-handoff search too. -- The `319` lane is the strongest bridge inside that family: `0x004133b0` drains queued - placed-structure ids through `0x0040e450`, sweeps every live site through `0x0040ee10`, and then - reaches the already-grounded linked-site follow-on `0x00480710`. The `320` and `321` lanes are - still explicit but weaker: `0x00421c20 -> 0x004235c0` stays on region-side demand balancing and - structure placement, while `0x00437b20 -> 0x00423d30` only refreshes the cached category band - `[region+0x27a/+0x27e/+0x282/+0x286]`. So the next non-hook region work should start from the - post-load `319` placed-structure replay seam and only then revisit the narrower region-side - `320/321` branches if the exact field bridge is still missing. -- The `319` adjacency is tighter in another negative way now too. Direct disassembly of - `0x004377a0` shows the post-`319` call staying on chairman-slot/profile materialization: it - normalizes the `16` slot bundles at `[world+0x69da..]`, republishes selector bytes into - `[0x006cec7c+0x87]`, populates the live chairman-profile collection `0x006ceb9c`, and clears the - selected company/chairman bytes `[world+0x21/+0x25]` before the optional `0x0047d440` news - follow-on. The same trace rules down the companion queue strip too: `0x004348e0` is only the - gate for the transient list at `[world+0x66a6]`, `0x00437c00` is its typed dispatcher over - queue-node kind byte `[node+0x08]`, and the later `0x0044d410` calls are world-rect/grid refresh - work. None of those helpers republish `[region+0x2a4]` or `[region+0x310/+0x338/+0x360]`, so the - remaining region search stays centered on `0x004133b0 -> 0x0040ee10 -> 0x00480710` and the later - post-load continuation above it rather than the chairman/queue-side `319` neighbors. -- The `319` plateau is split more cleanly now too: its neighboring world helpers are no longer - plausible region-body owners. `0x00437220` and `0x004377a0` are the chairman-slot selector and - profile materialization family over `[world+0x69d8]`, `[world+0x69db]`, and scenario selector - bytes `[0x006cec7c+0x87]`, while `0x00434d40` is only the subtype-`2` candidate runtime-latch - seeder over live placed structures. That leaves `0x004133b0` as the only region-adjacent `319` - bridge worth chasing for the missing restore seam. -- The `320` branch is narrower than that headline now too: direct worker recovery shows - `0x004235c0` staying inside the live region demand-and-placement family by routing through - `0x00422900` cached category accumulation, `0x004234e0` projected structure-count scalars, - `0x00422be0` placed-count subtraction, and `0x00422ee0` placement attempts over the live - placed-structure registry `0x0062b26c`. That keeps it on live setup and maintenance work rather - than any restore-time republisher for `[region+0x2a4]` or `[region+0x310/+0x338/+0x360]`. -- The `321` branch is narrower in the same way: `0x00437b20` only stages the fast-forward burst, - then re-enters the live region collection through `0x00423d30`, and that helper only republishes - `[region+0x27a/+0x27e/+0x282/+0x286]` via `0x00422900`. It should stay ruled down as a cached - summary refresh instead of a plausible owner for the missing restore-side body lanes. -- The later restore-side region owners are narrowed further now too: the `0x00421ce0 -> - 0x0041fb00 -> 0x00421730` sweep is class-`0` raster/id rebuild, `0x004881b0` is a companion - region-set cell-count rebuild over `[region+0x3d/+0x41]`, `0x00487de0` is a border-segment - emitter over the world raster, and `0x0044c4b0` is the center-cell bit-`0x10` reseed pass. So - the next region slice should stop revisiting those later owners and stay focused on the still- - missing save-owned latch / severity / stable-id seam. -- The later class-`0` batch at `0x00438087` is narrowed now too: it walks live class-`0` regions - through `0x0062bae0`, rescales the mirrored severity/source pair `[region+0x25a/+0x25e]` from - the current value using world-side factors, clamps the result, and then hands the collection to - `0x00421c20`; it still does not touch `[region+0x276/+0x302/+0x316]`. -- Its follow-on `0x00421c20` is bounded as a parameterized region-collection helper rather than a - latch owner: it loops the same collection with caller-supplied scalar arguments, dispatches each - record through `0x004235c0`, and does not write the pending/completion/one-shot lanes directly. -- The subsequent world follow-ons are narrower too: `0x00437b20` only stages a world-side reentry - guard at `[world+0x46c38]`, iterates the live region collection through `0x00423d30`, and tails - into `0x00434d40`, while `0x00437220` rebuilds broader world byte-set state around - `[world+0x66be/+0x69db]` and other global collections. Those later branches should stay out of - the remaining region latch-restore search too. -- The widened real-save region trace rules out one more false lead too: on grounded saves the - `0x55f2` fixed-policy chunk keeps all three reserved dwords at `0x00000000` and the trailing word - at invariant `0x0001`, so that fixed chunk is not currently carrying the missing latch or stable - region id/class discriminator either. -- Reconstruct the save-side placed-structure collection body on top of the newly grounded - `0x36b1/0x36b2/0x36b3` header seam so the blocked city-connection / linked-transit branch can - stop depending on atlas-only placed-structure and local-runtime refresh notes, especially the - semantics of the now-grounded compact `0x55f3` footer dword/status lane and the newly exposed - separate tagged side-buffer seam candidates, especially the exact `0x38a5/0x38a6/0x38a7` - family whose compact `6`-byte header pattern and embedded placed-structure-style `0x55f1` - name rows now make it the grounded placed-structure dynamic side-buffer owner; the remaining - blocker is semantic closure of the compact prefix regimes now summarized in real saves as seven - stable patterns on `q.gms` and their relation to the embedded `0x55f1/0x55f2/0x55f3` row - subset, especially now that the side-buffer name-pair corpus is proven disjoint from the - grounded `0x36b1` triplet name-pair corpus on `q.gms`; the next pass should treat `0x38a5` as - a separate infrastructure-asset owner seam, not a compact alias over the triplet records. -- Extend shellless clock advancement so more periodic-company service branches consume owned - runtime time state directly instead of only the explicit periodic service command. -- Keep widening selected-year world-owner state only when a full owning reader/rebuild family is - grounded strongly enough to avoid one-off leaf guesses. - -## In Progress - -- Widen shellless simulation from explicit service commands toward “advance the runtime clock and - the simulation-owned services advance with it.” - -## Queued - -- Rehost additional periodic finance/service branches that still depend on frozen world restore - fields instead of advanced runtime-owned time state. -- Reduce remaining company/chairman save-native gaps that still block standalone simulation - quality, especially controller-kind closure and any deeper finance/state fields that still rely - on conservative defaults. -- Rehost bounded live economy owner state beyond selector/catalog/override surfaces when a - concrete non-shell-owned seam is grounded. -- Keep tightening shell-owned parity families only when that directly supports later rehosting. - -## Blocked - -- Full shell/dialog ownership remains intentionally out of scope. -- Any candidate slice that requires guessing rather than rehosting owning state or real - reader/setter families stays blocked until a better owner seam is grounded. -- Missing owner seams or dispatch mappings are not by themselves a stop condition when a targeted - static-mapping pass or a higher-layer rehosted trace/evaluator surface can still narrow them - further without guessing. -- The city-connection announcement / linked-transit roster-maintenance branch is still blocked at - the record-body level, not the collection-identity level: the runtime now has a corrected - non-direct tagged region seam, a tagged train header-plus-directory seam, and a tagged - placed-structure header seam, but it does not yet reconstruct the live region or - placed-structure record bodies those service owners need. - -## Recently Done - -- `rrt-runtime` now exposes three higher-layer probe surfaces and matching CLI inspectors: - `runtime inspect-periodic-company-service-trace `, - `runtime inspect-region-service-trace `, and - `runtime inspect-infrastructure-asset-trace `. These reports separate grounded outer - owner inputs, runnable shellless branches, and explicit missing owner seams instead of leaving - the current city-connection / linked-transit frontier as an opaque blocker. -- Those same probes now also sharpen the next queue choice on grounded real saves: the periodic - company outer owner shows annual finance and route-preference override as grounded shellless - branches while city-connection and linked-transit stay blocked on region/infrastructure owner - seams; the region trace keeps the queued kind-`7` notice family on the transient side; and the - infrastructure trace now makes the `0x38a5` consumer-mapping blocker first-class after - disproving any alias to the `0x36b1` placed-structure triplet corpus. -- The infrastructure trace now also carries one small atlas-backed static-analysis layer above that - seam: bridge/tunnel/track-cap name-family counts from the real side-buffer corpus plus concrete - consumer candidates rooted at the `Infrastructure` child attach/rebuild/serializer helpers and - the later route/local-runtime follow-on owners. That means the next `0x38a5` pass can be - targeted static mapping instead of another generic scan. -- The same `0x38a5` probe now also exports payload-envelope summaries directly instead of only flat - name rows: policy/profile tag presence, dominant embedded `0x55f2` and `0x55f3` span lengths, - and sampled row boundaries. That means the next pass can decode the short embedded `0x55f3` - payload lane on top of already grounded row boundaries instead of rediscovering the same - envelopes again. -- That same probe now also exports the grounded short trailing flag-byte pair summary for the - dominant `0x06`-byte rows, while the infrastructure trace carries the matching - `0x0052ebd0/0x0052ec50` helper seam. That means the next pass can aim directly at how those - flags combine with compact-prefix regimes and primary-child restore state instead of treating the - short lane as anonymous payload. -- That same probe now also exports the fixed `0x55f2` six-dword policy samples and the grounded - shared trailing word `0x0101` for all embedded rows, while the infrastructure trace carries the - matching `0x00455870/0x00455930` helper seam. That means the next pass can focus on which of the - two restored dword triplets actually bridge into child-count / primary-child state instead of - rediscovering the fixed `0x55f2` row shape. -- The infrastructure trace now also carries the deeper `0x00530720/0x0052e8b0` bridge, so the next - pass can focus on the outer payload-stream header and compact-prefix regimes instead of revisiting - the fixed `0x55f2` six-dword row. -- That same trace now also ranks those consumers into explicit hypotheses, so the next - infrastructure pass should start with the attach/rebuild strip instead of treating all - candidate owners as equally likely. -- The region trace now also carries the corresponding atlas-backed candidate owner strip above the - unresolved save latches, so the region frontier is now explicitly “missing persisted owner seam - for `[region+0x25e/+0x276/+0x302/+0x316]` and stable region id/class,” not “unknown service - family.” -- That same trace now also ranks those owners into explicit hypotheses, so the next region pass - should start with the pending bonus service owner and peer/linkage strip rather than the queued - modal family. -- Save inspection now splits the shared `0x5209/0x520a/0x520b` 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 family is the larger non-direct `Marker09` - collection with live_id/count `0x96/0x91`; the tagged placed-structure header - (`0x36b1/0x36b2/0x36b3`) remains grounded alongside them. -- That same corrected region seam now also exposes repeated `0x55f1/0x55f2/0x55f3` serialized - record triplets with len-prefixed names plus fixed policy/profile chunk lengths, so the next - city-connection pass can target the real record envelope instead of another blind scan. -- The fixed `0x55f2` row inside each region triplet is now decoded structurally as three leading - `f32` lanes, three reserved `u32` lanes, and a trailing `u16` word, so the next save-region - slice can focus on the larger `0x55f3` payload where the pending/completion/one-shot latches are - most likely to live. -- The larger `0x55f3` payload now also exposes an embedded direct profile collection with grounded - live-id/count headers, fixed `0x22`-byte rows, profile names, and trailing weight scalars, so - the remaining region work is on the unresolved payload fields above that collection rather than - on the profile subcollection itself. -- Grounded real saves now also show that the region-side `0x55f3` payload has zero trailing - padding beyond that embedded profile collection, so the remaining region blocker has shifted - from “find the hidden tail inside this payload” to “find the separate owner seam that backs the - runtime latches the city-connection branch still reads.” -- Save inspection now also exports a generic low-tag unclassified collection scan over plausible - indexed-collection headers, now through a lightweight CLI path that does not require full bundle - inspection and now filters out candidates nested inside already-grounded company/chairman/train/ - region/placed-structure spans. -- That lightweight scan now also narrows the real save frontier to a much smaller stable candidate - set across `p.gms`, `q.gms`, and `Autosave.gms`, with the exact `0x38a5/0x38a6/0x38a7` family - standing out as the strongest current placed-structure dynamic side-buffer candidate. -- The `0x38a5/0x38a6/0x38a7` family now also has a first dedicated parser scaffold in - `rrt-runtime`: its synthetic regression is grounded, its header shape is checked in, and the - parser now expects a compact 6-byte prefix plus separator byte before an embedded - placed-structure-style dual-name row rather than treating the family as anonymous residue. -- That exact `0x38a5/0x38a6/0x38a7` parser is now also wired through a lightweight CLI inspector - and the normal save company/chairman analysis output, and grounded real saves now prove the - same seam directly: - `q.gms` exposes `live_record_count=3865`, prefix `0x0005d368/0x0001/0xff`, and first embedded - names `TrackCapST_Cap.3dp` / `Infrastructure`; `p.gms` exposes the same structure with - `live_record_count=2467`. -- That same direct `0x38a5` probe now also samples multiple embedded name rows with their - preceding compact prefixes, showing that the seam is not a one-off wrapper: grounded `q.gms` - samples include repeated `TunnelSTBrick_*` names under `Infrastructure` with compact leading - dwords like `0x000055f3` and `0xff0000ff`, so the next pass can target the semantics of those - compact prefix patterns instead of hunting the owner seam itself. -- The `0x38a5` probe now also summarizes all embedded compact prefix regimes instead of just the - first few samples: grounded `q.gms` currently exposes seven stable pattern groups across 138 - embedded rows, with the dominant `0xff000000/0x0001/0xff` group carrying 62 bridge-section - rows, the `0xff0000ff/0x0001/0xff` and `0xf3010100/0x0055/0x00` groups concentrating cap-like - rows, and a smaller `0x000055f3/0x0001/0xff` group carrying 17 tunnel-section / cap rows whose - leading dword matches the embedded placed-structure profile tag directly. -- The save-company/chairman analysis path now also compares that grounded `0x38a5` side-buffer - name-pair corpus against the grounded `0x36b1` triplet name-pair corpus directly; on `q.gms` - the overlap is currently zero (`0/138` decoded side-buffer rows and `0/5` unique side-buffer - name pairs match the 56-triplet corpus), which shifts the remaining placed-structure work away - from “prove these are aliases” toward “find how the separate infrastructure-asset owner seam is - consumed by city-connection / linked-transit service.” -- Save inspection now also has a dedicated probe for the atlas-backed region queued-notice node - shape (`payload seed 0x005c87a8`, kind `7`, zero promotion latch, region id, amount, `-1/-1` - tails), plus a matching lightweight CLI inspector. Grounded `q.gms`, `p.gms`, and `Autosave.gms` - all currently return `null`, which is useful negative evidence: the transient region notice - queue is not obviously persisted in these ordinary saves. -- The placed-structure tagged save stream now also exposes repeated `0x55f1/0x55f2/0x55f3` - triplets with dual name stems, a fixed five-`f32` policy row, and a compact `0x5dc1...0x5dc2` - footer carrying one raw `u32` payload lane plus one live `i32` status lane, so the remaining - placed-structure work is semantic closure of those owned fields rather than envelope discovery. -- That compact placed-structure `i32` footer status lane is now partially grounded as owned - semantics too: observed non-farm families stay at `-1`, while farm families use nonnegative - `0..11` buckets that are now exported as farm growth-stage indices instead of opaque raw status - residue. -- Stepped calendar progression now also refreshes save-world owner time fields, including packed - year, packed tuple words, absolute counter, and the derived selected-year gap scalar. -- Automatic year-rollover calendar stepping now invokes periodic-boundary service. -- Save-native world locomotive policy owner state now flows through runtime restore state, - summaries, and keyed world-flag execution for the grounded `All Steam/Diesel/Electric Locos - Avail.` descriptor strip plus the cached available-locomotive rating. -- The selected-year bucket ladder rooted in `0x00433bd0` is now checked in as a static artifact, - and runtime restore state now derives both the selected-year bucket scalar and the - `[world+0x0bde]` economic-tuning mirror from owner-family inputs instead of preserving stale - load-time residue. -- That same selected-year 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 checked-in `0x00433bd0` artifact instead of - preserving stale save-time residue. -- The 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 beside the city-connection and linked-transit finance gates. -- That same side-latch trio now also has a runtime-owned service-state map and summary surface, - so later periodic company-service work can stop reading those lanes directly from imported - market/cache residue. -- The periodic-boundary owner now also clears the transient preferred-locomotive side latch every - cycle and reseeds the finance latches from market state where present, while preserving - side-latch-only company context when no market projection exists. -- The outer periodic-company seam now also has a first-class runtime reader: - selected-company summaries and finance readers can resolve the base `[world+0x4c74]` - route-preference byte, the effective electric-only override fed by `0x0d17`, and the matching - `1.4x` versus `1.8x` route-quality multiplier through owned periodic-service state instead of - leaving that bridge in atlas notes. -- That same periodic-company seam now also owns a first-class route-preference apply/restore - mutation lane: runtime service state tracks the active and last electric override, beginning the - override rewrites `[world+0x4c74]` to the effective route preference for the selected company - service pass, and ending the override restores the base world byte instead of leaving the seam as - a pure reader bridge. -- The same route-preference mutation seam now also carries explicit apply/restore service counters - through runtime service state and summaries, so later periodic-company branches can assert that - override activity happened even before the missing city-connection / linked-transit service - owners are fully rehosted. -- Company cash, confiscation, and major governance effects now write through owner state instead of - drifting from market/cache readers. -- Company credit rating, prime rate, book value per share, investor confidence, and management - attitude now refresh from grounded owner-state readers. -- Annual finance service persists structured news events and grounded debt/share flow totals. +- [Archive snapshot](rehost-queue/archive-2026-04-19.md) +- [Progress history](history/progress-history.md) diff --git a/docs/rehost-queue/README.md b/docs/rehost-queue/README.md new file mode 100644 index 0000000..7f1112e --- /dev/null +++ b/docs/rehost-queue/README.md @@ -0,0 +1,6 @@ +# Rehost Queue Archive + +This directory preserves older queue snapshots and long-form implementation notes that are still +useful as evidence, but should not stay in the short active queue file. + +- `archive-2026-04-19.md`: preserved detailed queue snapshot from the pre-index cleanup. diff --git a/docs/rehost-queue/archive-2026-04-19.md b/docs/rehost-queue/archive-2026-04-19.md new file mode 100644 index 0000000..bf8d393 --- /dev/null +++ b/docs/rehost-queue/archive-2026-04-19.md @@ -0,0 +1,2120 @@ +# Rehost Queue + +Working rule: + +- Do not stop after commits. +- After each commit, check this queue and continue. +- Only stop if the queue is empty, the remaining work cannot be advanced by any further non-hook + work without guessing, or you need approval. +- Before any final response, state which stop condition is true. If none is true, continue. +- `final` responses are stop-only. +- Do not send placeholder or status-only `final` responses such as `Continuing.` or `Working on + it.`. +- If no stop condition is true, keep working and use `commentary` updates only. + +## Next + +- Make the current Tier-2 head explicit: the `00`-name mystery is closed, and the next source + pass is the stock style-family loader strip above the later banked clone pass. Direct recovery + now shows: + - `0x00416ce0` is the stock `gpdBuildingTypeDB` `%1*.bty` load callback and already rewrites + bare `port` / `warehouse` to `Port00` / `Warehouse00` + - `0x004196c0` is the stock import owner that walks `%1*.bty` matches, routes them through + `0x00414490`, calls the same `0x00416ce0` load callback, and only then tails into + `0x00419230`; the current recovered source names are + `StationSml`, `StationMed`, `StationLrg`, `ServiceTower`, `Maintenance`, `ClpBrd`, `Kyoto`, + `Persian`, `SoWest`, `Tudor`, and `Victorian` + - `0x00419230` is then the later two-bank, twelve-ordinal clone-and-rename pass that stamps + `Warehouse%02d` / `Port%02d` + - so the next honest question is which source rows out of that stock style-family strip first + satisfy each later `(bank, ordinal)` pair before `0x00419230` clones them + - that means the queue should keep tracing `0x004196c0 -> 0x00414490 -> 0x00416ce0 -> 0x00419230` + and its + callsites, not return to the older generic hidden-writer hypothesis + +- Treat the periodic-company trace as the main shellless simulation frontier now that the + infrastructure footer-bit residue is layout/presentation-owned. The checked-in + `runtime inspect-periodic-company-service-trace ` report now exposes concrete branch + owners instead of generic blockers: + - `industry_acquisition_side_branch` carries + `0x004019e0 -> 0x004014b0` with the city-connection sibling `0x00406050` + - `city_connection_announcement` carries + `0x004019e0 -> 0x00406050` plus peer helpers + `0x00420030 / 0x00420280 / 0x0047efe0` + - `linked_transit_roster_maintenance` carries + `0x004019e0 -> 0x00409720 -> 0x004093d0 / 0x00407bd0 -> 0x00408f70 -> 0x00409950` + - the linked-transit timing seam is now grounded save-side too: + `[company+0x0d3e]` is the shorter peer-cache refresh counter, + `[company+0x0d3a]` is the heavier autoroute site-score refresh counter, and + the route-anchor tuple `[company+0x0d35] / [company+0x7664/+0x7668/+0x766c]` remains save-native + - the train-side follow-ons are bounded too: + `0x00408280 / 0x00408380` are the ranked-site chooser and staged autoroute-entry builder above + the rebuilt site caches, and `0x00409770 / 0x00409830 / 0x00409950` are the append/add/balance + strip above that + - the chooser-local cache words are bounded too: + `[site+0x5c1]` is a live occupancy/count lane reset by `0x00481910` and adjusted by + `0x004819b0`, with counts sourced from current-site-id resolver `0x004a9340`; meanwhile + `[site+0x5c5]` is a world-counter age lane stamped at `0x004aee2b` + - the per-company cache root is bounded too: + `[site+0x5bd]` is allocated by `0x00407780` as a 0x20-entry table of 0x1a-byte per-company + cache cells and freed by `0x004077e0` + - the per-company cache-cell layout is bounded too: + bytes `+0x00/+0x01` gate participation, dwords `+0x02/+0x06/+0x0a` hold peer count, peer + pointer, and peer-cache refresh stamp, and floats `+0x0e/+0x12/+0x16` are the + weighted/raw/final score lanes + - the persisted-vs-live split is tighter now too: + the minimal save-backed identity set is `[site+0x276]`, `[site+0x04]`, `[site+0x2a4]`, + `[site+0x2a8]`, `[peer+0x04/+0x08]`, `[company+0x0d35/+0x0d56/+0x7664/+0x7668/+0x766c]`, + and world calendar lanes `[world+0x15/+0x0d]`, while the actual cache contents at + `[site+0x5bd]`, `[site+0x5c1/+0x5c5]`, and `[site+0x0e/+0x12/+0x16]` are live rebuilt scratch + lanes under `0x004093d0 / 0x00407bd0 / 0x00481910 / 0x004819b0 / 0x004aee2b` + - the upstream live-table owners are tighter now too: + active-company refresh owner `0x00429c10` walks the live company roster and re-enters + `0x004093d0`; candidate table root `0x0062ba8c` is world-load owned by + `0x0041f4e0 -> 0x0041ede0 -> 0x0041e970`; and route-entry tracker compatibility / chooser + helpers `0x004a6360 / 0x004a6630` already sit under owner-notify refresh `0x00494fb0` + - the placed-structure replay strip is tighter now too: + `0x00444690 -> 0x004133b0 -> 0x0040ee10 -> 0x0040edf6 -> 0x00480710` already republishes + anchor-side linked-peer ids, route-entry anchors, and world-cell owner chains, and later + runtime path `0x004160aa -> 0x0040ee10` re-enters the same family outside bring-up + - the subtype-`4` follow-on is tighter now too: + `0x0040eba0` already republishes `[site+0x2a4]` through `0x004814c0 / 0x00481480` plus + world-cell chain helpers `0x0042c9f0 / 0x0042c9a0`, so self-id replay is no longer the open + linked-transit blocker + - the owner-company branch is tighter now too: + direct inspection of `0x0040ea96..0x0040eb65` shows that it consumes `[site+0x276]` and + branches through owner/company helpers, but does not itself rehydrate `[site+0x276]` + - the nearby selected-company and live-site helper strip is tighter now too: + `0x004337a0` is the raw selected-company getter over `[world+0x21]`, and the adjacent + world-side helpers `0x00452d80 / 0x00452db0 / 0x00452fa0` are live selected-site or active + service-state setters/dispatchers over `[world+0x217d/+0x2181]` gated by mode byte + `[world+0x2175]`, not restore-time republishers for `[site+0x276]` + - the base placed-structure load callback is narrower now too: + local `.rdata` at `0x005cb4c0` shows the shared base table, not the `0x005c8c50` + specialization table, owns the `0x0045c150 / 0x0045b560 / 0x00455870 / 0x00455930` + load-save quartet; direct disassembly of `0x0045c150 -> 0x00455fc0` shows that callback only + reloads the generic `0x55f1/0x55f2/0x55f3` triplet/scalar bands and then re-enters the base + triplet/scalar slots `0x00455870 / 0x00455930`, so it does not repopulate `[site+0x276]` + - that makes the next linked-transit question narrower: + identify which earlier restore or service owner feeds `[site+0x276]` and the live linked-peer + rows before replay continuation `0x0040e360..0x0040edf6`, beyond the already-grounded + `0x00480710` anchor-side refresh, before `0x004093d0 / 0x00407bd0 / 0x004a6630`, because the + candidate table, route-entry-tracker owners, replay-strip framing, subtype-`4` self-id replay, + bounded train-side strip + `0x00409770 / 0x00409830 / 0x00409950`, and cache-cell semantics are no longer the blocker +- Make the next static/rehost slice the near-city industry acquisition owner seam under + `0x004014b0`, not another generic infrastructure pass. The concrete questions are: + - which minimum persisted peer-site fields on the already-grounded `0x006cec20` placed-structure + collection feed near-city unowned-industry candidates + - which placed-structure, city-or-region, and company linkage survives save/load strongly enough + to drive the proximity scan + - whether the acquisition branch can be rehosted as a shellless sibling beside the already + grounded annual-finance helper + - the save-side `0x36b1/0x36b2/0x36b3` triplet seam is now also loaded into the checked-in + save-slice model as first-class `placed_structure_collection` context, carrying stem pairs plus + grounded footer/policy status lanes instead of remaining inspection-only evidence + - the separate `0x38a5/0x38a6/0x38a7` side-buffer seam is now also loaded into the save-slice + model as first-class `placed_structure_dynamic_side_buffer_summary` context, carrying the + grounded owner-shared dword, compact-prefix summaries, name-pair summaries, and overlap counts + against the triplet corpus instead of remaining trace-only evidence + - the save-side `0x55f1/0x55f2/0x55f3` region triplet seam is now also loaded into the + save-slice model as first-class `region_collection` context, carrying region names, the + grounded policy lanes, reserved-policy dwords, and embedded profile rows instead of leaving + region triplets inspection-only + - the save-side fixed-row run candidate family is now also the next save-slice seam to keep + first-class once grounded, because it is the closest save-native candidate family to the + still-missing cached tri-lane inputs behind `[site+0x310/+0x338/+0x360]` + - cross-save fixed-row comparison is tighter now too: + `runtime compare-region-fixed-row-runs p.gms q.gms` shows shared shape-family matches even + though the best raw `rows_offset` drifts between saves, so the tri-lane-adjacent row family + should be treated as a stable shape-family seam rather than a fixed-offset seam + - the periodic-company trace now carries that save-native seam directly too: + it exposes the current top tri-lane-adjacent fixed-row shape-family candidates with row-count, + stride, rows-offset, and probable-density-lane hints, so the tri-lane frontier is now + structured as “save shape-family candidates present, fixed offset ruled down” instead of only + a prose note + - the `0x5dc1/0x5dc2` serializer bundle is tighter now too: + atlas-backed recovery bounds `0x0040c980 -> 0x0045b560` as emitting the derived payload over + `[site+0x23e/+0x242/+0x246/+0x24e/+0x252]`, so the remaining restore-owner question should + treat that persisted selector/child/runtime bundle as one seam rather than only + `[site+0x23e/+0x242]` + - the loader-side counterpart narrows the minimum shellless identity subset too: + `0x0045c150` restores `[owner+0x23e/+0x242]`, clears the transient roots, and then hands off + to `0x0045c310 / 0x0045b5f0 / 0x0045b6f0` to rebuild the primary child handle plus the larger + ambient/animation/light/random-sound family, so current shellless planning can keep the + minimum persisted subset at cached ids `[site+0x3cc/+0x3d0]`, restored name-pair + `[owner+0x23e/+0x242]`, and the post-secondary discriminator byte while treating + `[owner+0x246/+0x24e/+0x252]` as part of the broader saved bundle that still flows through the + rebuild side + - the periodic-company trace now surfaces the strongest non-transport owner-company candidate + family directly too: + ordinary loaded runtime-effect lane + `0x00444d92 -> 0x00432f40(kind 8) -> 0x004323a0 -> 0x00431b20` above the non-direct + `0x4e99/0x4e9a/0x4e9b` bundle, with the remaining gap narrowed to the control-lane mapping + from loaded rows into trigger kind `8` and then into the placed-structure mutation opcodes +- Direct disassembly now narrows that acquisition strip further: + - `0x004014b0` scans the live placed-structure collection at `0x0062b26c` + - `0x0041f6e0 -> 0x0042b2d0` is the center-cell token gate over the current region + - `0x0047de00 -> 0x0040c990` is the linked-region status branch reused from the city-connection + helper strip + - `0x004801a0` is the route-anchor reachability gate for one candidate site through + `0x00401860 -> 0x0048e3c0` + - the company-side half of that gate is now explicit too: `0x00401860` validates or rebuilds the + cached linked-transit route-anchor entry id `[company+0x0d35]` from the live route-entry + collection using fallback count lanes `[company+0x7664/+0x7668/+0x766c]` + - those four company lanes are now threaded into save-native company market state, so the + route-anchor side of the acquisition gate is no longer just a trace-only blocker + - `0x0040d360` is the subtype-`4` predicate over the current placed-structure subject's + candidate byte `[candidate+0x32]` + - `0x0040d540` scores site/company proximity with pending-bonus context + - `0x0040cac0` samples the cached site tri-lane at `[site+0x310/+0x338/+0x360]` + - `0x00405920` walks same-company linked site peers above the live placed-structure / peer-site + collection seam + - `0x00420030 / 0x00420280` is the boolean/selector peer-site pair over `0x006cec20`, combining + `0x0042b2d0`, the optional company filter through `0x0047efe0`, the station-or-transit gate + `0x0047fd50`, and the status branch `0x0047de00 -> 0x0040c990` + - `0x0047efe0` and `0x0047fd50` both consume `[site+0x04]` as the live backing-record selector + - `0x00480210` writes linked-peer row `[peer+0x04]` from the anchor-site id argument + - `0x0040f6d0 -> 0x00481390` writes the anchor-site linked peer id back into `[site+0x2a8]` + - `0x0047dda0` consumes `[peer+0x08]` as the linked route-entry anchor id + - `0x0041f7e0 / 0x0041f810 / 0x0041f850` already ground `[site+0x2a4]` as the record's own + placed-structure id lane beneath the peer-chain helpers + - `0x0040d210` is the owner-side placed-structure resolver from `[site+0x276]` through + `0x0062be10` + - `0x00480710 -> 0x0048abc0 / 0x00493cf0` is the linked-site refresh and route-entry rebind or + synthesis strip above that anchor lane + - the same `0x00480710` replay strip now also republishes the concrete world-cell owner chains: + `0x0042bbf0 / 0x0042bbb0` remove or prepend the current site in the owner chain rooted at + `[cell+0xd4]`, while `0x0042c9f0 / 0x0042c9a0` remove or prepend it in the linked-site chain + rooted at `[cell+0xd6]` + - late world bring-up `0x00444690` is the current caller of + `0x004133b0 placed_structure_collection_refresh_local_runtime_records_and_position_scalars` + - `0x004133b0` drains queued site ids through `0x0040e450` and then sweeps all live sites + through `0x0040ee10` + - `0x0040ee10` reaches `0x0040edf6 -> 0x00480710` for linked-peer refresh and then the later + `0x0040e360` follow-on + - `0x004160aa` is a separate non-bring-up runtime caller of `0x0040ee10` + - the direct `0x36b1` per-record callbacks serialize base scalar triplets + `[this+0x206/+0x20a/+0x20e]` plus the subordinate payload callback strip, and the + `0x4a9d/0x4a3a/0x4a3b` side-buffer owner only persists route-entry lists, three byte arrays, + five proximity buckets, and the sampled-cell list + - `0x004269b0` consumes the chosen site's own placed-structure id lane `[site+0x2a4]` +- That leaves the acquisition blocker set tighter than before: + - peer-site and linked-site replay seams are grounded enough for planning + - the live owner-company meaning of `[site+0x276]` is already grounded through `0x0047efe0`, + with the direct owner-side resolver bounded at `0x0040d210` + - `[site+0x2a4]` is already grounded as the record's own placed-structure id lane through the + peer-chain helpers `0x0041f7e0 / 0x0041f810 / 0x0041f850`, and constructor-side `0x00480210` + already seeds that lane for new linked-site rows + - direct disassembly now also shows `0x004269b0` resolving one chosen site id through live + placed-structure collection `0x0062b26c` before mutating `[site+0x276]`, so the + `[site+0x2a4]` self-id lane is reconstructible from collection identity rather than needing a + separate serializer-owned projection + - the subtype byte consumed as `[candidate+0x32] == 4` is already bounded under the + aux-candidate load/stem-policy chain + `0x004131f0 -> 0x00412fb0 -> 0x004120b0 -> 0x00412ab0` + - remaining non-hook gaps are the save or replay projection of `[site+0x276]` and the cached + tri-lane `[site+0x310/+0x338/+0x360]` + - the checked-in periodic-company trace now exposes those gaps as structured statuses instead of + only prose: + - site owner-company lane = `live_meaning_grounded_projection_missing` + - site self-id lane = `live_meaning_grounded_reconstructible_from_collection_identity` + - site cached tri-lane = `delta_reader_grounded_projection_missing` + - candidate subtype lane = `cached_candidate_id_bridge_grounded_via_stream_load` + - backing-record selector bridge = `stream_load_callback_grounded_via_0x40ce60` + - the same trace now also carries three explicit projection hypotheses for the next pass: + - `site_owner_replay_from_post_load_refresh_self_id_reconstructible` + - `site_cached_tri_lane_payload_or_restore_owner` + - `cached_source_candidate_id_to_subtype_projection` + - the first of those is now bounded more tightly: + `0x004133b0 -> 0x0040e450 / 0x0040ee10` rebuilds cloned local-runtime records and local + position/scalar triplets, but direct constructor and caller recovery now shows the + owner-company lane also living under the create-side allocator/finalize family + `0x004134d0 -> 0x0040f6d0 -> 0x0040ef10`, plus data-driven loader callers + `0x0046f073 / 0x004707ff -> 0x0040ef10` + - the checked-in replay strip is narrower than before now: + direct local inspection now splits it more precisely: + `0x0040ee10` itself only reads cached source lane `[site+0x3cc]` in the checked range, and + the bounded `0x00480710` neighborhood is working from `[site+0x04]`, `[site+0x08]`, and + `[site+0x3cc]`; but the broader immediate continuation `0x0040e360..0x0040edf6` still + consumes `[site+0x2a8]`, `[site+0x2a4]`, and `[site+0x276]` around + `0x0040d230 / 0x0040d1f0 / 0x00480710 / 0x00426b10 / 0x00455860`, so the replay family is + narrowed rather than ruled out for owner-company rehydration; in the checked range those + `[site+0x276]` uses are still reads/queries rather than a direct rehydrating store + - the create-side owner family is grounded too now: + `0x004134d0` allocates a new row through `0x00518900`, `0x0040f6d0` seeds `[site+0x2a4]`, + copied name bytes, `[site+0x276]`, `[site+0x3d4/+0x3d5]`, and cleared local caches, and the + shared finalize helper `0x0040ef10` has both create-side callers `0x00403ef3 / 0x00404489` + and data-driven loader callers `0x0046f073 / 0x004707ff` + - one persisted tuple path is grounded too now: + the data-driven loader callers `0x0046f073 / 0x004707ff` push tuple fields + `[+0x00/+0x04/+0x0c]` into `0x0040ef10`, and inside that helper arg3 becomes `ebx` and then + `[site+0x276]` at `0x0040f5d4` + - that tuple path is classified further now too: + the checked-in atlas ties `0x004707ff` to multiplayer transport selector-`0x13` body + `0x004706b0`, which attempts the placed-structure apply path through + `0x004197e0 / 0x004134d0 / 0x0040eba0 / 0x0052eb90 / 0x0040ef10` + - the neighboring batch builder is classified too: + `0x00472b40` is the multiplayer transport selector-`0x72` counted live-world apply path, and + its inner builders `0x00472bef / 0x00472d03` reach `0x004134d0` from counted transport + records rather than ordinary save-load restore + - one surviving non-transport `0x004134d0` caller is bounded away from persisted restore too: + `0x00422bb4` pushes one live `0x0062b2fc` record plus local args and literal flags `1/0` + into `0x004134d0`, then returns the created row id through an out-param instead of feeding + the tuple-backed finalize path + - the remaining `0x00508fd1 / 0x005098eb` family is bounded away too: + it caches the created site id in `[this+0x7c]`, re-enters `0x0040eba0` with immediate coords, + and later calls `0x0040ef10` with a hard zero third arg, so it reads as another live + controller path rather than the missing persisted owner seam + - the adjacent `0x00473c20` family is bounded away too: + it drains queued site ids and coordinate pairs from scratch band `0x006ce808..0x006ce988`, + re-enters `0x0040eba0` at `0x00473c98`, and clears each queued id slot, so it is a local + post-create refresh path rather than a persisted replay owner + - the remaining direct `[site+0x276]` store census is bounded away too: + `0x0042128d` is broad zero-init in the `0x00421430` constructor neighborhood, + `0x00422305` computes a live score/category lane before publishing event `0x7`, + `0x004269c9/0x00426a2a` are acquisition commit/clear helpers, and + `0x004282a9/0x004300d6` are bulk owner-transfer writes + - the paired tagged triplet serializer is bounded away too: + `0x00413440` is the save-side `0x36b1/0x36b2/0x36b3` serializer, dispatches each live record + through vtable slot `+0x44`, and keeps that seam on the already-grounded triplet payload + rather than the missing `[site+0x276]` replay owner + - the ordinary bring-up strip is narrower too: + `0x00444690 -> 0x004133b0` is still the checked ordinary restore-side replay owner above live + placed structures, but it only drains queued local-runtime ids through `0x0040e450` and then + sweeps live rows through `0x0040ee10`; after that, bring-up proceeds into later route-entry, + grid, and tagged refresh owners rather than re-entering the constructor/finalize family + `0x004134d0 / 0x0040f6d0 / 0x0040ef10` + - the ordinary restore staging order is explicit now too: + world bring-up calls the tagged `0x36b1/0x36b2/0x36b3` stream-load owner `0x00413280` at + `0x00444467`, refreshes the placed-structure dynamic side buffers through `0x00481210` at + `0x004444d8`, and only later enters the queued local-runtime replay owner + `0x00444690 -> 0x004133b0 -> 0x0040ee10` + - the broader load-side stream owner is separate too: + `0x00413280` is the actual tagged `0x36b1/0x36b2/0x36b3` stream-load owner, dispatching + per-entry vtable slot `+0x40`; current local recovery still only grounds that seam through the + cached-source bridge `0x0040ce60 -> 0x0040cd70 / 0x0045c150`, not through a direct + `[site+0x276]` republisher + - the grouped-opcode family is narrower rather than fully ruled down: + `0x00431b20` is still only reached through scenario runtime-effect service + `0x004323a0 -> 0x00432f40` via direct call `0x00432317`, but that service loop is itself + called from world bring-up at `0x00444d92` with trigger kind `8` under shell-profile latch + `[0x006cec7c+0x97]`; so `0x0061039c` currently reads as a startup-time live runtime-effect + application lane rather than an ordinary tagged restore owner + - that `kind 8` branch is ordinary in one important way now too: + restore-side loader `0x00433130` repopulates the live event collection `0x0062be18` from packed + chunk family `0x4e21/0x4e22`, and the event-detail editor strip + `0x004d90ba..0x004d91ed` writes trigger field `[event+0x7ef]` across the full `0x00..0x0a` + range through controls `0x4e98..0x4ea2`, including kind `8` at `0x004d91b3`; so the remaining + startup compact-effect question is no longer whether kind `8` is a special synthetic class, but + which loaded kind-`8` rows in `0x0062be18` can actually reach the placed-structure mutation + opcode families under `0x00431b20` + - the event-detail editor family tightens that one step further too: + selected-event control root `0x4e84` and refresh strip `0x004db02a / 0x004db1b8..0x004db309` + mirror current trigger field `[event+0x7ef]` back into those same `0x4e98..0x4ea2` controls, + while editor-side builder `0x004db9e5..0x004db9f1` allocates an ordinary runtime-effect row + into `0x0062be18` through `0x00432ea0`; so the remaining startup compact-effect question is no + longer whether kind `8` lives on a separate editor/build class either, but which loaded + kind-`8` rows actually carry the mutation-capable compact payloads + - bundle-side inspection now grounds the startup collection itself: + sampled maps such as `War Effort.gmp`, `British Isles.gmp`, `Germany.gmp`, and + `Texas Tea.gmp` expose non-direct `0x4e99/0x4e9a/0x4e9b` runtime-event collections, and the + compact `0x526f/0x4eb8/0x4eb9` row family is now decoded into actual condition/grouped row + summaries rather than opaque slices + - the rehosted collection summary now makes the remaining control-lane gap explicit too: + it reports `records_with_trigger_kind`, `records_missing_trigger_kind`, + `nondirect_compact_record_count`, `nondirect_compact_records_missing_trigger_kind`, and + `control_lane_notes`; real `War Effort.gmp` output currently shows `24/24` compact non-direct + rows still missing decoded trigger kind, which narrows the next owner question to the + non-row-body control lane rather than the compact row framing itself + - the same per-map summary now surfaces the add-building subset directly too: + it reports `add_building_dispatch_strip_record_indexes`, + `add_building_dispatch_strip_descriptor_labels`, and the matching + `add_building_dispatch_strip_records_with_trigger_kind` / + `add_building_dispatch_strip_records_missing_trigger_kind` counts, so inspected maps can now + show whether `Add Building` rows are present and still null-trigger without a separate cluster + pass + - that add-building subset is structured one layer deeper now too: + the per-map summary and the compact-dispatch cluster/counts reports both surface + add-building signature families, condition-tuple families, and combined + signature/condition clusters, so the remaining trigger-kind/source question can compare exact + compact subfamilies instead of only raw descriptor hits + - the first concrete add-building cluster split is now visible too: + targeted map inspections show `Texas Tea.gmp` carrying a one-row `Port01` cluster on + signature family `nondirect-ge1e-h0001-0007-0000-6d00-0200-p0000-0000-0000-ffff` with + condition family `[7:0]`, while `Alternate USA.gmp` carries repeated three-row + `FarmGrain`/`Logging Camp` clusters on `nondirect-ge34-h0002-0007-0004-73|75|7b|85...` with + condition family `[7:4,42:0]`; that narrows the remaining trigger-kind/source question to a + small set of compact subfamilies rather than a single undifferentiated add-building carrier + - the first exclusivity pass is positive too: + the widened cluster-counts report now surfaces, for each add-building + signature/condition cluster, the descriptor keys that share it plus the + non-add-building subset. Current targeted checks on `Texas Tea.gmp` and `Alternate USA.gmp` + show those observed add-building clusters have empty non-add-building companions, which biases + the next source/trigger-kind pass toward a distinct add-building compact subfamily rather than + a generic dispatch carrier reused by variable-band rows + - the descriptor-independent row-shape split is explicit now too: + the same cluster-counts surface reports normalized add-building row shapes. + Current targeted checks show `Texas Tea.gmp` on the one-row shape `[0:8:0]`, while + `Alternate USA.gmp` sits on the repeated three-row shape + `[0:8:-25,0:8:0,0:8:0]`; that gives the next pass a simpler shape-level discriminator even + when descriptor labels differ + - the shipped-map corpus is checked in now too: + `artifacts/exports/rt3-1.05/add-building-compact-dispatch-corpus.json` records the full + six-map add-building carrier set in bundled RT3 1.05 maps: + `Alternate USA`, `Chicago to New York`, `Louisiana`, `Pacific Coastal`, + `Rhodes Unfinished`, and `Texas Tea`. Across that corpus the normalized row-shape totals are + only four families: + `[0:8:-25,0:8:0,0:8:0] = 4`, + `[0:8:0,0:8:0,0:8:0,0:8:0] = 1`, + `[0:8:0,0:8:0,0:8:0] = 1`, + `[0:8:0] = 4`. + The accompanying signature/condition cluster totals now show the add-building carrier set is + broader than the first two sampled maps but still small enough to treat as a bounded compact + subfamily frontier rather than a diffuse generic event carrier. + - the same probe now narrows the candidate runtime-effect set too: + it reports which decoded records already carry grouped opcodes in the grounded + `0x00431b20` dispatch strip; real `War Effort.gmp` currently narrows that to record indexes + `[1, 9, 12, 13, 14, 15, 16, 17, 22]`, all through opcode `4`, with descriptor labels + `Game Variable 1`, `Company Variable 1..4`, and `Player Variable 1`, so the next pass can + focus on that smaller subset instead of the full 24-row compact collection while avoiding the + earlier overclaim that opcode `4` alone already proves a placed-structure mutation row + - the sampled-map framing split is narrower now too: + targeted real-map inspections of `Texas Tea.gmp`, `British Isles.gmp`, and `Germany.gmp` + still show their entire event-runtime collections as nondirect compact + `0x4e99/0x4e9a/0x4e9b` rows with `records_with_trigger_kind = 0`, even when grouped rows + already reach the grounded `0x00431b20` dispatch strip. That means the direct full-record + `0x4e21/0x4e22` parser path is not currently bridging `[event+0x7ef]` for the ordinary + mutation-capable rows in those sampled maps. + - the widened compact-cluster probe now makes that same gap first-class too: + `runtime inspect-compact-event-dispatch-cluster ` now reports all dispatch-strip + descriptor occurrences, their `trigger_kind`, payload family, and the aggregated + `dispatch_descriptor_occurrence_counts` / `dispatch_descriptor_map_counts` summaries instead of + only unknown descriptor ids. On `Texas Tea.gmp`, the widened report now shows + `548 Add Building Port01`, `8 Economic Status`, and `43 Company Variable 1` all arriving on + `real_packed_nondirect_compact_v1` rows with `trigger_kind = null`, while the count summary + reports `Company Variable 1 = 4` occurrences and the other two once each, so the next pass can + work from a checked scalable probe rather than ad hoc `inspect-smp` scrapes. + - the scalable summary sibling is grounded too: + `runtime inspect-compact-event-dispatch-cluster-counts ` reuses the same analysis + but emits only the corpus-level count fields, so the next broader map-install pass no longer + needs to wade through every occurrence payload just to compare descriptor or trigger-kind + coverage; the same counts output now also carries an explicit add-building subset so the + acquisition-side branch can compare `Add Building` records and their still-missing trigger-kind + coverage without grepping the full occurrence dump. + - the installed-map totals are grounded now too: + current `rt3_105/maps` corpus gives `41` bundled maps, `38` maps with dispatch-strip rows, and + `318` dispatch-strip records total, all on `real_packed_nondirect_compact_v1` with + `dispatch_strip_records_with_trigger_kind = 0`; the add-building subset inside that corpus is + only `10` grouped occurrences across `7` recovered descriptor keys + (`Barracks`, `Bauxite Mine`, `FarmGrain`, `Furniture Factory`, `Logging Camp`, `Port01`, + `Warehouse05`), again all still missing trigger kind + - the per-record trigger gate is explicit now too: + direct disassembly of `0x004323a0` shows the service returns before dispatch unless + one-shot latch `[event+0x81f]` is clear, mode byte `[event+0x7ef]` matches the selected + trigger-kind argument from `0x00432f40`, and the compact chain root `[event+0x00]` is + nonzero. The kind-`8` side path at `0x00432ca1..0x00432cb0` only calls `0x00438710` on + already-live records carrying `[event+0x7ef] == 8`. So the remaining ordinary bundle question + is no longer “does the service loop itself bypass the per-record trigger byte?”; it is which + later owner, if any, materializes or retags `[event+0x7ef]` for the nondirect compact rows. + - the post-load retagger is narrower than that missing owner too: + direct disassembly of `0x00442c30` (called from `0x00443a50` at `0x00444b50`) shows a + hardcoded scenario-name patch table over already-live records in `0x0062be18/0x0062bae0`. + The checked cases mostly tweak modifier bytes `[event+0x7f9/+0x7fa]` or nested payload scalars + on records that already carry concrete kinds such as `7` (`Open Aus`, `The American`), `6` + (`Test connections`), `5` (`Win - Gold`), and `1` (`Win - Silver` / `Win - Bronze`). So the + remaining trigger-kind frontier is no longer “maybe the name sweep bulk-materializes null + - the startup kind-`8` owner strip has a dedicated checked artifact now too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-startup-subgraph.{dot,md}` seeds + `0x00444d92`, `0x00432f40`, `0x004323a0`, `0x00431b20`, and `0x00433130` at depth `5`, + giving the current ordinary bring-up path a reusable static-analysis surface instead of only + scattered queue prose + - that artifact narrows the immediate owner one step higher too: + the ordinary restore/service pair `0x00433130` and `0x00432f40` now sit directly under + `world_entry_transition_and_runtime_bringup` `0x00443a50` in the checked subgraph, alongside + `0x004133b0` placed-structure refresh, `0x00421510` region refresh, and the + `0x00433bd0/0x00434130/0x004354a0/0x00435603` year-band/state refresh strip. That means the + next static recovery pass can focus on `0x00443a50` ordering and preconditions instead of + treating `0x00444d92` as a floating standalone caller. + - there is now a smaller checked artifact for that owner too: + `artifacts/exports/rt3-1.06/world-entry-bringup-refresh-subgraph.{dot,md}` seeds only + `0x00443a50` at depth `3`, giving a 343-node / 989-edge bringup-refresh surface that is + easier to work from than the broader kind-`8` startup export when the immediate question is + just ordering and neighboring refresh owners. + - the saved-runtime-state loader is boxed in one step lower now too: + `artifacts/exports/rt3-1.06/world-load-saved-runtime-state-subgraph.{dot,md}` seeds + `0x00446d40` at depth `3` and shows the loader directly reaches + `0x00433130 scenario_event_collection_refresh_runtime_records_from_packed_state`, but not + `0x00432f40`. That shifts the remaining ordinary kind-`8` question upward: the unresolved + service-vs-restore ordering is now specifically inside `0x00443a50`, not inside the loader + itself. + - the bringup owner note now supplies the high-level ordering too: + current `function-map.csv` notes for `world_entry_transition_and_runtime_bringup` + `0x00443a50` already state that the tagged load phase reloads event runtime records through + `0x00433130`, while the one-shot kind-`8` runtime-effect service through `0x00432f40` only + runs much later in the final reactivation tail, after the candidate/region/year-band refresh + strip and before shell-profile latch `[0x006cec7c+0x97]` is cleared. That means the remaining + unknown is no longer restore-vs-service ordering in the abstract; it is which late bringup + branch or retagger between those two points materializes the live kind-`8` records that carry + the mutation-capable compact payloads. + - the late retagger remains a prose-first seam: + a direct `0x00442c30` subgraph export currently collapses to the seed node only, because the + grounded function-map note carries the useful scenario-title and collection-mutation detail but + not many mapped-address backrefs. So the post-load scenario-fixup branch should currently be + treated as a manual-note recovery seam rather than one the generic subgraph exporter can answer + by itself. + - the relevant prose is checked in separately now too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-late-bringup-note.md` extracts the current + late-bringup facts for `0x00446d40`, `0x00443a50`, `0x00442c30`, and the explicit + `SP - GOLD` / `Labor` trigger-kind rewrites, so the next pass can work from one bounded note + instead of stitching the same ordering back together from the queue plus function-map prose. + - the shipped add-building carrier corpus no longer supports the older filename-mismatch bias: + the checked report + `artifacts/exports/rt3-1.05/add-building-map-title-hints.json` + now scans the six bundled carrier maps in + `artifacts/exports/rt3-1.05/add-building-compact-dispatch-corpus.json` + against the grounded `0x00442c30` title set (`Go West!`, `Germany`, `France`, + `State of Germany`, `New Beginnings`, `Dutchlantis`, `Britain`, `New Zealand`, + `South East Australia`, `Tex-Mex`, `Germantown`, `The American`, `Central Pacific`, + `Orient Express`). + - the new title-hint probe narrows that evidence precisely: + five of the six shipped carrier maps now show at least one grounded retagger-title hit, + but only one map currently shows an adjacent embedded `.gmp` reference plus grounded title and + only one shows a same-stem pair. `Louisiana.gmp` carries + `Dutchlantis.gmp` / `Dutchlantis` at offset `0x73d0` with zero byte distance, while the other + current carrier-map hits stay weaker (`Alternate USA.gmp` late `Germany` / `France` / + `Britain`, `Chicago to New York.gmp` late `Germany` / `France`, + `Pacific Coastal.gmp` later `Central Pacific`, `Texas Tea.gmp` later `Germany`, + `Rhodes Unfinished.gmp` no current hit). + - that keeps the title-fixup branch alive but no longer as a broad filename-level explanation: + the evidence now supports a narrow “one strong `Louisiana -> Dutchlantis` overlap plus several + weaker prose-only or late-string overlaps” reading rather than a clean one-to-one mapping from + the shipped add-building carrier filenames to the grounded `0x00442c30` scenario-title set. + - the direct runtime-event comparison narrows it further too: + the checked note + `artifacts/exports/rt3-1.06/runtime-effect-kind8-title-overlap-note.md` + shows that `Louisiana.gmp` is the only carrier with a same-stem + `Dutchlantis.gmp` / `Dutchlantis` pair, but `Dutchlantis.gmp` itself still has no current + add-building dispatch rows while `Louisiana.gmp` keeps the one-row + `Add Building Warehouse05` cluster on + `nondirect-ge1e-h0001-0007-0000-5200-0200-p0000-0000-0000-ffff :: [7:0]`. So the strongest + current title overlap still does not reproduce the actual shipped add-building row family. + - the post-reload candidate set is checked in now too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-post-reload-candidates.md` extracts the + currently plausible late `0x00443a50` branches between ordinary reload and final kind-`8` + service, with the current bias explicitly shifted away from the known title-fixup branch and + toward the Tier 2 candidate/world-state rebuild owners rather than the Tier 3 + shell-progress/year-scalar refresh strip. + - the Tier 2 owner strip is checked in separately now too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-candidate-rebuild-subgraph.md` + shows the current bounded rebuild band rooted at `0x00412c10 / 0x00412bd0 / 0x00437737` + reaching candidate runtime-record rebuild `0x00412d70 / 0x00412fb0`, + named-availability query/upsert `0x00434ea0 / 0x00434f20`, + cargo-economy filter refresh `0x0041eac0`, + port/warehouse recipe rebuild `0x00435630`, + and only then the later world bringup / event-service neighborhood + `0x004384d0 / 0x00443a50 / 0x00432f40`. + - the strongest same-stem title pair has a Tier 2 availability note now too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-named-availability-note.md` + shows `Louisiana.gmp` and `Dutchlantis.gmp` share the direct named-availability bit for + `Warehouse05` (`1/1`), as well as `Port01`, `Furniture Factory`, `FarmGrain`, and + `Logging Camp`; their current `18` named-availability differences fall elsewhere + (`Bauxite Mine`, `AluminumMill`, `Farm Corn`, `FarmCotton`, `FarmRice`, `FarmSugar`, etc.). + The same note now folds in two deeper grounded constraints too: + `0x00412d70` does not consult the scenario-side recipe-book name at `[state+0x0fe8]`, and + `0x00434f20` writes only a boolean availability override bit. So the current Tier 2 question + is no longer “is `Warehouse05` simply toggled differently?” or “is a recipe-book display name + leaking through?” but + “which broader candidate-state rebuild or latch sequencing difference above + `0x00437737 / 0x00412c10 / 0x00412bd0 / 0x00412d70 / 0x00412fb0` leads to the shipped + `Add Building Warehouse05` row in `Louisiana.gmp`?” + The broader `compare-candidate-table` surface now reinforces the same point too: + `Louisiana.gmp` versus `Dutchlantis.gmp` stays on the same + `scenario-named-candidate-availability-table` semantic family, still keeps `Warehouse05 = 1/1`, + and moves the actual `difference_count = 43` into the wider industry mix and zero-trailer-name + set rather than a unique `Warehouse05` gate. + - that sequencing question is checked in directly now too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-sequencing-note.md` + extracts the current late order around the explicit `0x197` checkpoint: + the earlier `0x00443ebc` recipe-runtime rebuild still sits before + `0x00412c10 / 0x00412bd0 / 0x00434130 / 0x00436af0`, while the later `0x00444ac1` + checkpoint runs after `0x004354a0 / 0x00487de0` and then falls through into + `0x00437737 -> 0x00434f20 -> 0x00412c10`, with the separate recipe-runtime side re-entering + `0x00435630 -> 0x00412d70`. That keeps the next recovery pass focused on Tier 2 sequencing + interactions instead of title strings or direct `Warehouse05` availability bits. + - the strongest title-overlap pair now has a recipe-book note too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-recipe-book-note.md` + shows `Louisiana.gmp` and `Dutchlantis.gmp` diverge heavily on the recipe-book surface even + though `Warehouse05` stays `1 / 1` in the named-availability table. `Louisiana.gmp` is sparse + there (currently only `book00.line02` stays nonzero), while `Dutchlantis.gmp` keeps multiple + later mixed lines and mode words. That shifts the current Tier 2 bias further toward the + `0x00435630 -> 0x00412d70` rebuild side rather than the direct `0x00437737 -> 0x00412c10` + availability side. + The same note now also boxes in the same-condition-family counterexample: + `Louisiana.gmp` and `Texas Tea.gmp` both sit on `[7:0]`, but `Louisiana.gmp` keeps the one-row + `Warehouse05` cluster with only `book00.line02 = 0x00080000`, while `Texas Tea.gmp` keeps the + one-row `Port01` cluster with a broader four-book nonzero mode strip. So the current evidence + no longer supports “shared `[7:0]` family implies one shared Tier 2 recipe/runtime shape.” + - the bundled add-building carrier set now has a checked recipe-book corpus too: + `artifacts/exports/rt3-1.05/add-building-carriers-recipe-book-scan.json` + shows the six carrier maps split into two recipe families: + `Alternate USA.gmp` stands alone with the richer five-book nonzero mode strip, while the other + five carrier maps fall into one broader family but still diverge sharply inside it. + Among that five-map family, `Louisiana.gmp` is currently the sparsest nonzero profile + (only `book00.line02 = 0x00080000` plus the paired `book00` token lanes), while + `Chicago to New York.gmp`, `Rhodes Unfinished.gmp`, and `Texas Tea.gmp` each keep broader + multi-book nonzero mode strips and `Pacific Coastal.gmp` keeps supplied-token-only activity. + That makes `Louisiana.gmp` look less like a generic carrier-map recipe pattern and more like a + genuinely narrow Tier 2 recipe/runtime-record case. + - the recipe/runtime owner strip is checked in separately now too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-recipe-runtime-note.md` + summarizes the new subgraph rooted at `0x00435630 / 0x00412d70 / 0x00412fb0`. + The current bounded shape is a coupled rebuild family rather than a one-way ladder: + `0x00435630` re-enters `0x00412d70`, `0x00412d70` re-enters `0x00435630` plus + `0x00411ce0 / 0x00411ee0`, and `0x00412fb0` re-enters + `0x004120b0 -> 0x00412d70 -> 0x00412ab0 -> 0x00412c10`. + That keeps the next pass focused on the internal sequencing and handoff across the coupled + recipe/runtime/availability rebuild strip, not on one isolated helper. + The same note now carries the first upstream feed-in difference too: + `compare-setup-payload-core Louisiana.gmp Dutchlantis.gmp` already differs on + `payload_word_0x14` (`1870` vs `2025`), `payload_byte_0x20` (`0x3a` vs `0xfd`), + `payload_word_0x3b2` (`2` vs `1`), and the candidate-header words + (`0xcdcdcdcd` vs `0x00000000`). So the next pass can now work on both sides of the coupled + Tier 2 strip: the upstream setup payload core and the downstream recipe/runtime rebuild loop. + - the carrier-set setup-core comparison is checked in now too: + `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-setup-core-note.md` + widens that upstream comparison across all six bundled add-building carriers. It shows + `Louisiana.gmp` is not unique on `payload_word_0x3b2` (`Chicago to New York.gmp` also has `2`) + and shows `Louisiana.gmp` is unique among that six-map carrier subset on the candidate-header + sentinel pair `0xcdcdcdcd / 0xcdcdcdcd`, while `Alternate USA.gmp` stays separately unique on + the recognized `rt3-105-map-container-v1` header pair. The same note now widens further across + all 41 bundled `rt3_105` maps and shows the candidate-header pair is not actually + `Louisiana`-specific in the full corpus at all: nine maps share `0xcdcdcdcd / 0xcdcdcdcd`, + matching the older atlas read that this looks like coarse scenario-family framing instead of a + direct Tier 2 trigger. That trims the next upstream Tier 2 question again: focus on the more + specific remaining setup-core combination (`payload_word_0x14`, `payload_byte_0x20`, plus the + sparse recipe/runtime profile), not on the candidate-header class by itself. The same widened + note now records that the full current `Louisiana.gmp` tuple + (`payload_word_0x14 = 1870`, `payload_byte_0x20 = 0x3a`, `payload_word_0x3b2 = 2`, + `candidate_header = 0xcdcdcdcd / 0xcdcdcdcd`) is still unique across the 41-map corpus even + after the coarse header class is demoted, so the next pass should compare that tighter tuple to + the sparse recipe/runtime family rather than reopening the broad header-pair question. The same + note now also carries the grounded owner bridge for those compared setup-core fields. The early + copy path is still `0x00442400 -> 0x00502220 -> 0x0047be50`, but the later reset or + reactivation owners are now bounded too: + `0x00436d10` and `0x00443a50` both reimport the staged subset + `[profile+0x77/+0xc5]` before rerunning the same rebuild family that includes + `0x00435603`, `0x00435630`, `0x0041e970`, `0x00412bd0`, `0x00434130`, and `0x00436af0`. + That narrows the open question again: + `+0x14/+0x20` now have one real bridge into the Tier 2 strip through + `[profile+0x77/+0xc5]`, while `+0x3b2/+0x3ba` still stop on the setup-panel threshold or + scroll path. The next pass should therefore test whether the unique `Louisiana.gmp` + `+0x14/+0x20` pair is enough to explain its Tier 2 runtime shape through that grounded bridge, + or whether the sparse recipe/runtime side is still the more plausible differentiator. One + adjacent `compare-setup-launch-payload` check now sharpens that too: `Louisiana.gmp` does not + collapse into the nearest setup-core peers on the launch-token side either, but that band is + still not part of the currently grounded Tier 2 bridge. So the next pass should keep launch + tokens as supporting context while testing the already-grounded `+0x14/+0x20 -> [profile+0x77/+0xc5]` + bridge against the sparse recipe/runtime family. The same widened setup-core note now shows the + split inside that bridge too: `payload_byte_0x20 = 0x3a` is unique to `Louisiana.gmp` across + all 41 bundled `rt3_105` maps, while `payload_word_0x14 = 1870` has only one peer (`Mexico`). + That makes the campaign/setup-byte side of the bridge the strongest remaining setup-core + differentiator, with the year-word side a secondary companion rather than a full peer-group + collapse. The same note now also carries the direct bridge-peer check: + `compare-recipe-book-lines Louisiana.gmp Mexico.gmp` shows that the only `+0x14` peer still + stays zero across the checked recipe-book surface while `Louisiana.gmp` keeps the sparse + nonzero `book00` profile. So the next pass should keep the setup bridge narrowed to the + unique `+0x20` campaign/setup-byte side while still treating the sparse recipe/runtime family + as the stronger remaining differentiator. The same note now also sharpens the `+0x20` bridge + itself: current grounded downstream consumers of mirrored `[world+0x66de]` are mostly the + editor metadata checkbox and campaign-gated branches inside `0x00442c30`, not the core + Tier 2 helpers directly. So the likely Tier 2 relevance of `+0x20` is the branch gate inside + `0x00436d10 / 0x00443a50` before the rebuild family runs, not a later direct + `[world+0x66de] -> 0x00435630/0x00412bd0/0x00412c10` data path. The same note now weakens that + bridge a step further: current grounded profile-side consumers treat `[profile+0xc5]` as a + nonzero gate, and both `0x004425d0` and the campaign-side launch branch force it to `1`. + So the raw `Louisiana.gmp` setup payload byte `+0x20 = 0x3a` is not yet a grounded unique + numeric Tier 2 input; on the current evidence it collapses to the ordinary nonzero campaign or + setup branch before `0x00436d10 / 0x00443a50` run. That shifts even more weight back onto the + sparse recipe/runtime family, with `+0x14` the only currently grounded preserved setup-side + scalar still crossing the bridge. The same note now also adds a same-header-class recipe check: + `compare-recipe-book-lines Louisiana.gmp "Argentina Opens Up.gmp"` shows that even another + `0xcdcdcdcd / 0xcdcdcdcd` peer keeps a broader mixed `book01/book02` profile while + `Louisiana.gmp` stays sparse. So the coarse setup-header class is no longer a plausible + predictor of the Tier 2 runtime shape either; the recipe/runtime family remains the dominant + differentiator. The checked Tier 2 recipe-runtime note now carries the stronger current read: + `0x00435630` only materializes nonzero-mode rows, `Mexico.gmp` is the only `+0x14` peer and + still stays zero across the checked recipe-book surface, and the same-header peer + `Argentina Opens Up.gmp` keeps additional nonzero `book01/book02` content. So the next pass + should bias toward finding other maps that share Louisiana's minimal imported nonzero + recipe-runtime set, rather than reopening broader setup-core or header-class hypotheses. That + scan is now checked in at + `artifacts/exports/rt3-1.06/runtime-effect-kind8-tier2-recipe-signature-note.md`: + across all 41 bundled `rt3_105` maps there are 23 nonzero-mode path signatures, and + `Louisiana.gmp` sits in a small three-map `book00.line02`-only mode family with + `Britain.gmp` and `South East USA.gmp`. But `Louisiana.gmp` becomes unique again once the + supplied and demanded token lanes are included. That makes the next concrete Tier 2 recipe pass + smaller: compare `Louisiana.gmp` directly against `Britain.gmp` and `South East USA.gmp` on + the token-bearing minimal imported set and then on any downstream runtime-facing differences. + The note now carries that first peer split too: `Britain.gmp` diverges strongly with different + mode and extra supplied rows, while `South East USA.gmp` stays structurally closer but still + differs on the exact `book00.line01` demanded token and `book00.line02` mode/supplied-token + pair. So the current queue head is no longer “find a peer”; it is “find the downstream + runtime-facing consequence of Louisiana’s distinct exact `book00` token-bearing signature.” + That downstream split is now partially grounded too: direct `inspect-smp` checks show both + `Britain.gmp` and `South East USA.gmp` still carry `Port01` and `Warehouse05` names in-file + but neither has any add-building dispatch-strip trigger rows at all. So `Louisiana.gmp` is now + the first checked member of that `book00.line02` mode-shape family whose exact token-bearing + signature also lines up with the shipped add-building runtime path. + - the next corpus split is tighter now too: + the apparent `0x00010000 / 0x6c430000 / 0x00080000+0x00004080` three-row cluster is no longer + just a local `Louisiana.gmp` curiosity; `Ireland.gmp` and `Eastern China.gmp` both reuse parts + of it, but only inside broader multi-book imported profiles, and neither currently reaches any + add-building dispatch-strip rows. `Louisiana.gmp` is the only bundled map whose *entire* + nonzero imported recipe surface collapses to that exact three-row cluster. So the current + queue head is now narrower again: + test whether the shipped `5200 :: [7:0]` `Add Building Warehouse05` strip is tied to that + minimal imported-cluster shape specifically, rather than to the mere presence of one reused + `0x00080000 / 0x00004080` triplet inside a broader recipe profile. + - the same split is tighter even inside the broader “single imported row” family: + eight bundled maps currently materialize only one nonzero recipe row, but `Louisiana.gmp` is + the only one whose supporting token surface is just two additional demand rows instead of three + or four extra token-bearing rows. So the next checked discriminator is now narrower than + “single imported row” too: + test whether the `5200 :: [7:0]` add-building strip tracks that exact two-demand-plus-one- + imported minimal cluster, not just the existence of one imported row or one reused + `0x00080000 / 0x00004080` triplet. + - the importer-side boundary is tighter now too: + direct `inspect-smp` checks show `Louisiana.gmp`, `Britain.gmp`, and `South East USA.gmp` all + collapse to the same first importer shape of exactly one + `imports_to_runtime_descriptor = true` line on `nonzero-supply-branch`, while the broader + reuse case `Ireland.gmp` imports four such rows. So the next queue head is no longer + “prove the first importer branch differs”; it is: + recover the later runtime-record / named-availability consequence under + `0x00412d70 / 0x00412fb0 / 0x00412c10` that makes the minimal cluster land on + `5200 :: [7:0]` only in `Louisiana.gmp`. + - the direct named-availability table is still not that consequence: + the nearest single-import peer `South East USA.gmp` keeps `Warehouse05 = 1` and `Port01 = 1` + just like `Louisiana.gmp`, matching the earlier `Dutchlantis.gmp` read. So the next later + Tier 2 question is now even narrower: + recover the runtime-record bank/template consequences under `0x00412d70` + (plus dependent `0x00411ce0 / 0x00411ee0`) before `0x00412c10` mirrors anything into + `[candidate+0x7ac]`. + - the `0x00412d70` runtime-record roots are grounded now too: + direct `objdump` over `RT3.exe` shows `0x005c93d8 = "Warehouse%02d"` and + `0x005c93e8 = "Port%02d"`, with the rebuild choosing the `Port%02d` root only when + `[candidate+0xba] != 0` and the `Warehouse%02d` root otherwise. That turns the next queue head + into a concrete port-versus-warehouse runtime-record question: + recover how the bank/template pass and live availability bytes `[candidate+0xba/+0xbb]` + make `Louisiana.gmp` land on the `Warehouse%02d` side that later lines up with + `5200 :: [7:0]`. + - the writer-side split is narrower now too: + direct disassembly of `0x004120b0` shows that the per-record stream-load helper clears + `[candidate+0x79c/+0x7a0/+0x78c/+0x790/+0x794/+0x7b0]`, restores the fixed fields through + `[candidate+0x33]`, restores `[candidate+0xb9/+0xba/+0xbb]`, and streams the packed `0xbc` + descriptor array into `[candidate+0x37]`. Direct disassembly of upstream source-record import + `0x00414490` also restores `[record+0xb8/+0xb9/+0xba/+0xbb]`. The new checked-in building + source inspector now shows the stock `Data/BuildingTypes/*.bca` corpus keeps those four bytes + zero across every observed file, including `Warehouse.bca` and `Port.bca`. So the next + concrete recovery target is no longer the lower tagged-record reader itself: + recover which later owner or alternate content path makes the live + `[candidate+0xba/+0xbb]` bank/template state diverge from that all-zero shipped BCA corpus + before `0x00411ee0 / 0x00411ce0 / 0x00412c10` run. + - the broader consumer strip above that split is narrower now too: + direct disassembly now rules three more neighbors onto the read-only side of the bank bytes. + `0x00419230` only scans already-linked owner candidates and runs two rebank-or-clone passes + keyed by `candidate[+0xba]` and `candidate[+0xbb]` before stamping `Port%02d` / + `Warehouse%02d` labels into the auxiliary pool. `0x00418610` only feeds `candidate[+0xba]` + plus subtype/class predicates into the projected-rectangle sample-band helper. And the broader + projected-offset lane at `0x0041a5fd..0x0041a944` resolves the same owner candidate and gates a + world-cell occupancy sweep on `candidate[+0xba]`, but still does not reconstruct the byte. + The `.smp` restore-side auxiliary branch is negative in the same way: `0x00413f80` only + restores queued temporary aux-record images without the fixed selector-byte body + `[+0xb8..+0xbb]`, and `0x0041a950` only releases the live aux collection before re-entering + the same `0x004196c0 -> 0x00419230` import-plus-follow-on strip when the restore flags demand + it. + The outer world-entry load branch is fixed in the same way: `0x00438c70` allocates the live + candidate pool through `0x004131f0` and the auxiliary/source pool through `0x0041aa50`, and + both constructors tail directly into the same fixed tagged-import families rather than taking a + title-specific source-selection branch in between. + The startup-side availability preseed is negative in the same way now too: direct disassembly + of `0x00437737` and its sibling callsite `0x00436ad7` shows both branches only *reading* + live candidate subtype `[candidate+0x32]` plus bank bytes `[candidate+0xba/+0xbb]` and then + feeding those predicates into the scenario-side availability table at `[state+0x66b2]` through + `0x00434f20`; the later bring-up caller `0x00444acc` simply re-enters that same + `0x00437737 -> 0x00434f20 -> 0x00412c10` strip. So even the visible startup preseed still + consumes already-materialized bank/template bytes rather than explaining how they became + nonzero in the first place. + The constructor/import seam is fixed in the same negative way now too: `0x00438c70` allocates + the live candidate pool through `0x004131f0`, calls `0x004411d90` (currently a no-op stub), + and only then allocates the aux/source pool through `0x0041aa50`, so there is no hidden branch + between those two constructors. The only checked caller of source-record importer `0x00414490` + is `0x00419788`, and the surrounding `.rdata` proves that strip is the stock + `"%1*.bca"` / `".\\Data\\BuildingTypes\\"` scan rather than a map-specific alternate + package path. So the remaining Tier-2 mystery is not “which hidden caller invokes the BCA + parser?”; it is “which later non-stock writer or projection seam makes live + `[candidate+0xba/+0xbb]` diverge after the fixed stock BCA import has already run?” + The stock asset-side negative is no longer completely uniform either. The checked-in + `inspect-building-type-sources` report now shows the shipped `Data/BuildingTypes` corpus is + zero at `0xb8..0xbb` for every current `.bca` except `MachineShop.bca`, which carries the lone + selector-window exception `(0x00, 0x80, 0x3f, 0x00)` on a `788`-byte row. The separate + `backup/Bldg/*.bca` corpus stays fully zero in that same window. So the queue head is narrower + again: explain whether the Machine Shop exception is part of the seeded Tier-2 family at all, + or recover the later projection seam that still has to account for the broader live divergence. + Direct map-payload inspection now sharpens that split further. The current Tier-2 maps + `Louisiana.gmp`, `Dutchlantis.gmp`, and `South East USA.gmp` all already carry the literal + `Port00` / `Warehouse00` / `Port01` / `Warehouse01` names inside the shared setup payload at + stable offsets `0x6f77`, `0x7087`, `0x70cb`, and `0x7241` respectively, while the same map + corpus shows no `MachineShop` string at all. That moves the queue head again: the live + `PortNN` / `WarehouseNN` family is now more plausibly a setup-payload candidate-table / source + row projection problem than a hidden stock-BCA stem problem. + The fixed candidate-table rows behind that hint are now checked directly too: + `runtime inspect-candidate-table` shows the same contiguous block in all three maps with + `Port00` at row `35` / file offset `28535`, `Warehouse00` at row `43` / offset `28807`, + `Port01` at row `45` / offset `28875`, and `Warehouse01` at row `56` / offset `29249`, with + the surrounding `Port02..11` / `Warehouse02..11` rows staying in the same contiguous cluster + and each carrying availability trailer `0x00000001`. So the remaining Tier-2 source question + is no longer whether those names exist as stable scenario rows; it is how that stable + candidate-table cluster is projected into the later aux-record bank and then into the live + clone families. + The new root scan sharpens that boundary further. `runtime scan-candidate-table-headers + rt3_wineprefix/drive_c/rt3/maps` shows `37` probe-bearing shipped maps and `4` skips, while + the narrower `runtime scan-candidate-table-named-runs` command confirms that + the shipped probe-bearing maps split into two stable `00`-row families rather than one: + `30` maps keep `Port00` / `Warehouse00` at rows `35` / `43`, while `7` maps move just those + two `00` rows earlier to `10` / `18`; the `Port01..11` and `Warehouse01..11` runs stay fixed + at `45..55` and `56..66` in all `37` probe-bearing maps. Trailer families split separately + too: `28` maps keep the numbered rows on trailer `0x00000001`, while `9` maps keep the same + row layout but zero those trailers. Raw map-string presence is broader than that actual + candidate-table seam too: + `Port00` appears in all `41` shipped `.gmp` files, but `Central Pacific.gmp`, `Italy.gmp`, + `Tex-Mex.gmp`, and `Texas Tea.gmp` do not expose the fixed candidate-table header at all. So + the next Tier-2 source pass should target the `37` probe-bearing maps rather than the noisier + full string-bearing map corpus. + The earlier `+0x173` writer census now trims several dead ends from that pass. Direct + inspection shows `0x00416ded` is only a collection-local row-id seed via `0x00518380` before + `0x00518900`; `0x004274d8` and `0x0042872d` are object-local rollover/allocator copies; and + `0x005b7a6f` is just a helper-side child allocation. The only candidate-family `+0x173` store + in this strip that still matters to Tier-2 is `0x00419559`, and it is simply the later clone + pass stamping the chosen seed-row id onto the new row. So the queue no longer needs another + generic `+0x173` writer sweep before returning to the actual bank-byte owner. + The `Port00` / `Warehouse00` name mystery is narrower than that bank-byte owner now too. + Direct recovery shows `0x00419680` constructing `gpdBuildingTypeDB` at `0x0062b2fc` by + storing vtable `0x005c9718`, and the surrounding `.rdata` descriptor at `0x005c9718` is the + `%1*.bty` table `{ 0x00416950, 0x00416ce0, 0x004168e0, "%1*.bty", ... }`. That same vtable is + used by the ordinary DB load path at `0x0044431c`, which calls slot `+0x04 = 0x00416ce0`, + while `0x00416950` iterates live rows and calls slot `+0x08 = 0x004168e0` as the per-entry + cleanup path. Inside `0x00416ce0`, the stock building-type loader rewrites only the bare + source names `port` (`0x005c8f94`) and `warehouse` (`0x005c8e4c`) to `Port00` + (`0x005c96a0`) and `Warehouse00` (`0x005c9694`) before scanning the live candidate collection + `0x0062b268`; when it finds the matching candidate row, it seeds `[row+0x173]` from the live + candidate row id at `0x00416ded` and then re-enters `0x00518900`. So the queue no longer + needs a generic hidden late writer for the `00` names themselves: that remap is already owned + by the stock `.bty` load callback. The remaining Tier-2 source question is now the row-family + choice and later bank selection above that remap, especially the shifted `00` families + (`35/43` vs `10/18`) and the still-fixed `01..11` run families (`45..55` / `56..66`). + The checked-in stock source report agrees with that split too: + `artifacts/exports/rt3-1.06/building-type-sources.json` still shows only bare source stems + `port` and `warehouse`, while `port00..11` and `warehouse00..11` stay in + `named_binding_comparison.binding_only_canonical_stems`. So the queue no longer needs more + stock `BuildingTypes/*.bty` discovery for the numbered families either; the remaining `01..11` + source work belongs under scenario/live candidate-table projection and row-family choice above + the already-grounded bare-name remap. + The shipped asset directory makes that remap boundary explicit too: `Data/BuildingTypes` + contains bare `Port.bty/.bca` and `Warehouse.bty/.bca`, but no stock `Port00` or + `Warehouse00` assets on disk. So the checked-in queue no longer needs to speculate about a + hidden on-disk `00` family; the loader-side `port -> Port00` and `warehouse -> Warehouse00` + rewrite is the actual stock bridge. + The stock source-family strip above that remap is grounded now too. `0x004196c0` walks the + `%1*.bty` file set, routes each match through `0x00414490`, calls the stock load callback + `0x00416ce0`, and only then tails into `0x00419230`. The recovered source-name table is + `StationSml`, `StationMed`, `StationLrg`, `ServiceTower`, `Maintenance`, `ClpBrd`, `Kyoto`, + `Persian`, `SoWest`, `Tudor`, and `Victorian`, so the next Tier-2 source question is no longer + “where do numbered names come from?” but “which style-family rows first survive into the later + two-bank, twelve-ordinal clone pass?” + The stock asset corpus now agrees with that table directly too. The shipped + `Data/BuildingTypes` directory contains `VictorianStationSml/Med/Lrg.bty`, + `TudorStationSml/Med/Lrg.bty`, `SoWestStationSml/Med/Lrg.bty`, + `PersianStationSml/Med/Lrg.bty`, `KyotoStationSml/Med/Lrg.bty`, + `ClpBrdStationSml/Med/Lrg.bty`, plus standalone `Maintenance.bty` and + `ServiceTower.bty`, alongside the style-specific house families. So the + `0x005f3c6c/0x005f3c80` tables are no longer just a recovered naming hypothesis; they line up + with real stock filename families on disk. + The exact-match resolver beneath that style-family strip is grounded now too. `0x00419590` + copies one source-kind name from `0x005f3c6c`, combines it with one style/theme entry from the + smaller subset table `0x005f3c80` through format `"%1%2"` at `0x005c8730`, and then scans the + live auxiliary pool `0x0062b2fc` for the first exact `[entry+0x04]` match. So the + `0x005f3c6c/0x005f3c80` pair is no longer an anonymous bank byte table; it is the stock + style-family-to-combined-name resolver that sits immediately below the remaining Tier-2 source + selection frontier. + The recovered `.bty` header probes tighten that source split further too. The checked-in + `inspect-building-type-sources` report now shows `Port.bty` and `Warehouse.bty` are ordinary + `type_id = 0x000003ec` rows with direct bare-name headers (`name_0x22` / `name_0x7c` = + `Port` or `Warehouse`) and shared `dword_0xbb = 0x000001f4`, while the style-station rows such + as `VictorianStationSml/Med/Lrg.bty` stay on the same `0x000003ec` family but keep + `name_0x7c = VictorianStations` and `dword_0xbb = 0x00000000`. The standalone + `Maintenance.bty` / `ServiceTower.bty` rows also stay in the same stock family, but expose + display names `Maintenance Facility` and `Service Tower` with zero `dword_0xbb`. The stock + nonzero family is explicit now too: only one recovered `.bty` header lane is nonzero, + `dword_0xbb = 0x000001f4`, and it spans exactly `22` files: + `Brewery`, `ConcretePlant`, `ConstructionFirm`, `DairyProcessor`, `Distillery`, + `ElectronicsPlant`, `Furnace`, `FurnitureFactory`, `Hospital`, `Lumbermill`, `MachineShop`, + `MeatPackingPlant`, `Museum`, `PaperMill`, `PharmaceuticalPlant`, `Port`, `Recycling Plant`, + `Steel Mill`, `Textile Mill`, `Tire Factory`, `Tool and Die`, and `Warehouse`. So the + remaining Tier-2 source question is no longer whether the numbered `Port%02d` / + `Warehouse%02d` banks are hidden station-style aliases; it is why the later clone path prefers + this narrower `0x000001f4` stock family over the zero-valued station and + maintenance/service families when it seeds those numbered banks. + The stock rebuild handoff above that seed question is tighter now too. Direct disassembly of + `0x004196c0` shows the broader stock `*.bca` rebuild loop formatting the wildcard path rooted + at `0x005c93fc`, iterating the file enumerator through the `0x005c8190/0x005c8194/0x005c819c` + find-first/find-next strip, calling the per-file stock loader `0x00414490` for each hit, and + only then tail-calling `0x00419230`. So the remaining Tier-2 source problem is increasingly + “which stock rows that rebuild admits or seeds with nonzero bank bytes” rather than “which + unrelated later scheduler invokes the banked clone pass.” + The direct `+0xba/+0xbb` writer census is narrower now too. The obvious newly surfaced stores + at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side portrait/string + refresh helpers: they walk a separate id-keyed collection through `0x0053f830`, free and + replace heap strings at dword `[entry+0xba]`, and mirror shell text from `[0x006cec74+0x1ef]`. + The other new dword writers at `0x00540251/0x0054034d`, `0x0055fd40`, `0x0055bdc4/0x0055bf01`, + `0x0055ca78`, `0x0055f290`, `0x005b5168`, and `0x005b6718` likewise belong to wider + non-candidate heap objects with their own vtables and field layouts, not to the live + `0x005c93cc` candidate rows. + The actual candidate import strip now has a tighter positive bound instead: `0x004120b0` + explicitly declares `[candidate+0xba]` and `[candidate+0xbb]` as one-byte parser fields through + `0x00531150`, while `0x00412d70` can later clone a whole already-materialized candidate row + through `rep movsl`, including those byte fields, before `0x00412f02` chooses the + `Port%02d`/`Warehouse%02d` naming branch from the cloned `[candidate+0xba]` bit. So the live + divergence frontier is narrower again: not generic direct stores into candidate rows, but the + earlier seed or projection seam that first makes some source/live rows reach that clone path + with nonzero bank bytes. + That owner chain is explicit now too: the stock candidate collection root `0x0062b268` is + constructed at `0x00438c8e -> 0x004131f0 -> 0x00412fb0 -> 0x004120b0`, and the adjacent + `.rdata` strings at `0x005c93f4..0x005c940e` prove that `0x00412fb0` is the + `Data\\BuildingTypes\\%1*.bca` / `%1*.bty` loader, not a map-side overlay reader. The + immediate follow-on call at `0x00438ca0 -> 0x00411d90` is a no-op, so there is no hidden + world-load mutation directly after the stock constructor. That narrows the next Tier-2 source + question again: either a later candidate-class serializer/import/export family such as + `0x00414500..0x00414b14` is still replaying non-stock bank bytes into live rows, or the + qualifying source rows already live in the stock `BuildingTypes` corpus and the remaining work + is to explain which rows the later clone path chooses. + The clone-group lane is tighter now too. Candidate dword `[candidate+0x794]` is not a broader + owner-state field written elsewhere; the current direct write set shows it is initialized to + zero in `0x004120b0` and then assigned only in `0x00412d70` as the chosen seed-row index that + later clones should follow. The known consumers now line up with that meaning: + `0x0041eba8` compares region-side row key `[row+0x2f2]` against `[candidate+0x794]`, + `0x004cccdc` excludes candidates whose `[candidate+0x794]` already matches one active row key, + and `0x0050a1cd` keeps the same seeded rows out of one later availability branch. So the open + question is not “who writes the mysterious group id?” but “which source/live rows become the + first seeded rows that let `0x00412d70` propagate nonzero bank bytes within that family?” + The later rebank-or-clone owner is narrower now too. Direct disassembly of `0x00419230` shows + one outer pass over bank selector `0` then `1`, with an inner `12`-ordinal sweep each time. + For each `(bank, ordinal)` pair it scans already-linked aux/source rows by owner candidate id + `[entry+0x173]`, requires nonzero bank byte `[candidate+0xba]` or `[candidate+0xbb]` + accordingly, prefers a previously materialized target whose ordinal field `[entry+0x187]` + already matches, otherwise clones one qualifying source row, copies the optional heap planes by + size `([entry+0xb8] * [entry+0xb9] << 5)`, stamps `[entry+0x187] = ordinal+1`, writes the + visible stem at `[entry+0x22]` from `Warehouse%02d` or `Port%02d`, mirrors that stem into the + exact-match key at `[entry+0x04]`, and finally rebinds `[entry+0x173]` by exact stem match + back into the live candidate pool. That moves the queue head one step earlier again: identify + which qualifying source rows first satisfy each `(bank, ordinal)` pair before `0x00419230` + ever clones or renames them. + So the honest next queue head is now one step earlier again: + recover the setup-payload or restore-time projection owner that carries those static + `PortNN` / `WarehouseNN` rows into the live candidate clone families with nonzero + `[candidate+0xba/+0xbb]`, beyond the separate `MachineShop.bca` selector-window exception in + the shipped `BuildingTypes/*.bca` corpus. + kinds”; it is the smaller set of scenario-specific records where that sweep explicitly writes + `[event+0x7ef]` itself or a still-later owner does. + - two explicit trigger-kind materializations are now grounded inside that retagger: + the `SP - GOLD` branch at `0x00443526` rewrites `[event+0x7ef]` from `1 -> 5` on live + runtime-event id `1` when `[world+0x66de]` is set and the checked root payload is kind `7` + with subtype byte `5`, while the `Labor` branch at `0x00443601` rewrites `[event+0x7ef]` + from `0 -> 2` on live runtime-event id `0x0d` when the same scenario flag is set and the + checked `0x3c -> 0x3d` child payload pair carries the matching negative scalar sentinel. + - cross-map probing now gives a better static-analysis lead too: + `British Isles.gmp` shows no current `0x00431b20` dispatch-strip rows, `Germany.gmp` stays on + `Game Variable 1` plus `Company Variable 3..4`, while `Texas Tea.gmp` adds `Economic Status` + and one still-unlabeled grouped descriptor id `548`; that makes descriptor `548` plus the + `opcode 8` branch on record `7` the next concrete non-hook analysis target above the compact + runtime-event loader + - the checked-in function map now sharpens that `Texas Tea.gmp` branch too: + opcode `0x08` in the grounded `0x00431b20` dispatch strip lands on + `0x00426d60 company_deactivate_and_clear_chairman_share_links`, so the open question is + whether grouped descriptor id `548` is the missing compact-event label for that destructive + company-clear path or a neighboring unmapped id-space entry in the same branch family + - the broader installed-map sweep narrows that question further: + across all `41` bundled `.gmp` files in the current `rt3_105/maps` install, grouped descriptor + id `548` currently appears only once, in `Texas Tea.gmp` record `7`, with `opcode 8`, + `scalar 0`, standalone condition tuple `(7, subtype 0)`, and compact signature family + `nondirect-ge1e-h0001-0007-0000-6d00-0200-p0000-0000-0000-ffff` + - the same wider sweep also rules out the simplest alias theory: + the ordinary checked-in `Deactivate Company` descriptor `13` does appear in real map bundles, + but only on `opcode 1` with scalar `1` in `British Isles.gmp`, `Chicago to New York.gmp`, + `East Coast, USA.gmp`, `Japan Trembles.gmp`, and `State of Germany.gmp`; it does not appear on + the `opcode 8` deactivation branch, so grouped descriptor id `548` is not just the obvious + compact stand-in for ordinary descriptor `13` + - that compact opcode-`8` cluster is now grounded as an artifact-boundary problem rather than a + mysterious compact-only id family: + direct binary inspection of the `0x00610398` EventEffects table shows the contiguous table does + not stop at row `519`; it continues cleanly through row `613`, with the extractor-side + sequential descriptor invariant still holding. The checked-in extractor and semantic catalog now + cover the full `614`-row export instead of the old truncated `520`-row slice + - that closes the earlier unlabeled cluster: + grouped descriptor ids `521`, `526`, `528`, `548`, and `563` are now recovered as + `Add Building FarmGrain`, `Add Building Furniture Factory`, `Add Building Logging Camp`, + `Add Building Port01`, and `Add Building Warehouse05` respectively. The checked-in + `event-effects-building-bindings.json` now carries the full descriptor-side candidate bridge + across all `111` add-building rows (`503..613`) through the direct `descriptor_id - 503` + mapping in `0x00430270`, while concrete candidate names remain grounded only for the stable + RT3 1.05 live-catalog run `0..66` exposed by `0x0041ede0` and the checked candidate-table + corpus + - the earlier `label_id - 2000` bridge for `548` and `563` is now known to be a false lead: + those numeric collisions hit the special-condition label table + (`Disable Building Stations`, `Completely Disable Money-Related Things`), but the extended + EventEffects table proves the actual grouped descriptors are add-building slots, not + special-condition verbs + - the compact opcode-`8` frontier therefore shifts: + direct disassembly of `0x00430270` now shows that the add-building consumer does not branch on + grouped opcode at all for descriptor strip `503..613`; it consumes the descriptor-derived + candidate id, placement count byte `0x11`, center words `0x12/0x14`, and radius word `0x16`, + clamps that radius to at least `1`, and retries randomized placements up to `200` times. + The next static-analysis pass should therefore target the remaining span-field meaning and + shell-owned placement-flow ownership on that strip, not more missing-label recovery: the + descriptor-side candidate bridge is now checked in across `503..613`, and the honest remaining + boundary is the missing non-hook name catalog for candidate ids `67..110` + - the new offline `BuildingTypes` source report sharpens that missing name-catalog boundary too: + `runtime inspect-building-type-sources rt3_wineprefix/drive_c/rt3/Data/BuildingTypes artifacts/exports/rt3-1.06/event-effects-building-bindings.json` + now reports `77` `.bca` files, `200` `.bty` files, and `208` canonical asset stems. Against + the checked-in named add-building bindings it finds exactly `43` shared canonical stems, + `24` binding-only stems (`Port00..11` and `Warehouse00..11`), and `165` broader asset-only + stems. The numbered live `Port00..11` and `Warehouse00..11` names therefore collapse to + generic asset stems `Port` and `Warehouse` on disk, so the `BuildingTypes` directory is now + grounded as a wider offline source catalog, but not yet as a direct second live candidate-name + owner for descriptor-side ids `67..110` + - the local-runtime builder strip now reinforces that same boundary: + direct disassembly of `0x00418be0` shows the broader placed-structure rebuild lane resolving + its caller-supplied stem only through `0x00416e20 indexed_collection_resolve_live_entry_id_by_stem_string` + against the current live candidate collection, then projecting runtime scratch through + `0x00416ec0` and `0x00418610`. So the broader `BuildingTypes` asset pool is not yet a proven + alternate live owner for descriptor-side add-building candidate ids; current non-hook evidence + still routes stem resolution back through the live candidate collection that tops out at the + contiguous named run `0..66` + - the shipped-map compact-dispatch corpus is narrower than the descriptor strip too: + `runtime inspect-compact-event-dispatch-cluster-counts rt3_wineprefix/drive_c/rt3_105/maps` + scans all `41` bundled maps and finds add-building dispatch occurrences only for descriptors + `506`, `507`, `521`, `526`, `528`, `548`, and `563`. No bundled-map dispatch rows currently + exercise descriptor ids `570..613`, so the back half of the widened add-building strip is now + bounded as descriptor-grounded but latent in the shipped compact-event corpus + - the concrete owner strip above that bundle is grounded now too: + `0x00433060` is the direct non-direct serializer loop that writes `0x4e99/0x4e9a/0x4e9b`, + calls `0x00430d70` per live collection row, and sits beside the sibling `0x00433130` size/load + family rather than behind an unknown blob writer + - those non-direct rows still carry stable structural family ids in the inspection notes: + the row probe emits `compact signature family = nondirect-...` keys alongside decoded grouped + descriptors, so repeated compact families can still be compared across maps without scraping + raw bytes + - that moves the startup compact-effect blocker again: + the remaining question is no longer compact row framing, but which serialized/live rows in this + now-decoded non-direct bundle correlate to loaded trigger-kind `8` rows and which of those can + reach the placed-structure mutation opcodes under `0x00431b20` + - the `[site+0x27a]` companion lane is grounded now too: + it is a live signed scalar accumulator rather than a second owner-identity seam, with zero-init + at `0x0042125d` and `0x0040f793`, accumulation at `0x0040dfec` and `0x00426ad8`, direct set on + acquisition commit at `0x004269e4`, and negated clear at `0x00426a44..0x00426a90` + - the remaining owner-company question is therefore narrower than “find any replay seam”: + identify which non-transport persisted source family outside the currently bounded direct + allocator/finalize/store families, the save-side `0x00413440` serializer, the load-side + `0x00413280` cached-source bridge, the checked ordinary replay strip + `0x00444690 -> 0x004133b0 -> 0x0040ee10`, and the already-bounded loaded runtime-effect lane + `0x00444d92 -> 0x00432f40(kind 8) -> 0x004323a0 -> 0x00431b20` feed the tuple or companion + restore calls that are sufficient to repopulate `[site+0x276]` for shellless acquisition; the + remaining runtime-effect subquestion is now which loaded kind-`8` rows can carry the + placed-structure mutation opcodes rather than whether kind `8` is synthetic + - the ordinary restore-vs-finalize split is tighter now too: + direct disassembly shows `0x00413280` stream-loading `0x36b1/0x36b2/0x36b3` through per-entry + vtable slot `+0x40`, and `0x00444690` simply enters `0x004133b0` before continuing into later + grid/world refresh owners; neither ordinary bring-up owner re-enters + `0x004134d0 / 0x0040f6d0 / 0x0040ef10` for already-restored rows. The positive + `[site+0x276]` store at `0x0040f5d4` therefore remains grounded only on explicit tuple/create + paths, so the next non-hook pass should look for a non-transport persisted tuple family or a + later ordinary restore owner rather than another generic replay scan. + - the loaded runtime-effect lane is narrower now too: + real map inspections already show decoded `0x00431b20` grouped-descriptor content inside the + ordinary non-direct bundle without any recovered trigger-kind metadata in the current surface: + `Texas Tea.gmp` reports `Add Building Port01`, `Alternate USA.gmp` reports + `Add Building FarmGrain` and `Add Building Logging Camp`, and `War Effort.gmp` reports only + variable bands, while all three currently summarize `trigger_kinds_present = []` and + `records_with_trigger_kind = 0`. That means the next non-hook pass on this branch is no longer + “do ordinary loaded rows reach `0x00431b20`?”; it is “where is trigger-kind represented or + bypassed for those already-decoded ordinary compact rows?” + - the adjacent control-lane inheritance path is narrower now too: + direct disassembly shows `0x0042db20` loading only the linked `0x1e`/`0x28` row bodies from + `0x4e9a`, while the separate deep-copy helper `0x0042e050` copies text bands plus the full + `[event+0x7ee..0x7fa]` block. Current caller census shows that deep-copy seam reached from the + event-editor duplication path at `0x004dba23`, not from the ordinary `0x00433130` restore + strip. So the next non-hook question is not “maybe load inherits trigger kind through + 0x0042e050”; it is which other ordinary owner, if any, seeds `[event+0x7ef]` for these + already-decoded non-direct rows. + - the first non-editor positive control-lane writer is bounded away from restore too: + direct disassembly now shows `0x00430b50` allocating a fresh live runtime-effect row through + `0x00432ea0 -> 0x0042d5a0`, zero-initializing the full `[event+0x7ee..0x7fa]` block, then + seeding `[event+0x7ef]` to `2` or `3` plus adjacent control bytes. That builder is reached + from the `0x004323a0` follow-on service strip at `0x0043232e`, not from `0x00433130` restore. + So the remaining ordinary-owner question is narrower again: neither deep-copy inheritance nor + the first positive control-lane builder currently belongs to the non-direct `0x4e9a` load path. + - the remaining non-editor control-lane mutators are bounded away from restore too: + direct disassembly of `0x00443200..0x004436e3` shows a name-driven live retagger sweep over + the already-materialized event collection `0x0062be18`, matching text bands through + `0x005a57cf` against scenario strings such as `New Beginnings`, `Chicago to New York`, + `The American`, and `Labor`, then mutating `[event+0x7ef/+0x7f9/+0x7fa]` on those live + records. That is now a grounded post-load live-maintenance seam, not the missing ordinary + `0x4e9a` control-lane owner. + - the positive-path caller census is effectively boxed in now too: + direct `0x0040ef10` callers are the create-side pair `0x00403ef3 / 0x00404489`, the transport + tuple pair `0x0046f073 / 0x004707ff`, and the already-ruled-down live controller + `0x005098eb`; direct `0x004134d0` callers are the create-side pair `0x00403ed5 / 0x0040446b`, + the transport-side pair `0x0046efbf / 0x0047074b`, the counted transport builders + `0x00472bef / 0x00472d03`, plus live/controller callers `0x00422bb4` and `0x00508fd1`. + That means the still-missing owner seam is no longer “find another obvious constructor/finalize + caller”; it is specifically a non-transport persisted tuple family or a later ordinary restore + owner outside this boxed caller set. + - the second is narrower in the same way: + the checked-in `0x36b1/0x36b2/0x36b3` triplet seam and the + `0x4a9d/0x4a3a/0x4a3b` side-buffer seam still do not serialize `[site+0x310/+0x338/+0x360]` + directly, so the known save seams are ruled down even though a later restore family is still open + - the dynamic side-buffer load seam is ruled down too: + `0x00481430 -> 0x0047d8e0` repopulates the route-entry list, three byte arrays, five + proximity buckets, and the trailing scratch band from stream, but still does not claim the + cached tri-lane + - the tri-lane now has one real runtime accumulator too: + direct local binary inspection shows `0x0040c9a0` folding `[site+0x310/+0x338/+0x360]` into + `[site+0x2b4/+0x2b8/+0x2bc]`, mirroring the nine-dword side array rooted at `[site+0x2e4]`, + and then clearing the tri-lane + - caller census keeps that tri-lane role narrow: + `0x0040c9a0` only appears under the broad live-collection sweep + `0x0040a3a1..0x0040a4d3`, while `0x0040cac0` stays under weighted scoring or evaluation + families such as `0x0040fcc0..0x0040fe28` and `0x00422c62..0x00422d3c` + - direct local binary inspection now rules out the old “no live writer” hypothesis too: + `0x0040d4aa/0x0040d4b0` add into `[site+0x310]`, + `0x0041114a7/0x004111572` add into `[site+0x310]`, + `0x0041114b7/0x004111582` add into `[site+0x338]`, and + `0x0041118aa/0x0041118f4` add into `[site+0x360]` + - the writer family is now bounded one level higher: + `0x0040d450` is a small owner-company-aware producer over `[site+0x276]`, + `0x00455810/0x00455800/0x0044ad60`, and `0x00436590` ids `0x66/0x68`, while + `0x00410b30..0x004118f4` is a broader candidate-processing loop walking `0xbc`-stride rows, + gating them through `0x00412560`, and then accumulating both stack temporaries and direct + writes into `[site+0x310/+0x338/+0x360]` + - `0x00412560` is now bounded as the shared candidate/admissibility gate above that loop: + it checks candidate-row fields `+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44`, world date/flags via + `0x006cec78`, and the candidate table `0x0062ba8c` + - the cached source/candidate bridge is now grounded on stream load too: + direct local binary inspection shows `0x00413280` dispatching per-entry vtable slot `+0x40` + on the `0x005c8c50` specialization table, that slot resolving to `0x0040ce60`, and + `0x0040ce60` immediately re-entering `0x0040cd70` plus `0x0045c150` + - the third hypothesis is now a cached source/candidate bridge question, not just a raw + `[site+0x04]` selector question: + `0x0040cd70` seeds `[site+0x3cc/+0x3d0]` from `0x62b2fc / 0x62b268`, + `0x0040cee0` resolves cached candidate id `[site+0x3d0]` back into the live candidate pool, + and `0x004138f0` already counts live placed structures by that cached candidate id + - the checked-in consumer side is tighter too: + `0x0040dc40` already consumes live owner company `[site+0x276]`, company stat-family + `0x2329/0x0d`, candidate field `[candidate+0x22]`, and the projected-cell validation strip + `0x00417840 -> 0x004197e0`, then commits the linked-site mutation through + `0x0040d1f0 / 0x00480710 / 0x0045b160 / 0x0045b9b0 / 0x00418be0 / 0x0040cd70` + - the periodic-company trace now carries the tri-lane live-service family as structured fields, + not just prose: + - site cached tri-lane status = + `live_writer_family_grounded_semantics_and_persisted_inputs_missing` + - tri-lane live service status = + `candidate_gate_and_live_writer_family_grounded_exact_formula_and_persisted_inputs_missing` + - tri-lane live owners include: + `0x0040d450`, `0x00410b30..0x004118f4`, `0x00412560`, `0x0040c9a0`, + and the downstream `0x0040fcc0..0x0040fe28 / 0x00422c62..0x00422d3c` consumers + - tri-lane gate fields include: + candidate-row fields `+0x20/+0x22/+0x24/+0x28/+0x2c/+0x44`, + world date/flags via `0x006cec78`, + candidate table `0x0062ba8c`, + and caller-provided subject vtable slot `+0x80` plus owner-present flag `[site+0x246]` + - tri-lane writer roles are now split explicitly between: + one owner-company-aware local scorer `0x0040d450`, + the broader `0x00410b30..0x004118f4` candidate loop, + and the later `0x0040c9a0` accumulator/reset + - direct caller families are now split explicitly too: + `0x0040fb70` is the small wrapper into `0x00412560`, + `0x004b4052 / 0x004b46ec` are collection-wide `0x0040fb70` census callers over + `0x0062b26c`, + `0x00401633` is an acquisition-adjacent `0x0040d540` caller that immediately feeds company + stat-family `0x2329/0x0d`, + `0x0044b81a` is an owner-company-aware `0x0040d540` caller that also reaches + `0x0040cb70` and news/event id `0x65`, + and `0x004b70f5 / 0x004b7979` are broader sibling `0x0040d540` callers routing through + `0x004337a0` and downstream `0x00540120 / 0x00518140` + - formula input lanes are now structured too: + `0x00412560` uses candidate-row time window `+0x20/+0x22`, + owner/absence booleans `+0x24/+0x28`, + list count `+0x2c`, + and membership list `+0x44`, + while the wider `0x00410b30..0x004118f4` loop consumes candidate-row + `+0x18/+0x1c/+0x2a/+0x2c/+0x44`, + subject latch `[site+0x78c]`, + personality byte `[site+0x391]`, + world lanes `[world+0x0d]`, `[world+0x4afb]`, `[world+0x4caa]`, + owner-company scalar `[company+0x0d5d]`, + and the local cache bands `[site+0x2e8]`, `[site+0x310]`, `[site+0x338]`, `[site+0x360]` + - the direct writer census now narrows the remaining owner-company question too: + grounded `[site+0x276]` writes cluster under create-side and live mutation families such as + `0x004269b0 / 0x00426a10`, the create-side `0x0040ef10 / 0x0040f6d0` strip, and the bulk + reassignment families `0x00426dce..0x00426ea1` and `0x00430040..0x004300d6`, not under the + known `0x00444690 -> 0x004133b0 -> 0x0040ee10` replay strip + - the direct writer census is split more cleanly now too: + `0x00421200` is the broad late-field constructor/reset zero-fill over the same + `0x23a/0x23e/0x25a/0x25e/...` row family and clears `[+0x276]` as part of that initialization, + `0x00428270` is a collection-wide live owner remap over `0x0062b26c` that rewrites + `[site+0x276]` only for rows matching one old owner id, and `0x00422280` is a subtype-local + synthetic scalar writer that stores one `100000 * rand(bucket)` result into `[+0x276]` before + publishing localized-id `7`, so none of those three writes is the missing replay owner seam + - the same write side is now grounded one level higher too: + direct local control-flow reconstruction shows those families hanging under the grouped opcode + dispatcher `0x00431b20` over `0x0061039c`, with opcodes `0x04..0x07` dispatching to + `0x00430040`, opcodes `0x08/0x10..0x13` dispatching to `0x00426d60`, and opcodes + `0x0d/0x16` dispatching to `0x0042fc90` + - `0x0042fc90` itself is now ruled onto the live mutation side too: + it iterates the live placed-structure collection `0x0062b26c`, filters rows through + `0x0040c990`, optional owner-company match `[site+0x276]`, and row vtable slot `+0x70`, + which keeps that branch on the live application side rather than replay + - the focused save-side triplet probe is now available directly too: + `runtime inspect-save-placed-structure-triplets ` dumps the grounded + `0x36b1/0x55f1/0x55f2/0x55f3` rows without the wider periodic trace wrapper + - real-save output from that focused probe rules the checked-in triplet seam down further: + on grounded `p.gms`, all 2026 rows keep policy trailing word `0x0101`, the profile side is + dominated by companion byte `0x00` with status `unset` (`1885` rows) plus farm-growth buckets + (`138` rows), and the only nonzero companion-byte residue is `3` `TextileMill` rows + - the create-side family is grounded separately too: + city-connection direct placement already reaches + `0x00402cb0 -> 0x00403ed5/0x0040446b -> 0x004134d0 -> 0x0040ef10` + as the shared constructor/finalize strip for newly created site rows + - so the acquisition blocker is no longer “is there any real consumer family above these + lanes?”, “how do new placed structures finalize?”, “is `[site+0x2a4]` still missing?”, or + “does the tri-lane even have live writers?”; it is now specifically the persisted inputs and + exact shellless service semantics above the grounded live tri-lane scorer family, plus the + save-native or replay owner that populates `[site+0x276]` for already-restored rows before + shellless acquisition runs + - direct disassembly now shows the generic base constructor `0x0052edf0` clearing base state + through `0x0052ecd0` and then writing `[this+0x04]` from caller arg `1` + - `0x00455b70` is the concrete placed-structure specialization constructor feeding + `0x0052edf0`, with arg `3` as the primary selector and arg `1` as fallback + - `0x00455c62` is the direct in-body call from that specialization constructor into + `0x0052edf0` + - `0x00456100` is a local wrapper that duplicates its first incoming arg across the + selector/fallback bundle before calling `0x00455b70` + - `0x00456072` is a fixed `0x55f2` callback that forwards three local dwords plus unit scalars + into `0x00455b70` + - `0x0045c36e / 0x0045da65 / 0x0045e0fc` are concrete callers of `0x00456100`, repeatedly + allocating `0x23a` rows, forwarding stack-backed buffers, and using the same default scalar + lanes + - the `0x00456100 -> 0x00455b70` wrapper mapping is now grounded far enough to say the sampled + selector-source lanes are `[owner+0x23e]` at `0x0045c36e`, literal zero at `0x0045da65`, and + `[ebp+0x08]` at `0x0045e0fc` + - direct disassembly now shows `0x0045c150` as a save-backed loader for `[owner+0x23e/+0x242]`: + it zeroes those fields, runs the shared tagged loader `0x00455fc0`, reads tagged payload + `0x5dc1`, and copies the two recovered lanes into `[owner+0x23e/+0x242]` before + `0x0045c310 -> 0x0045c36e` later feeds `[owner+0x23e]` into `0x00456100` + - the local linked-site helper neighborhood now reaches that same owner strip directly: + `0x0040ceab` calls `0x0045c150`, and `0x0040d1a1` jumps straight into `0x0045c310` + - `0x00485819` is one typed placed-structure caller of `0x0052edf0` through the generic + three-arg wrapper `0x00530640` + - `0x00490a79` is one chooser-side caller of `0x00455b70`, feeding literal selector + `0x005cfd74` with fallback seed `0x005c87a8` + - the periodic-company trace now also surfaces the save-side `0x5dc1` payload/status summaries + already parsed from the `0x36b1` triplet seam; on grounded `p.gms` the payload dword lane is + almost entirely unique while the status kind stays `unset`, and the dominant adjacent payload + delta is `0x00000780` across `1908` steps; grounded `q.gms` shows the same dominant adjacent + delta `0x00000780` across `1868` steps + - the same trace now also promotes the one-byte `0x5dc1` post-secondary discriminator explicitly: + grounded + `p.gms` shows dominant companion byte `0x00` on `2023` rows with only `3` `0x01` rows, and + grounded `q.gms` shows dominant companion byte `0x00` on `2043` rows with only `14` `0x01` + rows; the old “pre-footer padding” hypothesis is now better understood as a separate + post-secondary discriminator byte after the repeated secondary payload string, not as the + `[owner+0x242]` field itself + So the next owner question is no longer “what does the acquisition branch do?” or “which post- + load owner replays linked-site refresh?” but “which concrete `0x00455b70` caller family applies + to the live site rows, and which persisted lane becomes the selector bundle that ultimately + seeds `[site+0x04]`?” The current strongest restore-family hypothesis is now the save-backed + `0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70` strip. +- Make the next static/rehost slice the peer-site rebuild seam above persistence, not another save + scan: + - treat `0x00444690 -> 0x004133b0 -> 0x0040e450 / 0x0040ee10 -> 0x0040edf6 -> 0x00480710` as + the checked-in bring-up replay path for existing saves + - treat `0x004160aa -> 0x0040ee10` as the checked-in recurring runtime maintenance entry into the + same linked-peer refresh strip + - treat `0x0052edf0` as the checked-in field owner for `[site+0x04]` + - treat `0x00455b70` as the checked-in specialization constructor that maps selector/fallback + bundle lanes into that field owner + - distinguish which `0x00455b70` caller family actually seeds the live site rows before + `0x00420030 / 0x00420280 / 0x0047efe0 / 0x0047fd50` consume the resulting selector, with the + current first target being the save-backed + `0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100` family + - use the structured periodic-company trace selector fields now checked into + `inspect-periodic-company-service-trace`: owner strip + `0x0045c150 -> 0x0045c310 -> 0x0045c36e -> 0x00456100 -> 0x00455b70`, persisted tag + `0x5dc1`, selector lane `[owner+0x23e]`, class-identity status + `grounded_direct_local_helper_strip`, and helper linkage + `0x0040ceab -> 0x0045c150` / `0x0040d1a1 -> 0x0045c310` / + `0x0040cd70 seeds [site+0x3cc/+0x3d0] from 0x62b2fc / 0x62b268` + - use the new `0x5dc1` payload/status summary in the same trace as negative evidence too: + the current `profile_payload_dword` lane behaves like a save-invariant monotone ladder + (`dominant adjacent delta 0x780` on both `p.gms` and `q.gms`) rather than a compact selector + family, so the next peer-site slice should treat that raw dword as a likely allocator/offset + lane until a stronger selector interpretation appears + - use the new `0x5dc1` post-secondary-byte summary in the same trace as positive evidence: + that byte is overwhelmingly `0x00` with a tiny `0x01` residue on both grounded saves, so the + next peer-site slice should treat it as a real typed discriminator after the restored + `[owner+0x23e]` / `[owner+0x242]` payload strings and ask which later `0x004014b0` / + `0x00406050` predicates actually consume it + - use the new nonzero-companion name-pair summary in the same trace as a narrower acquisition + clue too: grounded `p.gms` exposes only `TextileMill/TextileMill x3`, while grounded `q.gms` + exposes `TextileMill x9`, `Toolndie x2`, and singleton `Brewery`, `MeatPackingPlant`, and + `MunitionsFactory` rows, so the next peer-site slice should treat nonzero post-secondary-byte + rows as a likely industry-like subset rather than a generic placed-structure mode split + - that bridge is explicit now too: the periodic trace can classify those nonzero companion + names against the recovered stock Tier-2 building family, and the current grounded overlap set + is `TextileMill`, `Toolndie`, `Brewery`, and `MeatPackingPlant` while + `MunitionsFactory` remains the clear current residue outside the recovered + `0x000001f4` stock header family + - the checked-in building-source summary now says which stock header lane is actually carrying + the shared alias roots too: within that recovered nonzero `0x000001f4` family, + `name_0x40` / `name_0x7c` mostly stay on direct file/display roots (`Warehouse x7` plus + singletons such as `Brewery`, `Port`, and `Toolndie`), while `name_0x5e` is the real + clustered alias-root lane (`TextileMill x10`, `LumberMill x4`, `MeatPackingPlant x4`, + `Distillery x2`, `Toolndie x2`). So the next Tier-2 source-selection pass should treat + `0x5e`-style alias roots as the stronger stock-family clue than the direct-name lanes + - the trace now keeps the explicit non-overlap residue first-class too: the current list outside + that recovered nonzero family is just `MunitionsFactory/MunitionsFactory x1`, so the next + chooser-side/source-selection slice can focus on whether that residue belongs to a zero-valued + stock-header family or to a later live projection seam rather than treating the whole nonzero + post-secondary set as one undifferentiated mystery + - that broader stock-header check is now grounded too: the checked-in `name_0x5e` dword-family + summary shows `MunitionsFactory` is not outside stock assets at all. It sits in a zero-valued + `WeaponsFactory` alias cluster (`Electric Plant`, `Fertilizer Factory`, `Munitions Factory`, + `Nuclear Power Plant`, `Oil Well`, `Weapons Factory`) while the recovered nonzero family keeps + its own `TextileMill`, `LumberMill`, `MeatPackingPlant`, `Distillery`, and `Toolndie` + clusters. So the next Tier-2 source-selection question is no longer “stock vs non-stock”; it + is which stock alias-root cluster gets selected, and why some later clone/replay paths prefer + the nonzero `0x000001f4` cluster while the peer-site residue can still surface a zero-family + `WeaponsFactory`-side root + - the stock-cluster-to-selector join is explicit now too: the checked-in `name_0x5e` + + `.bca` selector summary shows every grounded alias cluster is zero-selector by default, + including the nonzero `0x000001f4` clusters (`TextileMill x9`, `LumberMill x4`, + `MeatPackingPlant x4`, `Distillery x2`, `Toolndie x2`) and the zero-family + `WeaponsFactory x6` cluster. The only surfaced nonzero joined outlier is + `MachineShop` inside the nonzero `TextileMill` cluster (`byte_0xba = 0x3f`, `byte_0xbb = 0x00`). + So the next Tier-2 source-selection pass should no longer ask whether whole alias clusters map + to nonzero bank bytes; it should ask why one specific stock row inside the `TextileMill` + cluster surfaces a nonzero selector while its peer rows stay zero + - the direct-name plus alias plus selector join narrows that one step further: + inside the same nonzero `0x000001f4` family, the direct + `Warehouse/TextileMill/Warehouse` shape splits into six all-zero selector peers + (`ConcretePlant`, `ConstructionFirm`, `ElectronicsPlant`, `Hospital`, + `PharmaceuticalPlant`, and `Warehouse`) plus one unique selector outlier, + `MachineShop = 0x00/0x80/0x3f/0x00`. The sibling bare `Port/TextileMill/Port` row stays + all-zero. So the remaining Tier-2 question is no longer “does the bare `Port` or + `Warehouse` row carry the seeded selector?”; it is “why does one warehouse-shaped industrial + peer in that alias family carry the lone seeded selector while the bare rows do not?” + - the exact stock resolver-family strip is boxed in now too: + the checked-in `recovered_source_family_summaries` report shows every `0x00419590` + source-family row (`VictorianStation*`, `TudorStation*`, `SoWestStation*`, + `PersianStation*`, `KyotoStation*`, `ClpBrdStation*`, `Maintenance`, and `ServiceTower`) + stays on `type_id = 0x000003ec` with `dword_0xbb = 0`, and almost all of them have no `.bca` + pair at all; the only paired standalone row is `ServiceTower`, and it still carries + `byte_0xba = 0x00`, `byte_0xbb = 0x00`. So the remaining Tier-2 question is no longer + whether the exact `0x00419590` source-family strip itself carries the seeded nonzero bank + bytes; current evidence says it does not. + - the `0x00419590` caller surface is boxed in further too: + current direct caller recovery still keeps the real load-side use under the already-grounded + `0x00419230` rebank-or-clone strip, while the only newly surfaced non-local caller is + `0x00506424`, which reaches `0x00419590` from a live placed-structure consumer path that + immediately flows through `0x00402c90` and `0x0040dc40`. So the remaining Tier-2 question is + no longer whether `0x00419590` itself is a hidden load-side owner; it is still the earlier + seed-row or projection seam that makes later clone/consumer paths see nonzero bank bytes. + - the global stock `.bca` selector report narrows that one step further still: the exact + `MachineShop.bca` signature (`byte_0xb8 = 0x00`, `byte_0xb9 = 0x80`, `byte_0xba = 0x3f`, + `byte_0xbb = 0x00`) is unique across the checked-in stock corpus. So the current Tier-2 + frontier is not a broad hidden family of nonzero stock rows; it is a single surfaced stock-file + outlier plus whatever later clone/replay logic amplifies it into the numbered banked rows + - keep the already-grounded `0x0047fd50` class gate separate from that byte: direct disassembly + now says `0x0047fd50` resolves the linked peer through `[site+0x04]`, reads candidate class + byte `[candidate+0x8c]`, and returns true only for `0/1/2` while rejecting `3/4` and above, + so the next slice should not conflate the post-secondary byte with the existing + station-or-transit gate + - treat the peer-site selector seam itself as grounded enough for planning purposes + - use the new structured restore/runtime field split in the same trace: + restore subset + `[site+0x3cc/+0x3d0]` plus `0x5dc1`-backed `[owner+0x23e/+0x242]`, + and runtime subset + `[site+0x04]`, `[site+0x2a8]`, `[peer+0x08]` + - use the new structured reconstruction status in the same trace: + `restore_subset_and_bring_up_reconstruct_runtime_subset` + - treat the runtime subset as reconstructible from the restore subset plus the already-grounded + bring-up path for planning purposes + - use the new structured acquisition input families in the same trace: + region subset + `[region+0x276]`, region vtable `+0x80` byte-`0x32`, `[region+0x3d5]`, + `[region+0x310/+0x338/+0x360]`, `[region+0x2a4]`; + peer subset + center-cell token gate, `[site+0x04]`, `[site+0x2a8]`, `[peer+0x08]`, linked-region status; + company subset + stat-family reader `0x2329/0x0d`, chairman byte `[profile+0x291]`, company byte `[company+0x5b]` + plus indexed lane `[company+0x67 + 12*0x0042a0e0()]`, and the company-root argument passed into + `0x0040d540 / 0x00455f60` + - use the new shellless-readiness split in the same trace: + runtime-backed families + peer-site restore subset plus bring-up reconstruction, company stat-family `0x2329/0x0d`, + chairman byte `[profile+0x291]`, and save-native company/chairman identity; + remaining owner gaps + `[region+0x276]`, `[region+0x2a4]`, `[region+0x310/+0x338/+0x360]`, and the stable region + class/type discriminator consumed through `0x0040d360` + - use the new per-lane region status split in the same trace: + `[region+0x276]` already has a grounded runtime producer at `0x00422100` and is now only an + ordinary-save restore gap; + `[region+0x2a4]` currently has no region-class runtime writer in the binary scan and now looks + payload/restore-owned; + `[region+0x310/+0x338/+0x360]` has an exact raw delta reader at `0x0040cac0` and likewise no + direct region-class runtime writer in the current binary scan, so it now also looks + payload/restore-owned; + `0x0040d360` is now exact as `[owner_vtable+0x80+0x32] == 4`, so the remaining gap there is + only the save-native projection of that byte + - make the next periodic-company slice about the smaller shellless-simulation question instead: + which later save payload or restore owner rehydrates the remaining region-side `0x004014b0` + inputs `[region+0x2a4]` and `[region+0x310/+0x338/+0x360]` once the peer/company inputs are + treated as grounded and `[region+0x276]` is treated as a producer-known ordinary-save restore + gap +- Use the higher-layer probes as the standard entry point for the current blocked frontier instead + of generic save scans: + `runtime inspect-periodic-company-service-trace `, + `runtime inspect-region-service-trace `, and + `runtime inspect-infrastructure-asset-trace `. +- Follow the new higher-layer probe outputs instead of another blind save scan: + `runtime inspect-infrastructure-asset-trace ` now shows that the `0x38a5` + infrastructure-asset seam is grounded and the old alias hypothesis is disproved on `q.gms`, so + the next placed-structure slice should target the consumer mapping above that seam rather than + more collection discovery; the same trace now also carries atlas-backed candidate consumers + (`0x0048a1e0`, `0x0048dd50`, `0x00490a3c`, `0x004559d0`, `0x00455870`, `0x00455930`, + `0x00448a70/0x00493660/0x0048b660`, `0x004133b0`) plus bridge/tunnel/track-cap name-family + counts, so the next pass can start at those concrete owners instead of the whole + placed-structure family. +- Rehost or bound the next concrete `Infrastructure` consumer above `0x38a5` instead of treating + “consumer mapping missing” as a stop: start with the checked-in candidate strip + `0x0048a1e0`, `0x0048dd50`, `0x00490a3c`, `0x004559d0`, `0x00455870`, `0x00455930`, + `0x00448a70/0x00493660/0x0048b660`, `0x004133b0`, and narrow that list to the first true + shellless owner that consumes the side-buffer seam. The infrastructure trace now ranks the + current best hypothesis as the child attach/rebuild strip + (`0x0048a1e0`, `0x0048dd50`, `0x00490a3c`), with the serializer/load companions next and the + route/local-runtime follow-on family explicitly secondary. +- For that top-ranked infrastructure strip, treat the next pass as three exact owner questions + rather than a general “map the consumer” task: whether the `0x38a5` compact-prefix/name-pair + groups feed the first-child triplet clone lane, the caller-supplied payload-stem lane, or only a + later route/local-runtime refresh lane; which child fields or grouped rows absorb the + side-buffer payload before `0x00448a70/0x00493660/0x0048b660` become relevant; and, now that the + direct route-entry bridge helpers over `[this+0x206/+0x20a/+0x20e]` are grounded, which later + route/local-runtime owner still carries the remaining mixed exact classes once cached + primary-child slot `[this+0x248]` is demoted to child-list cache/cleanup state. +- Targeted disassembly now tightens that strip further: `0x0048a1e0` clones the first child through + `0x0052e880/0x0052e720`, destroys the prior child, seeds a literal `Infrastructure` child + through `0x00455b70` with payload seed `0x005c87a8`, and republishes the two sampled bands + through `0x0052e8b0/0x00530720` after attaching through `0x005395d0`; the non-clone branch + attaches through `0x0053a5d0`. So the next unknown is no longer whether this strip owns the + child/rebuild seam, but which `0x38a5` compact-prefix groups drive the clone-vs-payload choice. +- The outer rebuild owner is tighter now too: `0x0048dcf0` reads a child count plus optional + primary-child ordinal from the tagged stream through `0x00531150`, zeroes `[this+0x08]`, + dispatches each fresh child through `0x00455a50 -> vtable slot +0x40`, culls ordinals above `5`, + and restores cached primary-child slot `[this+0x248]` from the saved ordinal. That means the + child/rebuild loop is consuming an already-materialized child stream rather than parsing the + `0x38a5` compact-prefix seam directly. +- The upstream handoff is grounded now too: `0x00493be0` is the tagged collection load owner over + `0x38a5/0x38a6/0x38a7`, and it feeds each live infrastructure record straight into + `0x0048dcf0` after restoring one shared owner-local dword into the `0x90/0x94` lane. So the + remaining infrastructure question is no longer whether `0x38a5` reaches the child-stream restore + path at all. Direct disassembly now also shows `0x00518140` resolving a non-direct live entry by + tombstone bitset and then returning the first dword of a `12`-byte row from `[collection+0x3c]`, + while `0x00518680` loads that non-direct table family before `0x00493be0` starts iterating, and + `0x00493be0` itself now reads as an ordinal-to-live-id-to-payload-pointer walk through + `0x00518380(ordinal, 0)` then `0x00518140(live_id)`. So the next infrastructure question is no + longer “which row owns the payload pointer?”. Direct disassembly of `0x005181f0/0x00518260` now + also treats those `12`-byte rows as a live-entry directory with + `(payload pointer, previous live id, next live id)`, so the next infrastructure question is only + how those payload streams align with the embedded `0x55f1` name-pair groups and compact-prefix + regimes, and which tagged values inside each payload stream become the child count, optional + primary-child ordinal, and per-child callback sequence that `0x0048dcf0` consumes. Direct + disassembly now also shows the shared child payload callback `0x00455fc0` opening + `0x55f1 -> 0x55f2 -> 0x55f3`, parsing three `0x55f1` strings through `0x00531380`, seeding the + child through `0x00455b70`, and then dispatching slot `+0x48`; the widened save-side probe + currently sees `0` third `0x55f1` strings on grounded `q.gms`. That now looks less like a probe + failure and more like an ordinary fallback path, because direct disassembly of `0x00455b70` + stores the three payload strings into `[this+0x206/+0x20a/+0x20e]`, defaulting the second lane + through a fixed literal when absent and defaulting the third lane back to the first string when + absent. So the next pass should stay focused on payload-stream grouping and tagged value roles, + not on rediscovering a missing third-string encoding. +- The child loader identity is tighter now too: local `.rdata` at `0x005cfd00` proves the + `Infrastructure` child vtable uses the shared tagged callback strip directly, with + `+0x40 = 0x00455fc0`, `+0x44 = 0x004559d0`, `+0x48 = 0x00455870`, and `+0x4c = 0x00455930`. + Direct disassembly of `0x004559d0` then shows the concrete write-side chain for the child + payload: write `0x55f1`, serialize string lanes `[this+0x206/+0x20a/+0x20e]`, write `0x55f2`, + dispatch slot `+0x4c`, run `0x0052ec50`, and close `0x55f3`. So the remaining infrastructure + frontier is no longer “which slot does `0x00455a40` jump to?”; it is which chooser/seed values + reach those string lanes and the trailing footer path. +- That source side is narrower now too: direct disassembly shows the paired chooser siblings + calling `0x00490960` directly beside `0x0048a340/0x0048f4c0/0x00490200`, and `0x00490960` + copies selector fields into the child object (`[this+0x219]`, `[this+0x251]`, bit `0x20` in + `[this+0x24c]`, and `[this+0x226]`), allocates a fresh `0x23a` `Infrastructure` child, seeds it + through `0x00455b70` with caller-supplied stem input plus fixed literal `Infrastructure` at + `0x005cfd74`, attaches it through `0x005395d0`, seeds position lanes through + `0x00539530/0x0053a5b0`, and can cache it as primary child in `[this+0x248]`. The remaining + problem is no longer “where do the child payload lanes come from?” but “which chooser branches + feed `0x00490960` which caller stem and selector tuple for each grounded save-side class?”. +- One direct branch is grounded now too: the repeated chooser calls at + `0x004a2eba/0x004a30f9/0x004a339c` all feed `0x00490960` with mode arg `0x0a` and stem arg + `0x005cb138 = BallastCapDT_Cap.3dp`, which means they bypass the selector-copy block at + `0x004909e2` and go straight into fresh child allocation/seeding. So the remaining source-side + mapping problem is no longer generic BallastCap coverage; it is the other constructor branches, + especially the ones with mode `< 4` that actually populate the selector-byte copy block. +- The broader mode family is grounded now too. A wider static callsite sweep shows: + - mode `0x0b` with fixed `TrackCapDT_Cap.3dp` / `TrackCapST_Cap.3dp` + - mode `0x03` with `OverpassST_section.3dp` + - mode `0x02` with decoded tunnel table stems plus zero-stem fallbacks + - mode `0x01` with decoded bridge table stems plus zero-stem fallbacks + + The current grounded `q.gms` name corpus now also maps directly onto most of those families: + `BridgeSTWood_Section.3dp -> mode 0x01`, `TunnelSTBrick_* -> mode 0x02`, + `BallastCapST_Cap.3dp -> mode 0x0a`, and `TrackCapST_Cap.3dp -> mode 0x0b`, with only + `Overpass` still static-only in the current save corpus. + + So the remaining infrastructure question is no longer “what does `0x00490960` build?” or even + “which family is this name row?” but “how do the surviving compact-prefix regimes subdivide + those already-mapped families, especially inside bridge mode `0x01` and track-cap mode `0x0b`?”. +- The direct route-side bridge is grounded now too: `0x0048e140/0x0048e160/0x0048e180` simply + resolve `[this+0x206/+0x20a/+0x20e]` through live route collection `0x006cfca8`, and + `0x0048e1a0` compares those resolved peers against `[this+0x202]`. The neighboring + `0x0048ed30` path is now also narrower: it only tears down child list `[this+0x08]`, clearing + cached primary-child slot `[this+0x248]` when needed, so `[this+0x248]` is no longer the first + route bridge to chase. +- The later route/local-runtime follow-on family is tighter now too: `0x00448a70` is a + world-overlay helper over `[world+0x15e1/+0x162d]`, `0x00493660` is a counter-plus-companion- + region follow-on keyed by `[child+0x218]`, `[child+0x226]`, `[child+0x44]`, and `0x0048dcb0`, + `0x0048b660` is a presentation-color/style owner over `[child+0x216/+0x218/+0x226/+0x44]` and + bit `0x40` in `[child+0x201]`, and `0x0048e2c0/0x0048e330/0x0048e3c0` now read as flag / route- + tracker / region-test helpers rather than hidden payload decoders. So the next infrastructure + slice should stay focused on the remaining mixed exact compact-prefix classes and earlier + child-stream semantics, not on rediscovering the already-bounded presentation owners. +- The new probe correlation now makes that residual even more concrete: on grounded `q.gms`, the + dominant mixed `0x0001/0xff` class splits as `bridge:62 / track_cap:21 / tunnel:19`, while the + pure `0x0002/0xff` class is all bridge and the pure `0x0055/0x00` class is all ballast-cap. + So the next infrastructure slice should focus on subdividing the mixed one-child `0x0001/0xff` + class rather than revisiting the already-grounded pure classes. +- The sibling `0x00490200` is tighter now too: it reads the seeded lanes + `[this+0x206/+0x20a/+0x20e]` back through the live route collection at `0x006cfca8`, compares + them against the current owner using `[this+0x216/+0x218/+0x201/+0x202]`, and behaves like a + route/link comparator layered above the same child payload lanes that `0x004559d0` later + serializes. So the next infrastructure pass should treat `0x00490960` as the source owner and + `0x00490200` as a consumer of the same seeded lanes, not as separate unexplained seams. +- The smaller helper `0x00490a3c` is narrower now too: it allocates one literal `Infrastructure` + child, seeds it through `0x00455b70` with caller-provided stem input, attaches it through + `0x005395d0`, seeds position lanes through `0x00539530/0x0053a5b0`, and optionally caches it as + the primary child. So the next concrete infrastructure question is which upstream owner + maps the direct `0x38a5` rows into the child count, primary-child ordinal, and per-child payload + callbacks consumed by `0x0048dcf0`, and which restored child fields still retain those embedded + name-pair semantics before route/local-runtime follow-ons take over. +- The save-side `0x38a5` probe is now tighter at the payload-envelope level too: grounded + `q.gms` shows all `138` embedded `0x55f1` rows already live inside complete + `0x55f1 -> 0x55f2 -> 0x55f3` envelopes before the next name row, every embedded `0x55f2` chunk + is the fixed `0x1a` bytes that `0x00455fc0` expects, and the dominant embedded `0x55f3` + payload-to-next-name span is the short `0x06`-byte form across `72` rows. So the next + infrastructure pass should stop asking whether the shared tagged callback sequence is present at + all and instead decode the short `0x55f3` payload role and its relation to the compact-prefix + regimes and primary-child restore path. +- That short trailing lane is tighter now too: direct disassembly of `0x0052ebd0/0x0052ec50` + shows the post-`+0x48` helper pair loading and serializing two single-byte lanes that fold into + bits `0x20` and `0x40` of `[this+0x20]`, and the save-side probe now shows the dominant + `0x06`-byte rows all carrying the same grounded flag pair `0x00/0x00` on `q.gms`. So the next + concrete infrastructure question is no longer “is there a short trailing flag lane?”; it is how + the compact-prefix regimes and those flag-byte pairs feed the child-count / primary-child restore + state above `0x0048dcf0`. +- The fixed `0x55f2` lane is tighter now too: direct disassembly of `0x00455870/0x00455930` shows + the `+0x48/+0x4c` strip loading and serializing six `u32` lanes from the fixed `0x1a` chunk, + forwarding them through `0x00530720` and `0x0052e8b0`. Grounded `q.gms` probes now show every + embedded `0x55f2` row using the same trailing word `0x0101` while those six dword lanes vary by + asset row. So the next infrastructure question is no longer whether `0x55f2` is a fixed-format + child lane; it is which of those two dword triplets correspond to child-count / primary-child + restore state and which only seed published anchor or position bands. +- That split is tighter now too: direct disassembly of `0x00530720/0x0052e8b0` shows the first + fixed `0x55f2` triplet landing in `[this+0x1e2/+0x1e6/+0x1ea]` and the second in + `[this+0x4b/+0x4f/+0x53]`, with the companion setter also forcing bit `0x02`. So the next + infrastructure question is no longer whether the fixed `0x55f2` row hides the child count or + primary-child ordinal at all; those outer-header values now have to live outside the fixed row, + most likely in the surrounding payload-stream header or compact-prefix regime above + `0x0048dcf0`. +- The outer prelude itself is tighter now too: direct disassembly of `0x0048dcf0` shows it reading + one `u16` child count through `0x00531150`, zeroing `[this+0x08]`, and conditionally reading one + saved primary-child byte before the per-child callback loop runs. Grounded `q.gms` bytes now also + show the first `0x38a6` record starting immediately after the shared owner-local dword with + `child_count = 1`, `saved_primary_child_byte = 0xff`, and the first child `0x55f1` opening at + offset `+0x3`. So the next infrastructure question is no longer “what kind of values are we + looking for above the fixed rows?”; it is the narrower partitioning problem of how the observed + `0x55f3`-to-next-`0x55f1` gaps divide between the two `0x52ebd0` flag bytes and the next + record’s `u16 + byte` prelude. +- The widened prelude correlation closes part of that partitioning too: grounded `q.gms` rows with + a `0x03` post-profile gap now collapse cleanly to the next-record prelude pattern + `0x0001 / 0xff` across `17/17` rows, while the zero-length class is a separate grounded outlier + with dominant pattern `0x0055 / 0x00` across `18/18` rows and the `0x06` class remains the only + large mixed frontier. So the next infrastructure slice should focus on classifying the mixed + `0x06` rows, not on rediscovering the already-grounded pure-prelude `0x03` rows. +- That `0x06` class is now narrower too: grounded `q.gms` shows the dominant short-span class as + `BridgeSTWood_Section.3dp / Infrastructure` with compact prefix `0xff000000 / 0x0001 / 0xff` + across `62/72` rows and dominant prelude candidate `0x0001 / 0xff` across `63/72` rows. So the + next infrastructure slice should stop treating the `0x06` class as uniformly ambiguous and focus + on the smaller outlier families inside that class, especially the zero-like `BallastCap`-style + rows and any remaining non-`0x0001 / 0xff` prelude candidates. +- The exact compact-prefix classes are explicit across the whole prelude now too: + `0xff0000ff / 0x0002 / 0xff` is a pure bridge class, `0xff000000 / {0x0001,0x0002} / 0xff` + are pure bridge classes, `0xf3010100 / 0x0055 / 0x00` is a pure `BallastCap` class, and + `0x0005d368 / 0x0001 / 0xff` is a pure one-row `TrackCap` class. +- That sharpens the remaining infrastructure unknowns considerably: the only mixed exact + compact-prefix classes left on grounded `q.gms` are `0x000055f3 / 0x0001 / 0xff` and + `0xff0000ff / 0x0001 / 0xff`. +- The current `0x000055f3 / 0x0001 / 0xff` class is tunnel-dominant: + `TunnelSTBrick_Section.3dp / Infrastructure:13`, `TunnelSTBrick_Cap.3dp / Infrastructure:4`, + `TrackCapST_Cap.3dp / Infrastructure:0` in the exact-prefix correlation, with all `17` rows + staying on prior profile span `0x03`. +- The current `0xff0000ff / 0x0001 / 0xff` class is `TrackCap`-dominant but still carries `4` + tunnel rows: + `TrackCapST_Cap.3dp / Infrastructure:18`, + `TunnelSTBrick_Cap.3dp / Infrastructure:2`, + `TunnelSTBrick_Section.3dp / Infrastructure:2`. + Its rows are spread across many spans rather than one dominant restore span. +- Cross-save `q.gms` / `p.gms` traces sharpen that split further without changing it: + `0x000055f3 / 0x0001 / 0xff` stays on prelude `0x0001 / 0xff`, fixed short-flag pair + `0x01 / 0x00`, and fixed prior profile span `0x03` in both saves, while + `0xff0000ff / 0x0001 / 0xff` stays on prelude `0x0001 / 0xff`, fixed short-flag pair + `0x00 / 0x00`, and widely scattered prior profile spans in both saves. +- Direct consumers of those footer bits are grounded now too: `0x00528d90` only admits the child + when the explicit caller override is set, the surrounding global override byte + `[owner+0x3692]` is set, or bit `0x20` in `[child+0x20]` is set; the sibling loop + `0x00529730` only takes the later `0x530280` follow-on when bit `0x40` in `[child+0x20]` is + set. +- That footer-bit consumer strip is tied to a broader higher-layer owner family now too: + `0x005295f0..0x005297b7` repopulates candidate cells through `0x00533ba0`, walks candidate + child lists through `0x00556ef0/0x00556f00`, and honors the same controller mode byte + `[owner+0x3692]` that the atlas already places under the world-window presentation dispatcher. +- The neighboring helpers tighten that owner family further: atlas-backed `0x00533ba0` is the + nearby-presentation cell-table helper under the layout/presenter strip, direct disassembly shows + `0x00548da0` walking layout list root `[layout+0x2593]`, and direct disassembly of `0x0054bab0` + mutates layout slots `[layout+0x2637/+0x263b/+0x2643]`. +- That means the remaining infrastructure question is no longer both footer bytes. It is + specifically why the stable `0x000055f3 / 0x0001 / 0xff` tunnel family sets the first footer + byte / bit-`0x20` admission gate while the sparse `0xff0000ff / 0x0001 / 0xff` outlier class + clears it, inside that layout/presentation owner family rather than at the serializer layer. +- Source-side constructor analysis is narrower now too. `0x00490960` takes: + - mode at stack arg 1 + - stem at stack arg 2 + - args 3/4 into `0x539530` + - arg 5 into `0x53a5b0` + - arg 10 as the primary-child cache gate for `[this+0x248]` + - args 7/8/9 into the selector-copy block for `[this+0x219]`, `[this+0x251]`, and bit `0x20` + in `[this+0x24c]` when `mode < 4` +- That already separates the remaining mixed classes: + - fixed `TrackCap` mode `0x0b` callers at `0x0048ed01/0x0048ed20` push arg7/arg8/arg9 as + `-1 / -1 / 0` and bypass selector-copy entirely because `mode >= 4` + - tunnel mode `0x02` callers at + `0x004a17eb / 0x004a1995 / 0x004a1b44 / 0x004a1b7d / 0x004a1b95` + necessarily flow through selector-copy because `mode < 4`, with arg8 fixed at `1`, arg9 + fixed at `0`, and only arg7 varying through a branch-local one-bit register +- So the next infrastructure slice should stop treating the remaining frontier as a generic + “mixed 0x06/outlier” problem and instead target the owning constructor/restore semantics for + those two exact mixed compact-prefix classes, especially how tunnel arg7 and the fixed + `TrackCap` no-selector bundle both still collapse into the observed mixed save-side prefixes. +- The candidate-pattern classes are now explicit across the whole stream too: `0x0055 / 0x00` + is a pure `BallastCapST_Cap.3dp / Infrastructure` class across `18` rows, always preceded by a + zero-length prior profile span, while `0x0002 / 0xff` is a pure + `BridgeSTWood_Section.3dp / Infrastructure` class across `18` rows with dominant prior profile + span `0x06` (`10` rows). So the next infrastructure pass should split its owner questions: + treat `0x0055 / 0x00` as a `BallastCap`-specific boundary artifact class, and treat + `0x0002 / 0xff` as the grounded save-side bridge-specific two-child candidate class above + `0x0048a1e0/0x0048dcf0`, with the remaining unknown narrowed to the upstream chooser that emits + that class before the attach/rebuild path runs. +- That upstream chooser is grounded now too as paired siblings: direct disassembly shows + `0x004a2c80` routing the `DT` family and `0x004a34e0` routing the `ST` family, with both + repeatedly calling `0x0048a1e0`, branching on `[this+0x226]`, selector bytes + `[this+0x219]/[this+0x251]`, bit `0x20` in `[this+0x24c]`, and lookup tables `0x621a44..0x621a9c`, + then routing follow-on through `0x0048a340/0x0048f4c0/0x00490200/0x00490960`. So the remaining + infrastructure question is no longer “is there an upstream chooser?” but “how do the save-side + classes select the `DT` versus `ST` chooser sibling, and then which lookup-table families inside + that sibling map to the grounded `0x0002 / 0xff` bridge class and the `0x0055 / 0x00` + BallastCap class?”. +- Those lookup tables are decoded now too: `0x621a44/0x621a54` feed `BridgeST` caps/sections, + `0x621a64` feeds `TunnelST` cap/section variants, `0x621a74/0x621a84` feed `BridgeDT` + caps/sections, and `0x621a94` feeds `TunnelDT` variants, while fixed literals + `0x5cb138/0x5cb150` are `BallastCapDT/ST` and `0x5cb168/0x5cb180` are `OverpassDT/ST`. So the + remaining infrastructure question is no longer table discovery; it is the selector-byte mapping + from `[this+0x219]/[this+0x251]/[this+0x252]` onto those decoded families and then onto the + grounded `0x38a5` prefix classes. +- The top-level chooser meaning is grounded now too: within those paired DT/ST siblings, + `[this+0x226]==1` routes the bridge families, `[this+0x226]==2` routes the tunnel families, and + `[this+0x226]==3` routes the overpass/ballast family, while bit `0x20` in `[this+0x24c]` + selects the cap-oriented side over the section-oriented side. So the remaining infrastructure + selector problem is below that top-level split: the exact `[this+0x219]/[this+0x251]` values + that choose the decoded family entries and how those values surface in the save-side `0x38a5` + classes. +- Those material selectors are grounded now too: within the bridge branch, `[this+0x219]` + selects `steel`, `stone`, `suspension`, or `wood`, with value `2` taking the special + suspension-cap path through `[this+0x252]`; within the tunnel branch, `[this+0x251]` selects + `brick` versus `concrete`, while bit `0x20` chooses cap versus section by switching between the + base and `+0x8` table entry families. So the remaining infrastructure selector problem is no + longer “what do these bytes mean?” but “how do those already-grounded selector values surface in + the save-side `0x38a5` classes, especially the `0x0002 / 0xff` bridge class and the + `0x0055 / 0x00` BallastCap class?”. +- The exact setter seam is grounded now too: direct disassembly of `0x0048a340` shows its dword + argument writing `[this+0x226]`, its next two byte arguments writing `[this+0x219]` and + `[this+0x251]`, and its final byte argument toggling bit `0x20` in `[this+0x24c]`. So the + remaining infrastructure selector problem is no longer about hidden intermediate state; it is + specifically how those already-grounded setter values are serialized or rebuilt into the + save-side `0x38a5` prefix classes. +- One selector byte is partly grounded now too: when `[this+0x219]==2`, the chooser jump tables + stop using the general bridge families and instead route `[this+0x252]` through fixed + `BridgeDT/BridgeST` suspension-cap literals for `R10`, `L10`, `12`, `14`, `16`, and `18`. + So the remaining infrastructure selector problem is mostly `[this+0x219]/[this+0x251]` family + choice plus the exact save-side class mapping for the BallastCap branch. +- The current real-save corpus also narrows the active side further: grounded `q.gms`, `p.gms`, + `g.gms`, and `nom.gms` only expose `ST`-family side-buffer names, while classic `rt3/` saves in + this workspace currently expose no `0x38a5` side-buffer seam at all. So the save-driven part of + the next infrastructure slice should assume it is exercising the `ST` chooser sibling directly, + with `DT` still grounded statically but not yet exercised by the current save corpus. +- Reconstruct the save-side region record body on top of the newly corrected non-direct tagged + region seam (`0x5209/0x520a/0x520b`, stride hint `0x06`, `Marker09` record stems) now that the + `0x55f3` payload is known to be fully consumed by the embedded profile collection on grounded + real saves: the remaining blocker is no longer a hidden trailing payload tail, but finding the + separate save-owner seam for the pending bonus lane `[region+0x276]`, completion latch + `[region+0x302]`, one-shot notice latch `[region+0x316]`, severity/source lane `[region+0x25e]`, + and any stable region-id or class discriminator that can drive shellless city-connection + service. The newly grounded queue-node probe for the atlas-backed kind-`7` notice records is a + negative result on `q.gms`, `p.gms`, and `Autosave.gms`, so the next region pass should not + assume that the transient `[world+0x66a6]` queue family is persisted in ordinary saves; the + region trace now also carries the concrete queued/service owners (`0x00422100`, `0x004337c0`, + `0x00437c00`, `0x004c7520`, `0x004358d0`, `0x00438710`, `0x00420030/0x00420280`, + `0x0047efe0`) so the next pass can focus on the missing saved latches and stable region id/class + rather than on rediscovering the outer service family. +- Rehost or bound the next concrete region owner above the missing latches instead of treating the + absent persisted queue as a stop: start with the checked-in owner strip `0x00422100`, + `0x004337c0`, `0x00437c00`, `0x004c7520`, `0x004358d0`, `0x00438710`, + `0x00420030/0x00420280`, `0x0047efe0`, and reduce it to the first true save-owned or rebuild + owner that can explain `[region+0x25e/+0x276/+0x302/+0x316]` plus a stable region id/class. The + region trace now ranks the current best hypothesis as the pending bonus service owner + (`0x004358d0`) plus the peer/linkage strip (`0x00420030/0x00420280`, `0x0047efe0`), with the + transient producer/queue family explicitly secondary and the queued kind-`7` modal dispatch kept + as shell-adjacent reference only. +- For that top-ranked region strip, treat the next pass as three exact owner questions too: which + restore seam re-seeds `[region+0x25e]` and clears `[region+0x302/+0x316]` before the grounded + `0x00422100 -> 0x004358d0` producer/consumer cycle runs again, which stable region id or class + discriminator survives save/load strongly enough to drive `0x004358d0`, and how far the grounded city-connection peer/linkage helpers + (`0x00420030/0x00420280`, `0x0047efe0`) can be reused directly before the transient queued-notice + family matters again. +- Targeted disassembly now tightens that strip too: `0x004358d0` calls `0x00420030` twice plus + `0x00420280`, then resolves the linked company through `0x0047efe0`, posts company stat slot `4` + on success, and stamps `[region+0x302]` or `[region+0x316]` while clearing `[region+0x276]`. + `0x00420030` itself now reads as the real peer gate over collection `0x006cec20`, combining + `0x0042b2d0`, the optional company filter through `0x0047efe0`, the station-or-transit gate + `0x0047fd50`, and the status branch `0x0047de00 -> 0x0040c990`; `0x00420280` is the same scan + returning the first matching site id. So the remaining unknown is the persisted latch/id seam, + not the live peer/service logic. +- The producer half is grounded now too: `0x00422100` filters for class-`0` regions with + `[region+0x276]==0` and `[region+0x302]==0`, rejects already-connected pairs through + `0x00420030(1,1,0,0)`, chooses one eligible candidate, buckets severity/source lane + `[region+0x25e]` against the three checked thresholds, writes the resulting amount to + `[region+0x276]`, and appends the kind-`7` queued notice through `0x004337c0`. That means the + remaining region gap is now explicitly the upstream restore seam for `[region+0x25e]` and the + completion/fallback latch clear, not either side of the producer/consumer service pair. +- The severity/source lane itself is narrower now too: `0x004cc930` is a selected-region editor + helper that writes `[region+0x25a]` and `[region+0x25e]` together from one integer input, while + `0x00438150` and `0x00442cc0` are fixed-region global reseed/clamp owners over collection + `0x0062bae0` that adjust the same mirrored pair for hardcoded region ids. So the remaining + region restore question is no longer “what does `[region+0x25e]` mean?” but “which load/reseed + seam restores the mirrored severity pair before the producer runs?” +- Two more direct-hit writer bands are now explicitly ruled out too: `0x0043a5a0` is a separate + constructor under vtable root `0x005ca078` that zeroes its own `[this+0x302/+0x316]` fields + during local object setup, and `0x0045c460/0x0045c8xx` is a separate vtable-`0x005cb5e8` helper + family whose `[this+0x316]` is a child-array pointer serialized through `0x61a9/0x61aa/0x61ab`. + So those offset-collision classes should stay out of the remaining region restore search. +- The direct writer census is tighter now too: the other apparent `0x302/0x316` writer bands + (`0x0043dd45`, `0x0043de19`, `0x0043e0a7`, `0x0043f5bc`) all hang off that same non-region + `0x005ca078` family through helpers `0x0043af60` and `0x0043b030`. So the only grounded + region-owned literal writes left are the constructor `0x00421200` plus the producer/consumer + pair `0x00422100` and `0x004358d0`, which means the remaining region seam should now be treated + as an indirect restore/rebuild path rather than another direct offset writer hunt. +- The later post-load per-region sweep is narrowed too: in the broader `0x00444887` restore strip, + the follow-on loop at `0x00444b90` dispatches `0x00420560` over each live region, but that + helper only zeroes and recomputes `[region+0x312]` from the embedded profile collection + `[region+0x37f]/[region+0x383]` and lazily seeds the year-driven `[region+0x317/+0x31b]` band + through `0x00420350`. It still does not touch `[region+0x276/+0x302/+0x316]`, so that whole + follow-on branch should stay out of the remaining latch-restore search too. +- The checked-in constructor owner `0x00421200` + `world_region_construct_entry_with_id_class_and_default_marker09_profile_seed` now also grounds + the initialization side of this family: it clears `[region+0x276]`, `[region+0x302]`, + `[region+0x316]`, and neighboring cached bands at construction time while seeding + `[region+0x25a/+0x25e] = 100.0f` and `[region+0x31b] = 1.0f`. That means the remaining queue item + is specifically post-construction restore or rebuild of the same latches, not their basic field + identity. +- The next restore-side target is explicit now too: the checked-in function map already grounds + `0x00421510` as the tagged region-collection load owner that dispatches each live region through + vtable slot `+0x40`, and `0x0041f5c0` as the per-record load slot that reloads the tagged payload + through `0x00455fc0` before rebuilding profile collection `[region+0x37f]`. So the next region + pass should ask whether `[region+0x276/+0x302/+0x316]` are restored directly inside that payload + load or rebuilt immediately after it, rather than treating “restore seam” as a generic unknown. +- Direct disassembly now closes that callback identity too: `0x0041f590/0x0041f5b0` prove the + world-region vtable root is `0x005c9a28`, so the `0x00455fc0` dispatch at slot `+0x48` lands on + `0x00455870` and the serializer sibling at `+0x4c` lands on `0x00455930`. Those two callbacks + only restore and serialize two helper-local three-lane scalar bands: `0x00455870` reads six + dwords through `0x00531150` and forwards them to `0x00530720 -> [helper+0x1e2/+0x1e6/+0x1ea]` + and `0x0052e8b0 -> [helper+0x4b/+0x4f/+0x53]`, while `0x00455930` writes that same pair back + through `0x00531030`; they still do not touch acquisition-side lanes + `[region+0x2a4]` or `[region+0x310/+0x338/+0x360]`, and they still do not touch + `[region+0x276/+0x302/+0x316]`. That means the remaining region restore target is now the later + owner that rebuilds those latches or the separate tagged body seam that persists them. +- The save-side region payload probe is wider now too: the checked-in `region_record_triplets` + surface no longer stops at raw pre-name prefix bytes and now also emits structured prefix dword + candidates per record, and the fixed `0x55f2` policy chunk now also carries structured reserved + dword candidates instead of raw integers only. That gives the next region payload pass a direct + way to compare both opaque payload bands against the remaining acquisition-side lane shapes + instead of redoing raw hex inspection by hand. +- Grounded real-save output now narrows that new probe two steps further: on both `p.gms` and + `q.gms`, every decoded region triplet currently still has `pre_name_prefix_len = 0`, an empty + `pre_name_prefix_dword_candidates` vector, and `fixed 0x55f2 policy reserved dwords are nonzero + on 0 of 145 decoded region records`. So the remaining acquisition-side payload target does not + appear to live in either the pre-`0x55f1` prefix band or the fixed `0x55f2` reserved dword band + on grounded ordinary saves. That shifts the next region payload-comparison pass onto later body + seams, not back onto the prefix or fixed-policy chunk. +- The new fixed-row run candidate probe pushes that same payload search one seam later, but it is + not grounded yet: on both `p.gms` and `q.gms` it finds high-signal counted runs keyed to the + live region count `145` with fixed row stride `0x29` before the tagged `0x5209/0x520a/0x520b` + region collection, yet the top candidate offset is not stable (`p.gms = 0xd13239`, + `q.gms = 0xd2d7d7`). So the next region payload pass should compare candidate lane-shape + fingerprints across saves rather than promoting any one absolute pre-header offset as the fixed + restore seam. +- The new two-save `runtime compare-region-fixed-row-runs ` report now does + that comparison directly. Current result: `p.gms` vs `q.gms` has `0` exact shape overlaps, and + the only coarse family overlaps are lower-ranked fully mixed candidates where every dword lane is + still simultaneously small-nonzero and partially-zero. That means the fixed-row scan remains + useful negative evidence, but it is still not honest to promote as the missing region restore + seam; the next region pass should stay focused on later restore owners or a more selective row + family discriminator above this mixed pre-header corpus. +- The rest of `0x00455fc0` is ruled down further now too: after the `+0x48` callback it only runs + `0x0052ebd0`, which reads two one-byte generic flags through `0x531150` into base object bytes + `[this+0x20]`, `[this+0x8d]`, `[this+0x5c..+0x61]`, `[this+0x1ee]`, `[this+0x1fa]`, and + `[this+0x3e]`, and then it opens `0x55f3` only for span accounting before returning. So the + missing region latches are not hiding in the remainder of `0x00455fc0` either. +- The next restore-handoff strip is explicit now too: the region trace now carries a dedicated + later-global-restore hypothesis for `0x00444887`, because that continuation is the first caller + checkpoint above the ruled-down `0x00421510 -> 0x0041f5c0 -> 0x00455fc0` path. It immediately + advances into `0x00487c20` territory refresh and `0x0040b5d0` support refresh, then later + re-enters the per-region follow-on loop at `0x00444b90 -> 0x00420560`. Current disassembly keeps + `0x00420560` on the profile/class-mix scalar side only: it recomputes `[region+0x312]` from the + embedded profile collection and linked placed-structure class mix, then seeds the year-driven + `[region+0x317/+0x31b]` band through `0x00420350`. So the next region pass should treat the + broader `0x00444887` continuation as the live handoff seam when chasing + `[region+0x2a4]` and `[region+0x310/+0x338/+0x360]`, not as just another generic restore note. +- That same continuation is less ambiguous now too: the loader immediately after territory/support + refresh, `0x00433130`, is only + `scenario_event_collection_refresh_runtime_records_from_packed_state`, opening `0x4e99/0x4e9a`, + repopulating live event collection `0x0062be18` through `0x0042db20`, and closing `0x4e9b`. + So the event-side branch under `0x00444887` is ruled out as the missing later region restore + handoff too. +- That same continuation is slightly less symmetric now too: the atlas-backed territory side at + `0x00487c20` currently restores only collection metadata/live ids and still uses no-op per-entry + load/save callbacks `0x00487670/0x00487680`, so the next pass should bias more heavily toward + support refresh `0x0040b5d0` or the later region-local rebuild than toward territory payload as + the hidden source of `[region+0x2a4]` and `[region+0x310/+0x338/+0x360]`. +- The support side is less opaque now too: the same atlas already bounds `0x0040b5d0` above + support collection `0x0062b244`, whose grounded live owners maintain goose-entry counters, + neighboring world support lanes `[world+0x4c9a/+0x4c9e/+0x4ca6/+0x4caa]`, and selected + support-entry state rather than an obvious per-region acquisition latch family. So the next pass + should now bias even more toward the later region-local rebuild beneath the `0x00444887` + continuation, while still keeping `0x0040b5d0` as a weaker adjacent prerequisite rather than + treating it as the primary hidden owner. +- The next owner family is narrower now too: the checked-in shell-load subgraph and function map + place `world_load_saved_runtime_state_bundle` `0x00446d40` directly ahead of the post-load + generation pipeline `0x004384d0`, which is now the first explicit non-hook owner family above + the ruled-down `0x00444887` continuation. The current grounded stage order is concrete enough to + split the next static pass: `319` refreshes route entries, auxiliary route trackers, and then the + placed-structure replay strip `0x004133b0`; `320` runs the region-owned building setup strip + `0x00421c20 -> 0x004235c0`; and `321` runs the economy-seeding burst `0x00437b20` plus the + cached region summary refresher `0x00423d30`. That means the next region closure pass should + chase this `0x004384d0` handoff family directly instead of treating the remaining + `[region+0x2a4]` / `[region+0x310/+0x338/+0x360]` gap as a generic continuation below + `0x00444887`. +- The early `0x004384d0` setup strip is tighter now too: before the conditional `320/321` gates it + always runs `0x0044fb70` transport/pricing-grid setup and `0x0041ea50` + candidate-local-service setup, and the extra arg-guarded `0x00421b60 -> 0x004882e0` + default-region pair sits beside them as the last pre-`320/321` setup branch. Those helpers are + already grounded as world-grid, candidate-table, and border-refresh owners rather than the + missing `[region+0x2a4]` / `[region+0x310/+0x338/+0x360]` republisher, so they drop out of the + remaining region-handoff search too. +- The `319` lane is the strongest bridge inside that family: `0x004133b0` drains queued + placed-structure ids through `0x0040e450`, sweeps every live site through `0x0040ee10`, and then + reaches the already-grounded linked-site follow-on `0x00480710`. The `320` and `321` lanes are + still explicit but weaker: `0x00421c20 -> 0x004235c0` stays on region-side demand balancing and + structure placement, while `0x00437b20 -> 0x00423d30` only refreshes the cached category band + `[region+0x27a/+0x27e/+0x282/+0x286]`. So the next non-hook region work should start from the + post-load `319` placed-structure replay seam and only then revisit the narrower region-side + `320/321` branches if the exact field bridge is still missing. +- The `319` adjacency is tighter in another negative way now too. Direct disassembly of + `0x004377a0` shows the post-`319` call staying on chairman-slot/profile materialization: it + normalizes the `16` slot bundles at `[world+0x69da..]`, republishes selector bytes into + `[0x006cec7c+0x87]`, populates the live chairman-profile collection `0x006ceb9c`, and clears the + selected company/chairman bytes `[world+0x21/+0x25]` before the optional `0x0047d440` news + follow-on. The same trace rules down the companion queue strip too: `0x004348e0` is only the + gate for the transient list at `[world+0x66a6]`, `0x00437c00` is its typed dispatcher over + queue-node kind byte `[node+0x08]`, and the later `0x0044d410` calls are world-rect/grid refresh + work. None of those helpers republish `[region+0x2a4]` or `[region+0x310/+0x338/+0x360]`, so the + remaining region search stays centered on `0x004133b0 -> 0x0040ee10 -> 0x00480710` and the later + post-load continuation above it rather than the chairman/queue-side `319` neighbors. +- The `319` plateau is split more cleanly now too: its neighboring world helpers are no longer + plausible region-body owners. `0x00437220` and `0x004377a0` are the chairman-slot selector and + profile materialization family over `[world+0x69d8]`, `[world+0x69db]`, and scenario selector + bytes `[0x006cec7c+0x87]`, while `0x00434d40` is only the subtype-`2` candidate runtime-latch + seeder over live placed structures. That leaves `0x004133b0` as the only region-adjacent `319` + bridge worth chasing for the missing restore seam. +- The `320` branch is narrower than that headline now too: direct worker recovery shows + `0x004235c0` staying inside the live region demand-and-placement family by routing through + `0x00422900` cached category accumulation, `0x004234e0` projected structure-count scalars, + `0x00422be0` placed-count subtraction, and `0x00422ee0` placement attempts over the live + placed-structure registry `0x0062b26c`. That keeps it on live setup and maintenance work rather + than any restore-time republisher for `[region+0x2a4]` or `[region+0x310/+0x338/+0x360]`. +- The `321` branch is narrower in the same way: `0x00437b20` only stages the fast-forward burst, + then re-enters the live region collection through `0x00423d30`, and that helper only republishes + `[region+0x27a/+0x27e/+0x282/+0x286]` via `0x00422900`. It should stay ruled down as a cached + summary refresh instead of a plausible owner for the missing restore-side body lanes. +- The later restore-side region owners are narrowed further now too: the `0x00421ce0 -> + 0x0041fb00 -> 0x00421730` sweep is class-`0` raster/id rebuild, `0x004881b0` is a companion + region-set cell-count rebuild over `[region+0x3d/+0x41]`, `0x00487de0` is a border-segment + emitter over the world raster, and `0x0044c4b0` is the center-cell bit-`0x10` reseed pass. So + the next region slice should stop revisiting those later owners and stay focused on the still- + missing save-owned latch / severity / stable-id seam. +- The later class-`0` batch at `0x00438087` is narrowed now too: it walks live class-`0` regions + through `0x0062bae0`, rescales the mirrored severity/source pair `[region+0x25a/+0x25e]` from + the current value using world-side factors, clamps the result, and then hands the collection to + `0x00421c20`; it still does not touch `[region+0x276/+0x302/+0x316]`. +- Its follow-on `0x00421c20` is bounded as a parameterized region-collection helper rather than a + latch owner: it loops the same collection with caller-supplied scalar arguments, dispatches each + record through `0x004235c0`, and does not write the pending/completion/one-shot lanes directly. +- The subsequent world follow-ons are narrower too: `0x00437b20` only stages a world-side reentry + guard at `[world+0x46c38]`, iterates the live region collection through `0x00423d30`, and tails + into `0x00434d40`, while `0x00437220` rebuilds broader world byte-set state around + `[world+0x66be/+0x69db]` and other global collections. Those later branches should stay out of + the remaining region latch-restore search too. +- The widened real-save region trace rules out one more false lead too: on grounded saves the + `0x55f2` fixed-policy chunk keeps all three reserved dwords at `0x00000000` and the trailing word + at invariant `0x0001`, so that fixed chunk is not currently carrying the missing latch or stable + region id/class discriminator either. +- Reconstruct the save-side placed-structure collection body on top of the newly grounded + `0x36b1/0x36b2/0x36b3` header seam so the blocked city-connection / linked-transit branch can + stop depending on atlas-only placed-structure and local-runtime refresh notes, especially the + semantics of the now-grounded compact `0x55f3` footer dword/status lane and the newly exposed + separate tagged side-buffer seam candidates, especially the exact `0x38a5/0x38a6/0x38a7` + family whose compact `6`-byte header pattern and embedded placed-structure-style `0x55f1` + name rows now make it the grounded placed-structure dynamic side-buffer owner; the remaining + blocker is semantic closure of the compact prefix regimes now summarized in real saves as seven + stable patterns on `q.gms` and their relation to the embedded `0x55f1/0x55f2/0x55f3` row + subset, especially now that the side-buffer name-pair corpus is proven disjoint from the + grounded `0x36b1` triplet name-pair corpus on `q.gms`; the next pass should treat `0x38a5` as + a separate infrastructure-asset owner seam, not a compact alias over the triplet records. +- Extend shellless clock advancement so more periodic-company service branches consume owned + runtime time state directly instead of only the explicit periodic service command. +- Keep widening selected-year world-owner state only when a full owning reader/rebuild family is + grounded strongly enough to avoid one-off leaf guesses. + +## In Progress + +- Widen shellless simulation from explicit service commands toward “advance the runtime clock and + the simulation-owned services advance with it.” + +## Queued + +- Rehost additional periodic finance/service branches that still depend on frozen world restore + fields instead of advanced runtime-owned time state. +- Reduce remaining company/chairman save-native gaps that still block standalone simulation + quality, especially controller-kind closure and any deeper finance/state fields that still rely + on conservative defaults. +- Rehost bounded live economy owner state beyond selector/catalog/override surfaces when a + concrete non-shell-owned seam is grounded. +- Keep tightening shell-owned parity families only when that directly supports later rehosting. + +## Blocked + +- Full shell/dialog ownership remains intentionally out of scope. +- Any candidate slice that requires guessing rather than rehosting owning state or real + reader/setter families stays blocked until a better owner seam is grounded. +- Missing owner seams or dispatch mappings are not by themselves a stop condition when a targeted + static-mapping pass or a higher-layer rehosted trace/evaluator surface can still narrow them + further without guessing. +- The city-connection announcement / linked-transit roster-maintenance branch is still blocked at + the record-body level, not the collection-identity level: the runtime now has a corrected + non-direct tagged region seam, a tagged train header-plus-directory seam, and a tagged + placed-structure header seam, but it does not yet reconstruct the live region or + placed-structure record bodies those service owners need. + +## Recently Done + +- `rrt-runtime` now exposes three higher-layer probe surfaces and matching CLI inspectors: + `runtime inspect-periodic-company-service-trace `, + `runtime inspect-region-service-trace `, and + `runtime inspect-infrastructure-asset-trace `. These reports separate grounded outer + owner inputs, runnable shellless branches, and explicit missing owner seams instead of leaving + the current city-connection / linked-transit frontier as an opaque blocker. +- Those same probes now also sharpen the next queue choice on grounded real saves: the periodic + company outer owner shows annual finance and route-preference override as grounded shellless + branches while city-connection and linked-transit stay blocked on region/infrastructure owner + seams; the region trace keeps the queued kind-`7` notice family on the transient side; and the + infrastructure trace now makes the `0x38a5` consumer-mapping blocker first-class after + disproving any alias to the `0x36b1` placed-structure triplet corpus. +- The infrastructure trace now also carries one small atlas-backed static-analysis layer above that + seam: bridge/tunnel/track-cap name-family counts from the real side-buffer corpus plus concrete + consumer candidates rooted at the `Infrastructure` child attach/rebuild/serializer helpers and + the later route/local-runtime follow-on owners. That means the next `0x38a5` pass can be + targeted static mapping instead of another generic scan. +- The same `0x38a5` probe now also exports payload-envelope summaries directly instead of only flat + name rows: policy/profile tag presence, dominant embedded `0x55f2` and `0x55f3` span lengths, + and sampled row boundaries. That means the next pass can decode the short embedded `0x55f3` + payload lane on top of already grounded row boundaries instead of rediscovering the same + envelopes again. +- That same probe now also exports the grounded short trailing flag-byte pair summary for the + dominant `0x06`-byte rows, while the infrastructure trace carries the matching + `0x0052ebd0/0x0052ec50` helper seam. That means the next pass can aim directly at how those + flags combine with compact-prefix regimes and primary-child restore state instead of treating the + short lane as anonymous payload. +- That same probe now also exports the fixed `0x55f2` six-dword policy samples and the grounded + shared trailing word `0x0101` for all embedded rows, while the infrastructure trace carries the + matching `0x00455870/0x00455930` helper seam. That means the next pass can focus on which of the + two restored dword triplets actually bridge into child-count / primary-child state instead of + rediscovering the fixed `0x55f2` row shape. +- The infrastructure trace now also carries the deeper `0x00530720/0x0052e8b0` bridge, so the next + pass can focus on the outer payload-stream header and compact-prefix regimes instead of revisiting + the fixed `0x55f2` six-dword row. +- That same trace now also ranks those consumers into explicit hypotheses, so the next + infrastructure pass should start with the attach/rebuild strip instead of treating all + candidate owners as equally likely. +- The region trace now also carries the corresponding atlas-backed candidate owner strip above the + unresolved save latches, so the region frontier is now explicitly “missing persisted owner seam + for `[region+0x25e/+0x276/+0x302/+0x316]` and stable region id/class,” not “unknown service + family.” +- That same trace now also ranks those owners into explicit hypotheses, so the next region pass + should start with the pending bonus service owner and peer/linkage strip rather than the queued + modal family. +- Save inspection now splits the shared `0x5209/0x520a/0x520b` 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 family is the larger non-direct `Marker09` + collection with live_id/count `0x96/0x91`; the tagged placed-structure header + (`0x36b1/0x36b2/0x36b3`) remains grounded alongside them. +- That same corrected region seam now also exposes repeated `0x55f1/0x55f2/0x55f3` serialized + record triplets with len-prefixed names plus fixed policy/profile chunk lengths, so the next + city-connection pass can target the real record envelope instead of another blind scan. +- The fixed `0x55f2` row inside each region triplet is now decoded structurally as three leading + `f32` lanes, three reserved `u32` lanes, and a trailing `u16` word, so the next save-region + slice can focus on the larger `0x55f3` payload where the pending/completion/one-shot latches are + most likely to live. +- The larger `0x55f3` payload now also exposes an embedded direct profile collection with grounded + live-id/count headers, fixed `0x22`-byte rows, profile names, and trailing weight scalars, so + the remaining region work is on the unresolved payload fields above that collection rather than + on the profile subcollection itself. +- Grounded real saves now also show that the region-side `0x55f3` payload has zero trailing + padding beyond that embedded profile collection, so the remaining region blocker has shifted + from “find the hidden tail inside this payload” to “find the separate owner seam that backs the + runtime latches the city-connection branch still reads.” +- Save inspection now also exports a generic low-tag unclassified collection scan over plausible + indexed-collection headers, now through a lightweight CLI path that does not require full bundle + inspection and now filters out candidates nested inside already-grounded company/chairman/train/ + region/placed-structure spans. +- That lightweight scan now also narrows the real save frontier to a much smaller stable candidate + set across `p.gms`, `q.gms`, and `Autosave.gms`, with the exact `0x38a5/0x38a6/0x38a7` family + standing out as the strongest current placed-structure dynamic side-buffer candidate. +- The `0x38a5/0x38a6/0x38a7` family now also has a first dedicated parser scaffold in + `rrt-runtime`: its synthetic regression is grounded, its header shape is checked in, and the + parser now expects a compact 6-byte prefix plus separator byte before an embedded + placed-structure-style dual-name row rather than treating the family as anonymous residue. +- That exact `0x38a5/0x38a6/0x38a7` parser is now also wired through a lightweight CLI inspector + and the normal save company/chairman analysis output, and grounded real saves now prove the + same seam directly: + `q.gms` exposes `live_record_count=3865`, prefix `0x0005d368/0x0001/0xff`, and first embedded + names `TrackCapST_Cap.3dp` / `Infrastructure`; `p.gms` exposes the same structure with + `live_record_count=2467`. +- That same direct `0x38a5` probe now also samples multiple embedded name rows with their + preceding compact prefixes, showing that the seam is not a one-off wrapper: grounded `q.gms` + samples include repeated `TunnelSTBrick_*` names under `Infrastructure` with compact leading + dwords like `0x000055f3` and `0xff0000ff`, so the next pass can target the semantics of those + compact prefix patterns instead of hunting the owner seam itself. +- The `0x38a5` probe now also summarizes all embedded compact prefix regimes instead of just the + first few samples: grounded `q.gms` currently exposes seven stable pattern groups across 138 + embedded rows, with the dominant `0xff000000/0x0001/0xff` group carrying 62 bridge-section + rows, the `0xff0000ff/0x0001/0xff` and `0xf3010100/0x0055/0x00` groups concentrating cap-like + rows, and a smaller `0x000055f3/0x0001/0xff` group carrying 17 tunnel-section / cap rows whose + leading dword matches the embedded placed-structure profile tag directly. +- The save-company/chairman analysis path now also compares that grounded `0x38a5` side-buffer + name-pair corpus against the grounded `0x36b1` triplet name-pair corpus directly; on `q.gms` + the overlap is currently zero (`0/138` decoded side-buffer rows and `0/5` unique side-buffer + name pairs match the 56-triplet corpus), which shifts the remaining placed-structure work away + from “prove these are aliases” toward “find how the separate infrastructure-asset owner seam is + consumed by city-connection / linked-transit service.” +- Save inspection now also has a dedicated probe for the atlas-backed region queued-notice node + shape (`payload seed 0x005c87a8`, kind `7`, zero promotion latch, region id, amount, `-1/-1` + tails), plus a matching lightweight CLI inspector. Grounded `q.gms`, `p.gms`, and `Autosave.gms` + all currently return `null`, which is useful negative evidence: the transient region notice + queue is not obviously persisted in these ordinary saves. +- The placed-structure tagged save stream now also exposes repeated `0x55f1/0x55f2/0x55f3` + triplets with dual name stems, a fixed five-`f32` policy row, and a compact `0x5dc1...0x5dc2` + footer carrying one raw `u32` payload lane plus one live `i32` status lane, so the remaining + placed-structure work is semantic closure of those owned fields rather than envelope discovery. +- That compact placed-structure `i32` footer status lane is now partially grounded as owned + semantics too: observed non-farm families stay at `-1`, while farm families use nonnegative + `0..11` buckets that are now exported as farm growth-stage indices instead of opaque raw status + residue. +- Stepped calendar progression now also refreshes save-world owner time fields, including packed + year, packed tuple words, absolute counter, and the derived selected-year gap scalar. +- Automatic year-rollover calendar stepping now invokes periodic-boundary service. +- Save-native world locomotive policy owner state now flows through runtime restore state, + summaries, and keyed world-flag execution for the grounded `All Steam/Diesel/Electric Locos + Avail.` descriptor strip plus the cached available-locomotive rating. +- The selected-year bucket ladder rooted in `0x00433bd0` is now checked in as a static artifact, + and runtime restore state now derives both the selected-year bucket scalar and the + `[world+0x0bde]` economic-tuning mirror from owner-family inputs instead of preserving stale + load-time residue. +- That same selected-year 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 checked-in `0x00433bd0` artifact instead of + preserving stale save-time residue. +- The 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 beside the city-connection and linked-transit finance gates. +- That same side-latch trio now also has a runtime-owned service-state map and summary surface, + so later periodic company-service work can stop reading those lanes directly from imported + market/cache residue. +- The periodic-boundary owner now also clears the transient preferred-locomotive side latch every + cycle and reseeds the finance latches from market state where present, while preserving + side-latch-only company context when no market projection exists. +- The outer periodic-company seam now also has a first-class runtime reader: + selected-company summaries and finance readers can resolve the base `[world+0x4c74]` + route-preference byte, the effective electric-only override fed by `0x0d17`, and the matching + `1.4x` versus `1.8x` route-quality multiplier through owned periodic-service state instead of + leaving that bridge in atlas notes. +- That same periodic-company seam now also owns a first-class route-preference apply/restore + mutation lane: runtime service state tracks the active and last electric override, beginning the + override rewrites `[world+0x4c74]` to the effective route preference for the selected company + service pass, and ending the override restores the base world byte instead of leaving the seam as + a pure reader bridge. +- The same route-preference mutation seam now also carries explicit apply/restore service counters + through runtime service state and summaries, so later periodic-company branches can assert that + override activity happened even before the missing city-connection / linked-transit service + owners are fully rehosted. +- Company cash, confiscation, and major governance effects now write through owner state instead of + drifting from market/cache readers. +- Company credit rating, prime rate, book value per share, investor confidence, and management + attitude now refresh from grounded owner-state readers. +- Annual finance service persists structured news events and grounded debt/share flow totals. diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 50ae6b6..8227572 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -19,7 +19,7 @@ Implemented today: normalized runtime state validation - periodic trigger dispatch exists, including ordered periodic maintenance, dirty rerun `0x0a`, and a normalized runtime-effect surface with staged event-record mutation -- snapshots, state dumps, save-slice projection, and normalized state diffing already exist in the +- snapshots, state inputs, save-slice projection, and normalized state diffing already exist in the CLI and fixture layers - checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger service, snapshot-backed inputs, save-slice-backed inputs, overlay-import-backed inputs, @@ -498,7 +498,7 @@ Exit criteria: Current status: -- runtime snapshots and state dumps are implemented +- runtime snapshots and state inputs are implemented - `.smp` save inspection and partial save-slice projection already feed normalized runtime state - the packed event-collection bridge now carries per-record summaries into loaded save slices, projected runtime snapshots, normalized diffs, and fixtures @@ -636,9 +636,9 @@ camera work, or shell windows just because their names are nearby in the call gr The currently implemented normalized runtime surface is: - `CalendarPoint`, `RuntimeState`, `StepCommand`, `StepResult`, and `RuntimeSummary` -- fixture loading from inline state, snapshots, and state dumps +- fixture loading from inline state, snapshots, and state inputs - `runtime validate-fixture`, `runtime summarize-fixture`, `runtime export-fixture-state`, - `runtime summarize-state`, `runtime import-state`, and `runtime diff-state` + `runtime summarize-state`, `runtime snapshot-state`, and `runtime diff-state` - deterministic stepping, periodic trigger dispatch, one-shot event handling, dirty reruns, and a normalized runtime-effect vocabulary with staged event-record mutation - save-side inspection and partial state projection for `.smp` inputs, including per-record packed diff --git a/docs/atlas/README.md b/docs/subsystem-views/README.md similarity index 51% rename from docs/atlas/README.md rename to docs/subsystem-views/README.md index 015728a..8240fbe 100644 --- a/docs/atlas/README.md +++ b/docs/subsystem-views/README.md @@ -1,18 +1,18 @@ -# Atlas Notes +# Subsystem Views -These notes are a finer-grained navigation layer over the canonical split atlas in +These files are a curated cross-cut navigation layer over the canonical split atlas in [docs/control-loop-atlas/](/home/jan/projects/rrt/docs/control-loop-atlas/README.md), with [control-loop-atlas.md](/home/jan/projects/rrt/docs/control-loop-atlas.md) retained as a compatibility index. Current subsystem views: -- [startup-shell-and-content.md](/home/jan/projects/rrt/docs/atlas/startup-shell-and-content.md) -- [multiplayer.md](/home/jan/projects/rrt/docs/atlas/multiplayer.md) -- [runtime-and-world-tools.md](/home/jan/projects/rrt/docs/atlas/runtime-and-world-tools.md) -- [route-entry-and-trackers.md](/home/jan/projects/rrt/docs/atlas/route-entry-and-trackers.md) -- [company-and-ledger.md](/home/jan/projects/rrt/docs/atlas/company-and-ledger.md) -- [editor-and-site-service.md](/home/jan/projects/rrt/docs/atlas/editor-and-site-service.md) +- [startup-shell-and-content.md](/home/jan/projects/rrt/docs/subsystem-views/startup-shell-and-content.md) +- [multiplayer.md](/home/jan/projects/rrt/docs/subsystem-views/multiplayer.md) +- [runtime-and-world-tools.md](/home/jan/projects/rrt/docs/subsystem-views/runtime-and-world-tools.md) +- [route-entry-and-trackers.md](/home/jan/projects/rrt/docs/subsystem-views/route-entry-and-trackers.md) +- [company-and-ledger.md](/home/jan/projects/rrt/docs/subsystem-views/company-and-ledger.md) +- [editor-and-site-service.md](/home/jan/projects/rrt/docs/subsystem-views/editor-and-site-service.md) Scope policy: diff --git a/docs/atlas/company-and-ledger.md b/docs/subsystem-views/company-and-ledger.md similarity index 97% rename from docs/atlas/company-and-ledger.md rename to docs/subsystem-views/company-and-ledger.md index 9a88c79..591bb0a 100644 --- a/docs/atlas/company-and-ledger.md +++ b/docs/subsystem-views/company-and-ledger.md @@ -13,12 +13,12 @@ Current grounded owners: - `shell_company_detail_window_construct` - `shell_company_detail_window_handle_message` - `shell_load_screen_window_construct` -- `company_service_periodic_city_connection_finance_and_linked_transit_lanes` -- `company_evaluate_annual_finance_policy_and_publish_news` +- `company_service_periodic_city_connection_finance_and_linked_transit` +- `company_apply_annual_finance_policy_and_publish_news` - `company_balance_linked_transit_train_roster` -- `company_try_buy_unowned_industry_near_city_and_publish_news` -- `company_evaluate_and_publish_city_connection_bonus_news` -- `simulation_try_select_and_publish_company_start_or_city_connection_news` +- `company_try_acquire_unowned_industry_near_city_and_publish_news` +- `company_try_publish_city_connection_bonus_news` +- `simulation_try_publish_startup_company_or_city_connection_news` Current bounded finance verbs: diff --git a/docs/atlas/editor-and-site-service.md b/docs/subsystem-views/editor-and-site-service.md similarity index 100% rename from docs/atlas/editor-and-site-service.md rename to docs/subsystem-views/editor-and-site-service.md diff --git a/docs/atlas/multiplayer.md b/docs/subsystem-views/multiplayer.md similarity index 100% rename from docs/atlas/multiplayer.md rename to docs/subsystem-views/multiplayer.md diff --git a/docs/atlas/route-entry-and-trackers.md b/docs/subsystem-views/route-entry-and-trackers.md similarity index 99% rename from docs/atlas/route-entry-and-trackers.md rename to docs/subsystem-views/route-entry-and-trackers.md index c1f5007..a312a16 100644 --- a/docs/atlas/route-entry-and-trackers.md +++ b/docs/subsystem-views/route-entry-and-trackers.md @@ -9,7 +9,7 @@ that are easy to lose inside the broader runtime section. Current grounded owners: -- `city_connection_try_build_route_with_optional_direct_site_placement` +- `city_connection_try_build_route_and_optionally_place_direct_site` - `route_entry_collection_try_build_path_between_optional_endpoint_entries` - `route_entry_collection_search_path_between_entry_or_coord_endpoints` - `route_entry_collection_create_endpoint_entry_from_coords_and_policy` diff --git a/docs/atlas/runtime-and-world-tools.md b/docs/subsystem-views/runtime-and-world-tools.md similarity index 100% rename from docs/atlas/runtime-and-world-tools.md rename to docs/subsystem-views/runtime-and-world-tools.md diff --git a/docs/atlas/startup-shell-and-content.md b/docs/subsystem-views/startup-shell-and-content.md similarity index 100% rename from docs/atlas/startup-shell-and-content.md rename to docs/subsystem-views/startup-shell-and-content.md diff --git a/fixtures/runtime/minimal-world-raw-state.json b/fixtures/runtime/minimal-world-state-input.json similarity index 80% rename from fixtures/runtime/minimal-world-raw-state.json rename to fixtures/runtime/minimal-world-state-input.json index ae7073e..cd19dbb 100644 --- a/fixtures/runtime/minimal-world-raw-state.json +++ b/fixtures/runtime/minimal-world-state-input.json @@ -1,8 +1,8 @@ { "format_version": 1, - "dump_id": "minimal-world-raw-state", + "input_id": "minimal-world-state-input", "source": { - "description": "Raw runtime state dump equivalent to the minimal world smoke setup." + "description": "Runtime state input equivalent to the minimal world smoke setup." }, "state": { "calendar": { diff --git a/fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json b/fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json index a6e0f51..ee8827f 100644 --- a/fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json +++ b/fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 1 imports and executes on selected-chairman scope." }, - "state_import_path": "packed-event-chairman-cash-overlay.json", + "state_input_path": "packed-event-chairman-cash-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json b/fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json index 9228caa..5391de2 100644 --- a/fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json +++ b/fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving grounded chairman metric conditions import and execute through selected-chairman context." }, - "state_import_path": "packed-event-chairman-condition-overlay.json", + "state_input_path": "packed-event-chairman-condition-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-chairman-missing-context-save-slice-fixture.json b/fixtures/runtime/packed-event-chairman-missing-context-save-slice-fixture.json index 21790aa..bc9f914 100644 --- a/fixtures/runtime/packed-event-chairman-missing-context-save-slice-fixture.json +++ b/fixtures/runtime/packed-event-chairman-missing-context-save-slice-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving chairman-targeted rows stay parity-only without runtime chairman context." }, - "state_import_path": "packed-event-chairman-cash-save-slice.json", + "state_input_path": "packed-event-chairman-cash-save-slice.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json b/fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json index 870bd9c..69155c4 100644 --- a/fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json +++ b/fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving unsupported chairman scopes stay parity-only under an explicit blocker." }, - "state_import_path": "packed-event-chairman-scope-parity-save-slice.json", + "state_input_path": "packed-event-chairman-scope-parity-save-slice.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json b/fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json index 59cba52..e0e4867 100644 --- a/fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json +++ b/fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving grounded company governance conditions import and execute." }, - "state_import_path": "packed-event-company-governance-condition-overlay.json", + "state_input_path": "packed-event-company-governance-condition-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-confiscate-all-overlay-fixture.json b/fixtures/runtime/packed-event-confiscate-all-overlay-fixture.json index 0e6d0c9..8399e4f 100644 --- a/fixtures/runtime/packed-event-confiscate-all-overlay-fixture.json +++ b/fixtures/runtime/packed-event-confiscate-all-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 9 Confiscate All imports and executes through the ordinary runtime path." }, - "state_import_path": "packed-event-confiscate-all-overlay.json", + "state_input_path": "packed-event-confiscate-all-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json b/fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json index 88c185a..5acb763 100644 --- a/fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json +++ b/fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 14 imports and executes on selected-chairman scope." }, - "state_import_path": "packed-event-deactivate-chairman-overlay.json", + "state_input_path": "packed-event-deactivate-chairman-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json b/fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json index 3c923a8..9b788aa 100644 --- a/fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json +++ b/fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture backed by an overlay import document so the real Deactivate Company row executes against selected-company context." }, - "state_import_path": "packed-event-deactivate-company-overlay.json", + "state_input_path": "packed-event-deactivate-company-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-deactivate-player-overlay-fixture.json b/fixtures/runtime/packed-event-deactivate-player-overlay-fixture.json index a94f4d6..835b5cf 100644 --- a/fixtures/runtime/packed-event-deactivate-player-overlay-fixture.json +++ b/fixtures/runtime/packed-event-deactivate-player-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 14 Deactivate Player imports and executes through the ordinary runtime path." }, - "state_import_path": "packed-event-deactivate-player-overlay.json", + "state_input_path": "packed-event-deactivate-player-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-economic-status-overlay-fixture.json b/fixtures/runtime/packed-event-economic-status-overlay-fixture.json index a2ebc04..1d3d107 100644 --- a/fixtures/runtime/packed-event-economic-status-overlay-fixture.json +++ b/fixtures/runtime/packed-event-economic-status-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 8 Economic Status imports and executes through the ordinary runtime path." }, - "state_import_path": "packed-event-economic-status-overlay.json", + "state_input_path": "packed-event-economic-status-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-locomotive-availability-overlay-fixture.json b/fixtures/runtime/packed-event-locomotive-availability-overlay-fixture.json index 417cd20..bbc58bc 100644 --- a/fixtures/runtime/packed-event-locomotive-availability-overlay-fixture.json +++ b/fixtures/runtime/packed-event-locomotive-availability-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture backed by an overlay import document so scalar locomotive availability descriptors execute against captured catalog context." }, - "state_import_path": "packed-event-locomotive-availability-overlay.json", + "state_input_path": "packed-event-locomotive-availability-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json b/fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json index 47540f3..b09dfe8 100644 --- a/fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json +++ b/fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture backed by an overlay import document so recovered scalar locomotive cost descriptors execute against captured catalog context." }, - "state_import_path": "packed-event-locomotive-cost-overlay.json", + "state_input_path": "packed-event-locomotive-cost-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json b/fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json index aeb6ed0..ddbc2da 100644 --- a/fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json +++ b/fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture backed by an overlay import document proving mixed real grouped-row records stay parity-only." }, - "state_import_path": "packed-event-mixed-company-descriptor-overlay.json", + "state_input_path": "packed-event-mixed-company-descriptor-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json b/fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json index 41f2f8c..4c2c08c 100644 --- a/fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json +++ b/fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture backed by an overlay import document so the first real negative-sentinel company-scope row executes against selected-company context." }, - "state_import_path": "packed-event-negative-company-scope-overlay.json", + "state_input_path": "packed-event-negative-company-scope-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-ordinary-company-finance-overlay-fixture.json b/fixtures/runtime/packed-event-ordinary-company-finance-overlay-fixture.json index 26d54ca..72bbfe9 100644 --- a/fixtures/runtime/packed-event-ordinary-company-finance-overlay-fixture.json +++ b/fixtures/runtime/packed-event-ordinary-company-finance-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving real ordinary Current Cash conditions gate Company Cash through the normal runtime path." }, - "state_import_path": "packed-event-ordinary-company-finance-overlay.json", + "state_input_path": "packed-event-ordinary-company-finance-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-ordinary-company-territory-track-overlay-fixture.json b/fixtures/runtime/packed-event-ordinary-company-territory-track-overlay-fixture.json index 1db2b9e..f677848 100644 --- a/fixtures/runtime/packed-event-ordinary-company-territory-track-overlay-fixture.json +++ b/fixtures/runtime/packed-event-ordinary-company-territory-track-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving real ordinary company-territory thresholds gate Company Cash through the normal runtime path." }, - "state_import_path": "packed-event-ordinary-company-territory-track-overlay.json", + "state_input_path": "packed-event-ordinary-company-territory-track-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-ordinary-company-track-overlay-fixture.json b/fixtures/runtime/packed-event-ordinary-company-track-overlay-fixture.json index 4dd244d..76db55f 100644 --- a/fixtures/runtime/packed-event-ordinary-company-track-overlay-fixture.json +++ b/fixtures/runtime/packed-event-ordinary-company-track-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving real ordinary company-track thresholds gate Company Track Pieces Buildable through the normal runtime path." }, - "state_import_path": "packed-event-ordinary-company-track-overlay.json", + "state_input_path": "packed-event-ordinary-company-track-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-ordinary-named-company-territory-overlay-fixture.json b/fixtures/runtime/packed-event-ordinary-named-company-territory-overlay-fixture.json index c7a32bb..0a110c3 100644 --- a/fixtures/runtime/packed-event-ordinary-named-company-territory-overlay-fixture.json +++ b/fixtures/runtime/packed-event-ordinary-named-company-territory-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving named-territory company-territory ordinary rows bind exactly and execute through the normal runtime path." }, - "state_import_path": "packed-event-ordinary-named-company-territory-overlay.json", + "state_input_path": "packed-event-ordinary-named-company-territory-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-ordinary-named-territory-executable-overlay-fixture.json b/fixtures/runtime/packed-event-ordinary-named-territory-executable-overlay-fixture.json index 9b15ef5..9cc80df 100644 --- a/fixtures/runtime/packed-event-ordinary-named-territory-executable-overlay-fixture.json +++ b/fixtures/runtime/packed-event-ordinary-named-territory-executable-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving named-territory ordinary rows bind exactly and execute through the normal runtime path." }, - "state_import_path": "packed-event-ordinary-named-territory-executable-overlay.json", + "state_input_path": "packed-event-ordinary-named-territory-executable-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-ordinary-named-territory-overlay-fixture.json b/fixtures/runtime/packed-event-ordinary-named-territory-overlay-fixture.json index 8a7d4f4..8af44bd 100644 --- a/fixtures/runtime/packed-event-ordinary-named-territory-overlay-fixture.json +++ b/fixtures/runtime/packed-event-ordinary-named-territory-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving named-territory ordinary conditions stay parity-only with an explicit blocker." }, - "state_import_path": "packed-event-ordinary-named-territory-overlay.json", + "state_input_path": "packed-event-ordinary-named-territory-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-ordinary-territory-track-overlay-fixture.json b/fixtures/runtime/packed-event-ordinary-territory-track-overlay-fixture.json index 7f8e712..426b490 100644 --- a/fixtures/runtime/packed-event-ordinary-territory-track-overlay-fixture.json +++ b/fixtures/runtime/packed-event-ordinary-territory-track-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving aggregate territory thresholds can gate real packed-event execution when overlay territory context is present." }, - "state_import_path": "packed-event-ordinary-territory-track-overlay.json", + "state_input_path": "packed-event-ordinary-territory-track-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-player-cash-overlay-fixture.json b/fixtures/runtime/packed-event-player-cash-overlay-fixture.json index 158c11e..169026f 100644 --- a/fixtures/runtime/packed-event-player-cash-overlay-fixture.json +++ b/fixtures/runtime/packed-event-player-cash-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 1 Player Cash imports and executes through the ordinary runtime path." }, - "state_import_path": "packed-event-player-cash-overlay.json", + "state_input_path": "packed-event-player-cash-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-retire-train-company-overlay-fixture.json b/fixtures/runtime/packed-event-retire-train-company-overlay-fixture.json index 8ca57e9..843d208 100644 --- a/fixtures/runtime/packed-event-retire-train-company-overlay-fixture.json +++ b/fixtures/runtime/packed-event-retire-train-company-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 15 Retire Train executes against company scope." }, - "state_import_path": "packed-event-retire-train-company-overlay.json", + "state_input_path": "packed-event-retire-train-company-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-retire-train-territory-overlay-fixture.json b/fixtures/runtime/packed-event-retire-train-territory-overlay-fixture.json index df0ab8b..71b1fb7 100644 --- a/fixtures/runtime/packed-event-retire-train-territory-overlay-fixture.json +++ b/fixtures/runtime/packed-event-retire-train-territory-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 15 Retire Train executes against territory and locomotive filters." }, - "state_import_path": "packed-event-retire-train-territory-overlay.json", + "state_input_path": "packed-event-retire-train-territory-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json b/fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json index 425c960..cf5e9b2 100644 --- a/fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json +++ b/fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving runtime-variable ordinary conditions gate imported records through bounded world/company/player/territory variable surfaces." }, - "state_import_path": "packed-event-runtime-variable-condition-overlay.json", + "state_input_path": "packed-event-runtime-variable-condition-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json b/fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json index 0e93b3f..0255012 100644 --- a/fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json +++ b/fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptors 39..54 import and execute through bounded world/company/player/territory runtime-variable surfaces." }, - "state_import_path": "packed-event-runtime-variable-overlay.json", + "state_input_path": "packed-event-runtime-variable-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-selection-only-context-overlay-fixture.json b/fixtures/runtime/packed-event-selection-only-context-overlay-fixture.json index ee6cd4c..8f07b40 100644 --- a/fixtures/runtime/packed-event-selection-only-context-overlay-fixture.json +++ b/fixtures/runtime/packed-event-selection-only-context-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving selection-only raw save company/chairman context overrides base selected ids without replacing base rosters." }, - "state_import_path": "packed-event-selection-only-context-overlay.json", + "state_input_path": "packed-event-selection-only-context-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-selective-import-overlay-fixture.json b/fixtures/runtime/packed-event-selective-import-overlay-fixture.json index 571fc4f..4426ff2 100644 --- a/fixtures/runtime/packed-event-selective-import-overlay-fixture.json +++ b/fixtures/runtime/packed-event-selective-import-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture backed by an overlay import document so save-derived packed events execute against captured company context." }, - "state_import_path": "packed-event-selective-import-overlay.json", + "state_input_path": "packed-event-selective-import-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json b/fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json index 6a4c8ba..7968838 100644 --- a/fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json +++ b/fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture backed by an overlay import document so symbolic company-target packed events execute against selected-company and controller-role context." }, - "state_import_path": "packed-event-symbolic-company-scope-overlay.json", + "state_input_path": "packed-event-symbolic-company-scope-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-territory-access-overlay-fixture.json b/fixtures/runtime/packed-event-territory-access-overlay-fixture.json index 31310ee..091bba3 100644 --- a/fixtures/runtime/packed-event-territory-access-overlay-fixture.json +++ b/fixtures/runtime/packed-event-territory-access-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving descriptor 3 Territory - Allow All imports as company territory-access rights and executes through the ordinary runtime path." }, - "state_import_path": "packed-event-territory-access-overlay.json", + "state_input_path": "packed-event-territory-access-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-track-capacity-overlay-fixture.json b/fixtures/runtime/packed-event-track-capacity-overlay-fixture.json index ff840de..bc15171 100644 --- a/fixtures/runtime/packed-event-track-capacity-overlay-fixture.json +++ b/fixtures/runtime/packed-event-track-capacity-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture backed by an overlay import document so the real Company Track Pieces Buildable row executes against selected-company context." }, - "state_import_path": "packed-event-track-capacity-overlay.json", + "state_input_path": "packed-event-track-capacity-overlay.json", "commands": [ { "kind": "service_trigger_kind", diff --git a/fixtures/runtime/packed-event-world-flag-condition-overlay-fixture.json b/fixtures/runtime/packed-event-world-flag-condition-overlay-fixture.json index 525f050..1e5a2c8 100644 --- a/fixtures/runtime/packed-event-world-flag-condition-overlay-fixture.json +++ b/fixtures/runtime/packed-event-world-flag-condition-overlay-fixture.json @@ -5,7 +5,7 @@ "kind": "captured-runtime", "description": "Fixture proving a checked-in world-flag condition gates a whole-game effect through the ordinary runtime path." }, - "state_import_path": "packed-event-world-flag-condition-overlay.json", + "state_input_path": "packed-event-world-flag-condition-overlay.json", "commands": [ { "kind": "service_trigger_kind",