6737 lines
259 KiB
Rust
6737 lines
259 KiB
Rust
#![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, 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_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,
|
|
},
|
|
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<PathBuf>,
|
|
},
|
|
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<PathBuf>,
|
|
},
|
|
RuntimeCompareRt3105Profile {
|
|
smp_paths: Vec<PathBuf>,
|
|
},
|
|
RuntimeCompareCandidateTable {
|
|
smp_paths: Vec<PathBuf>,
|
|
},
|
|
RuntimeCompareRecipeBookLines {
|
|
smp_paths: Vec<PathBuf>,
|
|
},
|
|
RuntimeCompareSetupPayloadCore {
|
|
smp_paths: Vec<PathBuf>,
|
|
},
|
|
RuntimeCompareSetupLaunchPayload {
|
|
smp_paths: Vec<PathBuf>,
|
|
},
|
|
RuntimeComparePostSpecialConditionsScalars {
|
|
smp_paths: Vec<PathBuf>,
|
|
},
|
|
RuntimeScanCandidateTableHeaders {
|
|
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<FinanceDiffEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeFixtureSummaryReport {
|
|
fixture_id: String,
|
|
command_count: usize,
|
|
final_summary: RuntimeSummary,
|
|
expected_summary_matches: bool,
|
|
expected_summary_mismatches: Vec<String>,
|
|
expected_state_fragment_matches: bool,
|
|
expected_state_fragment_mismatches: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeStateSummaryReport {
|
|
snapshot_id: String,
|
|
summary: RuntimeSummary,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeStateDiffReport {
|
|
matches: bool,
|
|
difference_count: usize,
|
|
differences: Vec<JsonDiffEntry>,
|
|
}
|
|
|
|
#[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 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<String, usize>,
|
|
dispatch_descriptor_occurrence_counts: BTreeMap<String, usize>,
|
|
dispatch_descriptor_map_counts: BTreeMap<String, usize>,
|
|
dispatch_descriptor_occurrences:
|
|
BTreeMap<String, Vec<RuntimeCompactEventDispatchClusterOccurrence>>,
|
|
unknown_descriptor_ids: Vec<u32>,
|
|
unknown_descriptor_special_condition_label_matches: Vec<String>,
|
|
unknown_descriptor_occurrences:
|
|
BTreeMap<u32, Vec<RuntimeCompactEventDispatchClusterOccurrence>>,
|
|
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<String, usize>,
|
|
add_building_descriptor_map_counts: BTreeMap<String, usize>,
|
|
add_building_signature_family_occurrence_counts: BTreeMap<String, usize>,
|
|
add_building_signature_family_map_counts: BTreeMap<String, usize>,
|
|
add_building_condition_tuple_occurrence_counts: BTreeMap<String, usize>,
|
|
add_building_condition_tuple_map_counts: BTreeMap<String, usize>,
|
|
add_building_signature_condition_cluster_occurrence_counts: BTreeMap<String, usize>,
|
|
add_building_signature_condition_cluster_map_counts: BTreeMap<String, usize>,
|
|
add_building_signature_condition_cluster_descriptor_keys: BTreeMap<String, Vec<String>>,
|
|
add_building_signature_condition_cluster_non_add_building_descriptor_keys:
|
|
BTreeMap<String, Vec<String>>,
|
|
}
|
|
|
|
#[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<String, usize>,
|
|
dispatch_descriptor_occurrence_counts: BTreeMap<String, usize>,
|
|
dispatch_descriptor_map_counts: BTreeMap<String, usize>,
|
|
unknown_descriptor_ids: Vec<u32>,
|
|
unknown_descriptor_special_condition_label_matches: Vec<String>,
|
|
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<String, usize>,
|
|
add_building_descriptor_map_counts: BTreeMap<String, usize>,
|
|
add_building_signature_family_occurrence_counts: BTreeMap<String, usize>,
|
|
add_building_signature_family_map_counts: BTreeMap<String, usize>,
|
|
add_building_condition_tuple_occurrence_counts: BTreeMap<String, usize>,
|
|
add_building_condition_tuple_map_counts: BTreeMap<String, usize>,
|
|
add_building_signature_condition_cluster_occurrence_counts: BTreeMap<String, usize>,
|
|
add_building_signature_condition_cluster_map_counts: BTreeMap<String, usize>,
|
|
add_building_signature_condition_cluster_descriptor_keys: BTreeMap<String, Vec<String>>,
|
|
add_building_signature_condition_cluster_non_add_building_descriptor_keys:
|
|
BTreeMap<String, Vec<String>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
struct RuntimeCompactEventDispatchClusterOccurrence {
|
|
path: String,
|
|
record_index: usize,
|
|
live_entry_id: u32,
|
|
payload_family: String,
|
|
trigger_kind: Option<u8>,
|
|
signature_family: Option<String>,
|
|
condition_tuples: Vec<RuntimeCompactEventDispatchClusterConditionTuple>,
|
|
rows: Vec<RuntimeCompactEventDispatchClusterRow>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
struct RuntimeCompactEventDispatchClusterConditionTuple {
|
|
raw_condition_id: i32,
|
|
subtype: u8,
|
|
metric: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
struct RuntimeCompactEventDispatchClusterRow {
|
|
group_index: usize,
|
|
descriptor_id: u32,
|
|
descriptor_label: Option<String>,
|
|
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<RuntimeClassicProfileDifferenceValue>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeClassicProfileComparisonReport {
|
|
file_count: usize,
|
|
matches: bool,
|
|
common_profile_family: Option<String>,
|
|
samples: Vec<RuntimeClassicProfileSample>,
|
|
difference_count: usize,
|
|
differences: Vec<RuntimeClassicProfileDifference>,
|
|
}
|
|
|
|
#[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<String>,
|
|
samples: Vec<RuntimeRt3105ProfileSample>,
|
|
difference_count: usize,
|
|
differences: Vec<RuntimeClassicProfileDifference>,
|
|
}
|
|
|
|
#[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<String>,
|
|
footer_progress_word_0_hex: String,
|
|
footer_progress_word_1_hex: String,
|
|
availability_by_name: BTreeMap<String, u32>,
|
|
}
|
|
|
|
#[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<String>,
|
|
entries: Vec<RuntimeCandidateTableEntrySample>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeCandidateTableComparisonReport {
|
|
file_count: usize,
|
|
matches: bool,
|
|
common_profile_family: Option<String>,
|
|
common_semantic_family: Option<String>,
|
|
samples: Vec<RuntimeCandidateTableSample>,
|
|
difference_count: usize,
|
|
differences: Vec<RuntimeClassicProfileDifference>,
|
|
}
|
|
|
|
#[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<String, String>,
|
|
book_line_area_kind_by_index: BTreeMap<String, String>,
|
|
max_annual_production_word_hex_by_book: BTreeMap<String, String>,
|
|
line_kind_by_path: BTreeMap<String, String>,
|
|
mode_word_hex_by_path: BTreeMap<String, String>,
|
|
annual_amount_word_hex_by_path: BTreeMap<String, String>,
|
|
supplied_cargo_token_word_hex_by_path: BTreeMap<String, String>,
|
|
demanded_cargo_token_word_hex_by_path: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeRecipeBookLineComparisonReport {
|
|
file_count: usize,
|
|
matches: bool,
|
|
content_matches: bool,
|
|
common_profile_family: Option<String>,
|
|
samples: Vec<RuntimeRecipeBookLineSample>,
|
|
difference_count: usize,
|
|
differences: Vec<RuntimeClassicProfileDifference>,
|
|
content_difference_count: usize,
|
|
content_differences: Vec<RuntimeClassicProfileDifference>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct RuntimeRecipeBookLineScanSample {
|
|
path: String,
|
|
profile_family: String,
|
|
source_kind: String,
|
|
nonzero_mode_paths: BTreeMap<String, String>,
|
|
nonzero_supplied_token_paths: BTreeMap<String, String>,
|
|
nonzero_demanded_token_paths: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeRecipeBookLineFieldSummary {
|
|
line_path: String,
|
|
file_count_present: usize,
|
|
distinct_value_count: usize,
|
|
sample_value_hexes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeRecipeBookLineFamilySummary {
|
|
profile_family: String,
|
|
source_kinds: Vec<String>,
|
|
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<String>,
|
|
stable_nonzero_supplied_token_paths: Vec<String>,
|
|
stable_nonzero_demanded_token_paths: Vec<String>,
|
|
mode_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
|
|
supplied_token_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
|
|
demanded_token_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
|
|
sample_paths: Vec<String>,
|
|
}
|
|
|
|
#[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<RuntimeRecipeBookLineFamilySummary>,
|
|
}
|
|
|
|
#[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<String>,
|
|
candidate_header_word_1_hex: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeSetupPayloadCoreComparisonReport {
|
|
file_count: usize,
|
|
matches: bool,
|
|
samples: Vec<RuntimeSetupPayloadCoreSample>,
|
|
difference_count: usize,
|
|
differences: Vec<RuntimeClassicProfileDifference>,
|
|
}
|
|
|
|
#[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<String>,
|
|
campaign_progress_page_index: Option<usize>,
|
|
launch_selector_byte_0x33: u8,
|
|
launch_selector_byte_0x33_hex: String,
|
|
launch_token_block_0x23_0x32_hex: String,
|
|
campaign_selector_values: BTreeMap<String, u8>,
|
|
nonzero_campaign_selector_values: BTreeMap<String, u8>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeSetupLaunchPayloadComparisonReport {
|
|
file_count: usize,
|
|
matches: bool,
|
|
samples: Vec<RuntimeSetupLaunchPayloadSample>,
|
|
difference_count: usize,
|
|
differences: Vec<RuntimeClassicProfileDifference>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimePostSpecialConditionsScalarSample {
|
|
path: String,
|
|
profile_family: String,
|
|
source_kind: String,
|
|
nonzero_relative_offset_hexes: Vec<String>,
|
|
values_by_relative_offset_hex: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimePostSpecialConditionsScalarComparisonReport {
|
|
file_count: usize,
|
|
matches: bool,
|
|
common_profile_family: Option<String>,
|
|
samples: Vec<RuntimePostSpecialConditionsScalarSample>,
|
|
difference_count: usize,
|
|
differences: Vec<RuntimeClassicProfileDifference>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeCandidateTableHeaderCluster {
|
|
header_word_0_hex: String,
|
|
header_word_1_hex: String,
|
|
file_count: usize,
|
|
profile_families: Vec<String>,
|
|
source_kinds: Vec<String>,
|
|
zero_trailer_count_min: usize,
|
|
zero_trailer_count_max: usize,
|
|
zero_trailer_count_values: Vec<usize>,
|
|
distinct_zero_name_set_count: usize,
|
|
sample_paths: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeCandidateTableHeaderScanReport {
|
|
root_path: String,
|
|
file_count: usize,
|
|
cluster_count: usize,
|
|
skipped_file_count: usize,
|
|
clusters: Vec<RuntimeCandidateTableHeaderCluster>,
|
|
}
|
|
|
|
#[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<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
struct RuntimeSpecialConditionsScanSample {
|
|
path: String,
|
|
profile_family: String,
|
|
source_kind: String,
|
|
enabled_visible_count: usize,
|
|
enabled_visible_labels: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeSpecialConditionsSlotSummary {
|
|
slot_index: u8,
|
|
label: String,
|
|
file_count_enabled: usize,
|
|
sample_paths: Vec<String>,
|
|
}
|
|
|
|
#[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<RuntimeSpecialConditionsSlotSummary>,
|
|
sample_files_with_any_enabled: Vec<RuntimeSpecialConditionsScanSample>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct RuntimePostSpecialConditionsScalarScanSample {
|
|
path: String,
|
|
profile_family: String,
|
|
source_kind: String,
|
|
nonzero_relative_offsets: Vec<usize>,
|
|
values_by_relative_offset_hex: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimePostSpecialConditionsScalarOffsetSummary {
|
|
relative_offset_hex: String,
|
|
file_count_present: usize,
|
|
distinct_value_count: usize,
|
|
sample_value_hexes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimePostSpecialConditionsScalarFamilySummary {
|
|
profile_family: String,
|
|
source_kinds: Vec<String>,
|
|
file_count: usize,
|
|
files_with_any_nonzero_count: usize,
|
|
distinct_nonzero_offset_set_count: usize,
|
|
stable_nonzero_relative_offset_hexes: Vec<String>,
|
|
union_nonzero_relative_offset_hexes: Vec<String>,
|
|
offset_summaries: Vec<RuntimePostSpecialConditionsScalarOffsetSummary>,
|
|
sample_paths: Vec<String>,
|
|
}
|
|
|
|
#[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<RuntimePostSpecialConditionsScalarFamilySummary>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct RuntimePostSpecialConditionsTailScanSample {
|
|
path: String,
|
|
profile_family: String,
|
|
source_kind: String,
|
|
nonzero_relative_offsets: Vec<usize>,
|
|
values_by_relative_offset_hex: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimePostSpecialConditionsTailOffsetSummary {
|
|
relative_offset_hex: String,
|
|
file_count_present: usize,
|
|
distinct_value_count: usize,
|
|
sample_value_hexes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimePostSpecialConditionsTailFamilySummary {
|
|
profile_family: String,
|
|
source_kinds: Vec<String>,
|
|
file_count: usize,
|
|
files_with_any_nonzero_count: usize,
|
|
distinct_nonzero_offset_set_count: usize,
|
|
stable_nonzero_relative_offset_hexes: Vec<String>,
|
|
union_nonzero_relative_offset_hexes: Vec<String>,
|
|
offset_summaries: Vec<RuntimePostSpecialConditionsTailOffsetSummary>,
|
|
sample_paths: Vec<String>,
|
|
}
|
|
|
|
#[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<RuntimePostSpecialConditionsTailFamilySummary>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct RuntimeAlignedRuntimeRuleBandScanSample {
|
|
path: String,
|
|
profile_family: String,
|
|
source_kind: String,
|
|
nonzero_band_indices: Vec<usize>,
|
|
values_by_band_index: BTreeMap<usize, String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeAlignedRuntimeRuleBandOffsetSummary {
|
|
band_index: usize,
|
|
relative_offset_hex: String,
|
|
lane_kind: String,
|
|
known_label: Option<String>,
|
|
file_count_present: usize,
|
|
distinct_value_count: usize,
|
|
sample_value_hexes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct RuntimeAlignedRuntimeRuleBandFamilySummary {
|
|
profile_family: String,
|
|
source_kinds: Vec<String>,
|
|
file_count: usize,
|
|
files_with_any_nonzero_count: usize,
|
|
distinct_nonzero_index_set_count: usize,
|
|
stable_nonzero_band_indices: Vec<usize>,
|
|
union_nonzero_band_indices: Vec<usize>,
|
|
offset_summaries: Vec<RuntimeAlignedRuntimeRuleBandOffsetSummary>,
|
|
sample_paths: Vec<String>,
|
|
}
|
|
|
|
#[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<RuntimeAlignedRuntimeRuleBandFamilySummary>,
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
fn main() {
|
|
if let Err(err) = real_main() {
|
|
eprintln!("error: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|
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::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::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<Command, Box<dyn std::error::Error>> {
|
|
let args: Vec<String> = 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, 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-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 <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime inspect-candidate-table <file.smp> | runtime inspect-compact-event-dispatch-cluster <maps-dir> | runtime inspect-compact-event-dispatch-cluster-counts <maps-dir> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime inspect-save-company-chairman <file.smp> | runtime inspect-save-placed-structure-triplets <file.smp> | runtime compare-region-fixed-row-runs <left.gms> <right.gms> | runtime inspect-periodic-company-service-trace <file.smp> | runtime inspect-region-service-trace <file.smp> | runtime inspect-infrastructure-asset-trace <file.smp> | runtime inspect-save-region-queued-notice-records <file.smp> | runtime inspect-placed-structure-dynamic-side-buffer <file.smp> | runtime inspect-unclassified-save-collections <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime export-overlay-import <snapshot.json> <save-slice.json> <overlay-import.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-cargo-types <CargoTypes-dir> | runtime inspect-building-type-sources <BuildingTypes-dir> [building-bindings.json] | runtime inspect-cargo-skins <Cargo106.PK4> | runtime inspect-cargo-economy-sources <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-production-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-price-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
|
|
.into(),
|
|
),
|
|
}
|
|
}
|
|
|
|
fn run_finance_eval(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let outcome = load_finance_outcome(snapshot_path)?;
|
|
println!("{}", serde_json::to_string_pretty(&outcome)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_finance_diff(left_path: &Path, right_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let left = load_finance_outcome(left_path)?;
|
|
let right = load_finance_outcome(right_path)?;
|
|
let report = diff_finance_outcomes(&left, &right)?;
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_validate_fixture(fixture_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let fixture = load_fixture_document(fixture_path)?;
|
|
let report = validate_fixture_document(&fixture);
|
|
print_runtime_validation_report(&report)?;
|
|
if !report.valid {
|
|
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_summarize_fixture(fixture_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let fixture = load_fixture_document(fixture_path)?;
|
|
let validation_report = validate_fixture_document(&fixture);
|
|
if !validation_report.valid {
|
|
print_runtime_validation_report(&validation_report)?;
|
|
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
|
|
}
|
|
|
|
let mut state = fixture.state.clone();
|
|
for command in &fixture.commands {
|
|
execute_step_command(&mut state, command)?;
|
|
}
|
|
|
|
let final_summary = RuntimeSummary::from_state(&state);
|
|
let expected_summary_mismatches = fixture.expected_summary.compare(&final_summary);
|
|
let expected_state_fragment_mismatches = match &fixture.expected_state_fragment {
|
|
Some(expected_fragment) => {
|
|
let normalized_state = normalize_runtime_state(&state)?;
|
|
compare_expected_state_fragment(expected_fragment, &normalized_state)
|
|
}
|
|
None => Vec::new(),
|
|
};
|
|
let report = RuntimeFixtureSummaryReport {
|
|
fixture_id: fixture.fixture_id,
|
|
command_count: fixture.commands.len(),
|
|
expected_summary_matches: expected_summary_mismatches.is_empty(),
|
|
expected_summary_mismatches: expected_summary_mismatches.clone(),
|
|
expected_state_fragment_matches: expected_state_fragment_mismatches.is_empty(),
|
|
expected_state_fragment_mismatches: expected_state_fragment_mismatches.clone(),
|
|
final_summary,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
|
|
if !expected_summary_mismatches.is_empty() || !expected_state_fragment_mismatches.is_empty() {
|
|
let mut mismatch_messages = expected_summary_mismatches;
|
|
mismatch_messages.extend(expected_state_fragment_mismatches);
|
|
return Err(format!(
|
|
"fixture summary mismatched expected output: {}",
|
|
mismatch_messages.join("; ")
|
|
)
|
|
.into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_export_fixture_state(
|
|
fixture_path: &Path,
|
|
output_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let fixture = load_fixture_document(fixture_path)?;
|
|
let validation_report = validate_fixture_document(&fixture);
|
|
if !validation_report.valid {
|
|
print_runtime_validation_report(&validation_report)?;
|
|
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
|
|
}
|
|
|
|
let mut state = fixture.state.clone();
|
|
for command in &fixture.commands {
|
|
execute_step_command(&mut state, command)?;
|
|
}
|
|
|
|
let snapshot = RuntimeSnapshotDocument {
|
|
format_version: SNAPSHOT_FORMAT_VERSION,
|
|
snapshot_id: format!("{}-final-state", fixture.fixture_id),
|
|
source: RuntimeSnapshotSource {
|
|
source_fixture_id: Some(fixture.fixture_id.clone()),
|
|
description: Some(format!(
|
|
"Exported final runtime state for fixture {}",
|
|
fixture.fixture_id
|
|
)),
|
|
},
|
|
state,
|
|
};
|
|
save_runtime_snapshot_document(output_path, &snapshot)?;
|
|
let summary = snapshot.summary();
|
|
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&RuntimeStateSummaryReport {
|
|
snapshot_id: snapshot.snapshot_id,
|
|
summary,
|
|
})?
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_summarize_state(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
if let Ok(snapshot) = load_runtime_snapshot_document(snapshot_path) {
|
|
validate_runtime_snapshot_document(&snapshot)
|
|
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
|
|
let report = RuntimeStateSummaryReport {
|
|
snapshot_id: snapshot.snapshot_id.clone(),
|
|
summary: snapshot.summary(),
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
return Ok(());
|
|
}
|
|
|
|
let 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<dyn std::error::Error>> {
|
|
let left = load_normalized_runtime_state(left_path)?;
|
|
let right = load_normalized_runtime_state(right_path)?;
|
|
let differences = diff_json_values(&left, &right);
|
|
let report = RuntimeStateDiffReport {
|
|
matches: differences.is_empty(),
|
|
difference_count: differences.len(),
|
|
differences,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn load_normalized_runtime_state(path: &Path) -> Result<Value, Box<dyn std::error::Error>> {
|
|
if let Ok(snapshot) = load_runtime_snapshot_document(path) {
|
|
validate_runtime_snapshot_document(&snapshot)
|
|
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
|
|
return normalize_runtime_state(&snapshot.state);
|
|
}
|
|
|
|
let import = load_runtime_state_import(path)?;
|
|
normalize_runtime_state(&import.state)
|
|
}
|
|
|
|
fn run_runtime_import_state(
|
|
input_path: &Path,
|
|
output_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
let report = RuntimeSmpInspectionOutput {
|
|
path: smp_path.display().to_string(),
|
|
inspection: inspect_smp_file(smp_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_compact_event_dispatch_cluster(
|
|
root_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = build_runtime_compact_event_dispatch_cluster_report(root_path)?;
|
|
let output = RuntimeCompactEventDispatchClusterOutput {
|
|
root_path: root_path.display().to_string(),
|
|
report,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&output)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_compact_event_dispatch_cluster_counts(
|
|
root_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = build_runtime_compact_event_dispatch_cluster_report(root_path)?;
|
|
let output = RuntimeCompactEventDispatchClusterCountsOutput {
|
|
root_path: root_path.display().to_string(),
|
|
report: RuntimeCompactEventDispatchClusterCountsReport {
|
|
maps_scanned: report.maps_scanned,
|
|
maps_with_event_runtime_collection: report.maps_with_event_runtime_collection,
|
|
maps_with_dispatch_strip_records: report.maps_with_dispatch_strip_records,
|
|
dispatch_strip_record_count: report.dispatch_strip_record_count,
|
|
dispatch_strip_records_with_trigger_kind: report
|
|
.dispatch_strip_records_with_trigger_kind,
|
|
dispatch_strip_records_missing_trigger_kind: report
|
|
.dispatch_strip_records_missing_trigger_kind,
|
|
dispatch_strip_payload_families: report.dispatch_strip_payload_families,
|
|
dispatch_descriptor_occurrence_counts: report.dispatch_descriptor_occurrence_counts,
|
|
dispatch_descriptor_map_counts: report.dispatch_descriptor_map_counts,
|
|
unknown_descriptor_ids: report.unknown_descriptor_ids,
|
|
unknown_descriptor_special_condition_label_matches: report
|
|
.unknown_descriptor_special_condition_label_matches,
|
|
add_building_dispatch_record_count: report.add_building_dispatch_record_count,
|
|
add_building_dispatch_records_with_trigger_kind: report
|
|
.add_building_dispatch_records_with_trigger_kind,
|
|
add_building_dispatch_records_missing_trigger_kind: report
|
|
.add_building_dispatch_records_missing_trigger_kind,
|
|
add_building_descriptor_occurrence_counts: report
|
|
.add_building_descriptor_occurrence_counts,
|
|
add_building_descriptor_map_counts: report.add_building_descriptor_map_counts,
|
|
add_building_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<RuntimeCompactEventDispatchClusterReport, Box<dyn std::error::Error>> {
|
|
let mut input_paths = Vec::new();
|
|
collect_compact_event_dispatch_cluster_input_paths(root_path, &mut input_paths)?;
|
|
input_paths.sort();
|
|
|
|
let mut maps_with_event_runtime_collection = 0usize;
|
|
let mut maps_with_dispatch_strip_records = 0usize;
|
|
let mut dispatch_strip_record_count = 0usize;
|
|
let mut dispatch_strip_records_with_trigger_kind = 0usize;
|
|
let mut dispatch_strip_records_missing_trigger_kind = 0usize;
|
|
let mut dispatch_strip_payload_families = BTreeMap::<String, usize>::new();
|
|
let mut dispatch_descriptor_occurrence_counts = BTreeMap::<String, usize>::new();
|
|
let mut dispatch_descriptor_map_counts = BTreeMap::<String, usize>::new();
|
|
let mut add_building_dispatch_record_count = 0usize;
|
|
let mut add_building_dispatch_records_with_trigger_kind = 0usize;
|
|
let mut add_building_dispatch_records_missing_trigger_kind = 0usize;
|
|
let mut add_building_descriptor_occurrence_counts = BTreeMap::<String, usize>::new();
|
|
let mut add_building_descriptor_map_counts = BTreeMap::<String, usize>::new();
|
|
let mut add_building_signature_family_occurrence_counts = BTreeMap::<String, usize>::new();
|
|
let mut add_building_signature_family_map_counts = BTreeMap::<String, usize>::new();
|
|
let mut add_building_condition_tuple_occurrence_counts = BTreeMap::<String, usize>::new();
|
|
let mut add_building_condition_tuple_map_counts = BTreeMap::<String, usize>::new();
|
|
let mut add_building_signature_condition_cluster_occurrence_counts =
|
|
BTreeMap::<String, usize>::new();
|
|
let mut add_building_signature_condition_cluster_map_counts = BTreeMap::<String, usize>::new();
|
|
let mut signature_condition_cluster_descriptor_keys =
|
|
BTreeMap::<String, BTreeSet<String>>::new();
|
|
let mut add_building_signature_condition_clusters = BTreeSet::<String>::new();
|
|
let mut dispatch_descriptor_occurrences =
|
|
BTreeMap::<String, Vec<RuntimeCompactEventDispatchClusterOccurrence>>::new();
|
|
let mut unknown_descriptor_occurrences =
|
|
BTreeMap::<u32, Vec<RuntimeCompactEventDispatchClusterOccurrence>>::new();
|
|
|
|
for path in &input_paths {
|
|
let inspection = inspect_smp_file(path)?;
|
|
let Some(summary) = inspection.event_runtime_collection_summary else {
|
|
continue;
|
|
};
|
|
maps_with_event_runtime_collection += 1;
|
|
|
|
let mut map_dispatch_strip_record_count = 0usize;
|
|
let mut map_descriptor_keys = BTreeSet::<String>::new();
|
|
let mut map_add_building_descriptor_keys = BTreeSet::<String>::new();
|
|
let mut map_add_building_signature_families = BTreeSet::<String>::new();
|
|
let mut map_add_building_condition_tuples = BTreeSet::<String>::new();
|
|
let mut map_add_building_signature_condition_clusters = BTreeSet::<String>::new();
|
|
for record in &summary.records {
|
|
let matching_rows = record
|
|
.grouped_effect_rows
|
|
.iter()
|
|
.filter(|row| compact_event_dispatch_strip_opcode(row.opcode))
|
|
.fold(
|
|
BTreeMap::<u32, Vec<RuntimeCompactEventDispatchClusterRow>>::new(),
|
|
|mut grouped, row| {
|
|
grouped.entry(row.descriptor_id).or_default().push(
|
|
RuntimeCompactEventDispatchClusterRow {
|
|
group_index: row.group_index,
|
|
descriptor_id: row.descriptor_id,
|
|
descriptor_label: row.descriptor_label.clone(),
|
|
opcode: row.opcode,
|
|
raw_scalar_value: row.raw_scalar_value,
|
|
},
|
|
);
|
|
grouped
|
|
},
|
|
);
|
|
if matching_rows.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
map_dispatch_strip_record_count += 1;
|
|
if record.trigger_kind.is_some() {
|
|
dispatch_strip_records_with_trigger_kind += 1;
|
|
} else {
|
|
dispatch_strip_records_missing_trigger_kind += 1;
|
|
}
|
|
*dispatch_strip_payload_families
|
|
.entry(record.payload_family.clone())
|
|
.or_insert(0) += 1;
|
|
let mut record_has_add_building = false;
|
|
let condition_tuples = record
|
|
.standalone_condition_rows
|
|
.iter()
|
|
.map(|row| RuntimeCompactEventDispatchClusterConditionTuple {
|
|
raw_condition_id: row.raw_condition_id,
|
|
subtype: row.subtype,
|
|
metric: row.metric.clone(),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let signature_family = compact_event_signature_family_from_notes(&record.notes);
|
|
let condition_tuple_family =
|
|
compact_event_dispatch_condition_tuple_family(&condition_tuples);
|
|
let 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_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 signature_family in map_add_building_signature_families {
|
|
*add_building_signature_family_map_counts
|
|
.entry(signature_family)
|
|
.or_insert(0) += 1;
|
|
}
|
|
for condition_tuple_family in map_add_building_condition_tuples {
|
|
*add_building_condition_tuple_map_counts
|
|
.entry(condition_tuple_family)
|
|
.or_insert(0) += 1;
|
|
}
|
|
for signature_condition_cluster in map_add_building_signature_condition_clusters {
|
|
*add_building_signature_condition_cluster_map_counts
|
|
.entry(signature_condition_cluster)
|
|
.or_insert(0) += 1;
|
|
}
|
|
}
|
|
|
|
let unknown_descriptor_ids = unknown_descriptor_occurrences
|
|
.keys()
|
|
.copied()
|
|
.collect::<Vec<_>>();
|
|
let unknown_descriptor_special_condition_label_matches = unknown_descriptor_ids
|
|
.iter()
|
|
.filter_map(|descriptor_id| {
|
|
special_condition_label_for_compact_dispatch_descriptor(*descriptor_id)
|
|
.map(|label| format!("{descriptor_id} -> {label}"))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let add_building_signature_condition_cluster_descriptor_keys =
|
|
add_building_signature_condition_clusters
|
|
.iter()
|
|
.map(|cluster| {
|
|
let keys = signature_condition_cluster_descriptor_keys
|
|
.get(cluster)
|
|
.map(|keys| keys.iter().cloned().collect::<Vec<_>>())
|
|
.unwrap_or_default();
|
|
(cluster.clone(), keys)
|
|
})
|
|
.collect::<BTreeMap<_, _>>();
|
|
let add_building_signature_condition_cluster_non_add_building_descriptor_keys =
|
|
add_building_signature_condition_cluster_descriptor_keys
|
|
.iter()
|
|
.map(|(cluster, keys)| {
|
|
let filtered = keys
|
|
.iter()
|
|
.filter(|key| !key.contains("Add Building"))
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
(cluster.clone(), filtered)
|
|
})
|
|
.collect::<BTreeMap<_, _>>();
|
|
|
|
Ok(RuntimeCompactEventDispatchClusterReport {
|
|
maps_scanned: input_paths.len(),
|
|
maps_with_event_runtime_collection,
|
|
maps_with_dispatch_strip_records,
|
|
dispatch_strip_record_count,
|
|
dispatch_strip_records_with_trigger_kind,
|
|
dispatch_strip_records_missing_trigger_kind,
|
|
dispatch_strip_payload_families,
|
|
dispatch_descriptor_occurrence_counts,
|
|
dispatch_descriptor_map_counts,
|
|
dispatch_descriptor_occurrences,
|
|
unknown_descriptor_ids,
|
|
unknown_descriptor_special_condition_label_matches,
|
|
unknown_descriptor_occurrences,
|
|
add_building_dispatch_record_count,
|
|
add_building_dispatch_records_with_trigger_kind,
|
|
add_building_dispatch_records_missing_trigger_kind,
|
|
add_building_descriptor_occurrence_counts,
|
|
add_building_descriptor_map_counts,
|
|
add_building_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<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(smp_path)?;
|
|
let summary = inspection.save_load_summary.ok_or_else(|| {
|
|
format!(
|
|
"{} did not expose a recognizable save-load summary",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
let report = RuntimeSaveLoadSummaryOutput {
|
|
path: smp_path.display().to_string(),
|
|
summary,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_load_save_slice(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeLoadedSaveSliceOutput {
|
|
path: smp_path.display().to_string(),
|
|
save_slice: load_save_slice_file(smp_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_save_company_chairman(
|
|
smp_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeSaveCompanyChairmanAnalysisOutput {
|
|
path: smp_path.display().to_string(),
|
|
analysis: inspect_save_company_and_chairman_analysis_file(smp_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_save_placed_structure_triplets(
|
|
smp_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let analysis = inspect_save_company_and_chairman_analysis_file(smp_path)?;
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&analysis.placed_structure_record_triplets)?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_compare_region_fixed_row_runs(
|
|
left_path: &Path,
|
|
right_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let left = inspect_save_company_and_chairman_analysis_file(left_path)?;
|
|
let right = inspect_save_company_and_chairman_analysis_file(right_path)?;
|
|
let comparison = compare_save_region_fixed_row_run_candidates(&left, &right)
|
|
.ok_or("save inspection did not expose grounded region fixed-row candidate probes")?;
|
|
let report = RuntimeRegionFixedRowRunComparisonOutput {
|
|
left_path: left_path.display().to_string(),
|
|
right_path: right_path.display().to_string(),
|
|
comparison,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_periodic_company_service_trace(
|
|
smp_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimePeriodicCompanyServiceTraceOutput {
|
|
path: smp_path.display().to_string(),
|
|
trace: inspect_save_periodic_company_service_trace_file(smp_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_region_service_trace(
|
|
smp_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeRegionServiceTraceOutput {
|
|
path: smp_path.display().to_string(),
|
|
trace: inspect_save_region_service_trace_file(smp_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_infrastructure_asset_trace(
|
|
smp_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeInfrastructureAssetTraceOutput {
|
|
path: smp_path.display().to_string(),
|
|
trace: inspect_save_infrastructure_asset_trace_file(smp_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_save_region_queued_notice_records(
|
|
smp_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&inspect_save_region_queued_notice_records_file(smp_path)?)?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_placed_structure_dynamic_side_buffer(
|
|
smp_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&inspect_save_placed_structure_dynamic_side_buffer_file(
|
|
smp_path
|
|
)?)?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_unclassified_save_collections(
|
|
smp_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&inspect_unclassified_save_collection_headers_file(
|
|
smp_path
|
|
)?)?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_import_save_state(
|
|
smp_path: &Path,
|
|
output_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
let save_slice = load_save_slice_file(smp_path)?;
|
|
let report = export_runtime_save_slice_document(smp_path, output_path, save_slice)?;
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_export_overlay_import(
|
|
snapshot_path: &Path,
|
|
save_slice_path: &Path,
|
|
output_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report =
|
|
export_runtime_overlay_import_document(snapshot_path, save_slice_path, output_path)?;
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn export_runtime_save_slice_document(
|
|
smp_path: &Path,
|
|
output_path: &Path,
|
|
save_slice: SmpLoadedSaveSlice,
|
|
) -> Result<RuntimeSaveSliceExportOutput, Box<dyn std::error::Error>> {
|
|
let document = RuntimeSaveSliceDocument {
|
|
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
|
|
save_slice_id: smp_path
|
|
.file_stem()
|
|
.and_then(|stem| stem.to_str())
|
|
.unwrap_or("save-slice")
|
|
.to_string(),
|
|
source: RuntimeSaveSliceDocumentSource {
|
|
description: Some(format!(
|
|
"Exported loaded save slice from {}",
|
|
smp_path.display()
|
|
)),
|
|
original_save_filename: smp_path
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.map(ToString::to_string),
|
|
original_save_sha256: None,
|
|
notes: vec![],
|
|
},
|
|
save_slice,
|
|
};
|
|
save_runtime_save_slice_document(output_path, &document)?;
|
|
Ok(RuntimeSaveSliceExportOutput {
|
|
path: smp_path.display().to_string(),
|
|
output_path: output_path.display().to_string(),
|
|
save_slice_id: document.save_slice_id,
|
|
})
|
|
}
|
|
|
|
fn export_runtime_overlay_import_document(
|
|
snapshot_path: &Path,
|
|
save_slice_path: &Path,
|
|
output_path: &Path,
|
|
) -> Result<RuntimeOverlayImportExportOutput, Box<dyn std::error::Error>> {
|
|
let import_id = output_path
|
|
.file_stem()
|
|
.and_then(|stem| stem.to_str())
|
|
.unwrap_or("overlay-import")
|
|
.to_string();
|
|
let document = RuntimeOverlayImportDocument {
|
|
format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION,
|
|
import_id: import_id.clone(),
|
|
source: RuntimeOverlayImportDocumentSource {
|
|
description: Some(format!(
|
|
"Overlay import referencing {} and {}",
|
|
snapshot_path.display(),
|
|
save_slice_path.display()
|
|
)),
|
|
notes: vec![],
|
|
},
|
|
base_snapshot_path: snapshot_path.display().to_string(),
|
|
save_slice_path: save_slice_path.display().to_string(),
|
|
};
|
|
save_runtime_overlay_import_document(output_path, &document)?;
|
|
Ok(RuntimeOverlayImportExportOutput {
|
|
output_path: output_path.display().to_string(),
|
|
import_id,
|
|
base_snapshot_path: document.base_snapshot_path,
|
|
save_slice_path: document.save_slice_path,
|
|
})
|
|
}
|
|
|
|
fn run_runtime_inspect_pk4(pk4_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimePk4InspectionOutput {
|
|
path: pk4_path.display().to_string(),
|
|
inspection: inspect_pk4_file(pk4_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_cargo_types(
|
|
cargo_types_dir: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeCargoTypeInspectionOutput {
|
|
path: cargo_types_dir.display().to_string(),
|
|
inspection: inspect_cargo_types_dir(cargo_types_dir)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_building_type_sources(
|
|
building_types_dir: &Path,
|
|
bindings_path: Option<&Path>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeBuildingTypeInspectionOutput {
|
|
path: building_types_dir.display().to_string(),
|
|
inspection: inspect_building_types_dir_with_bindings(building_types_dir, bindings_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_cargo_skins(
|
|
cargo_skin_pk4_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeCargoSkinInspectionOutput {
|
|
path: cargo_skin_pk4_path.display().to_string(),
|
|
inspection: inspect_cargo_skin_pk4(cargo_skin_pk4_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_cargo_economy_sources(
|
|
cargo_types_dir: &Path,
|
|
cargo_skin_pk4_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let cargo_bindings_path =
|
|
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
|
|
let report = RuntimeCargoEconomyInspectionOutput {
|
|
cargo_types_dir: cargo_types_dir.display().to_string(),
|
|
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
|
|
inspection: inspect_cargo_economy_sources_with_bindings(
|
|
cargo_types_dir,
|
|
cargo_skin_pk4_path,
|
|
Some(cargo_bindings_path),
|
|
)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_cargo_production_selector(
|
|
cargo_types_dir: &Path,
|
|
cargo_skin_pk4_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let cargo_bindings_path =
|
|
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
|
|
let inspection = inspect_cargo_economy_sources_with_bindings(
|
|
cargo_types_dir,
|
|
cargo_skin_pk4_path,
|
|
Some(cargo_bindings_path),
|
|
)?;
|
|
let selector = inspection
|
|
.production_selector
|
|
.ok_or("named cargo production selector is not available in the checked-in bindings")?;
|
|
let report = RuntimeCargoSelectorInspectionOutput {
|
|
cargo_types_dir: cargo_types_dir.display().to_string(),
|
|
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
|
|
selector,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_cargo_price_selector(
|
|
cargo_types_dir: &Path,
|
|
cargo_skin_pk4_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let cargo_bindings_path =
|
|
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
|
|
let inspection = inspect_cargo_economy_sources_with_bindings(
|
|
cargo_types_dir,
|
|
cargo_skin_pk4_path,
|
|
Some(cargo_bindings_path),
|
|
)?;
|
|
let report = RuntimeCargoSelectorInspectionOutput {
|
|
cargo_types_dir: cargo_types_dir.display().to_string(),
|
|
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
|
|
selector: inspection.price_selector,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_win(win_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeWinInspectionOutput {
|
|
path: win_path.display().to_string(),
|
|
inspection: inspect_win_file(win_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_extract_pk4_entry(
|
|
pk4_path: &Path,
|
|
entry_name: &str,
|
|
output_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimePk4ExtractionOutput {
|
|
path: pk4_path.display().to_string(),
|
|
output_path: output_path.display().to_string(),
|
|
extraction: extract_pk4_entry_file(pk4_path, entry_name, output_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_campaign_exe(exe_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = RuntimeCampaignExeInspectionOutput {
|
|
path: exe_path.display().to_string(),
|
|
inspection: inspect_campaign_exe_file(exe_path)?,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_compare_classic_profile(
|
|
smp_paths: &[PathBuf],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let samples = smp_paths
|
|
.iter()
|
|
.map(|path| load_classic_profile_sample(path))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let common_profile_family = samples
|
|
.first()
|
|
.map(|sample| sample.profile_family.clone())
|
|
.filter(|family| {
|
|
samples
|
|
.iter()
|
|
.all(|sample| sample.profile_family == *family)
|
|
});
|
|
let differences = diff_classic_profile_samples(&samples)?;
|
|
let report = RuntimeClassicProfileComparisonReport {
|
|
file_count: samples.len(),
|
|
matches: differences.is_empty(),
|
|
common_profile_family,
|
|
difference_count: differences.len(),
|
|
differences,
|
|
samples,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_compare_rt3_105_profile(
|
|
smp_paths: &[PathBuf],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let samples = smp_paths
|
|
.iter()
|
|
.map(|path| load_rt3_105_profile_sample(path))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let common_profile_family = samples
|
|
.first()
|
|
.map(|sample| sample.profile_family.clone())
|
|
.filter(|family| {
|
|
samples
|
|
.iter()
|
|
.all(|sample| sample.profile_family == *family)
|
|
});
|
|
let differences = diff_rt3_105_profile_samples(&samples)?;
|
|
let report = RuntimeRt3105ProfileComparisonReport {
|
|
file_count: samples.len(),
|
|
matches: differences.is_empty(),
|
|
common_profile_family,
|
|
difference_count: differences.len(),
|
|
differences,
|
|
samples,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_compare_candidate_table(
|
|
smp_paths: &[PathBuf],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let samples = smp_paths
|
|
.iter()
|
|
.map(|path| load_candidate_table_sample(path))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let common_profile_family = samples
|
|
.first()
|
|
.map(|sample| sample.profile_family.clone())
|
|
.filter(|family| {
|
|
samples
|
|
.iter()
|
|
.all(|sample| sample.profile_family == *family)
|
|
});
|
|
let common_semantic_family = samples
|
|
.first()
|
|
.map(|sample| sample.semantic_family.clone())
|
|
.filter(|family| {
|
|
samples
|
|
.iter()
|
|
.all(|sample| sample.semantic_family == *family)
|
|
});
|
|
let differences = diff_candidate_table_samples(&samples)?;
|
|
let report = RuntimeCandidateTableComparisonReport {
|
|
file_count: samples.len(),
|
|
matches: differences.is_empty(),
|
|
common_profile_family,
|
|
common_semantic_family,
|
|
difference_count: differences.len(),
|
|
differences,
|
|
samples,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_inspect_candidate_table(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let report = load_candidate_table_inspection_report(smp_path)?;
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_compare_recipe_book_lines(
|
|
smp_paths: &[PathBuf],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let samples = smp_paths
|
|
.iter()
|
|
.map(|path| load_recipe_book_line_sample(path))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let common_profile_family = samples
|
|
.first()
|
|
.map(|sample| sample.profile_family.clone())
|
|
.filter(|family| {
|
|
samples
|
|
.iter()
|
|
.all(|sample| sample.profile_family == *family)
|
|
});
|
|
let differences = diff_recipe_book_line_samples(&samples)?;
|
|
let content_differences = diff_recipe_book_line_content_samples(&samples)?;
|
|
let report = RuntimeRecipeBookLineComparisonReport {
|
|
file_count: samples.len(),
|
|
matches: differences.is_empty(),
|
|
content_matches: content_differences.is_empty(),
|
|
common_profile_family,
|
|
difference_count: differences.len(),
|
|
differences,
|
|
content_difference_count: content_differences.len(),
|
|
content_differences,
|
|
samples,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_compare_setup_payload_core(
|
|
smp_paths: &[PathBuf],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let samples = smp_paths
|
|
.iter()
|
|
.map(|path| load_setup_payload_core_sample(path))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let differences = diff_setup_payload_core_samples(&samples)?;
|
|
let report = RuntimeSetupPayloadCoreComparisonReport {
|
|
file_count: samples.len(),
|
|
matches: differences.is_empty(),
|
|
difference_count: differences.len(),
|
|
differences,
|
|
samples,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_compare_setup_launch_payload(
|
|
smp_paths: &[PathBuf],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let samples = smp_paths
|
|
.iter()
|
|
.map(|path| load_setup_launch_payload_sample(path))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let differences = diff_setup_launch_payload_samples(&samples)?;
|
|
let report = RuntimeSetupLaunchPayloadComparisonReport {
|
|
file_count: samples.len(),
|
|
matches: differences.is_empty(),
|
|
difference_count: differences.len(),
|
|
differences,
|
|
samples,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_compare_post_special_conditions_scalars(
|
|
smp_paths: &[PathBuf],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let samples = smp_paths
|
|
.iter()
|
|
.map(|path| load_post_special_conditions_scalar_sample(path))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let common_profile_family = samples
|
|
.first()
|
|
.map(|sample| sample.profile_family.clone())
|
|
.filter(|family| {
|
|
samples
|
|
.iter()
|
|
.all(|sample| sample.profile_family == *family)
|
|
});
|
|
let differences = diff_post_special_conditions_scalar_samples(&samples)?;
|
|
let report = RuntimePostSpecialConditionsScalarComparisonReport {
|
|
file_count: samples.len(),
|
|
matches: differences.is_empty(),
|
|
common_profile_family,
|
|
difference_count: differences.len(),
|
|
differences,
|
|
samples,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_scan_candidate_table_headers(
|
|
root_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut candidate_paths = Vec::new();
|
|
collect_candidate_table_input_paths(root_path, &mut candidate_paths)?;
|
|
|
|
let mut samples = Vec::new();
|
|
let mut skipped_file_count = 0usize;
|
|
for path in candidate_paths {
|
|
match load_candidate_table_header_scan_sample(&path) {
|
|
Ok(sample) => samples.push(sample),
|
|
Err(_) => skipped_file_count += 1,
|
|
}
|
|
}
|
|
|
|
let mut grouped =
|
|
BTreeMap::<(String, String), Vec<RuntimeCandidateTableHeaderScanSample>>::new();
|
|
for sample in samples {
|
|
grouped
|
|
.entry((
|
|
sample.header_word_0_hex.clone(),
|
|
sample.header_word_1_hex.clone(),
|
|
))
|
|
.or_default()
|
|
.push(sample);
|
|
}
|
|
|
|
let file_count = grouped.values().map(Vec::len).sum();
|
|
let clusters = grouped
|
|
.into_iter()
|
|
.map(|((header_word_0_hex, header_word_1_hex), samples)| {
|
|
let mut profile_families = samples
|
|
.iter()
|
|
.map(|sample| sample.profile_family.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
let mut source_kinds = samples
|
|
.iter()
|
|
.map(|sample| sample.source_kind.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
let mut zero_trailer_count_values = samples
|
|
.iter()
|
|
.map(|sample| sample.zero_trailer_entry_count)
|
|
.collect::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
let distinct_zero_name_set_count = samples
|
|
.iter()
|
|
.map(|sample| sample.zero_trailer_entry_names.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.len();
|
|
let zero_trailer_count_min = samples
|
|
.iter()
|
|
.map(|sample| sample.zero_trailer_entry_count)
|
|
.min()
|
|
.unwrap_or(0);
|
|
let zero_trailer_count_max = samples
|
|
.iter()
|
|
.map(|sample| sample.zero_trailer_entry_count)
|
|
.max()
|
|
.unwrap_or(0);
|
|
let sample_paths = samples
|
|
.iter()
|
|
.take(12)
|
|
.map(|sample| sample.path.clone())
|
|
.collect::<Vec<_>>();
|
|
profile_families.sort();
|
|
source_kinds.sort();
|
|
zero_trailer_count_values.sort();
|
|
|
|
RuntimeCandidateTableHeaderCluster {
|
|
header_word_0_hex,
|
|
header_word_1_hex,
|
|
file_count: samples.len(),
|
|
profile_families,
|
|
source_kinds,
|
|
zero_trailer_count_min,
|
|
zero_trailer_count_max,
|
|
zero_trailer_count_values,
|
|
distinct_zero_name_set_count,
|
|
sample_paths,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let report = RuntimeCandidateTableHeaderScanReport {
|
|
root_path: root_path.display().to_string(),
|
|
file_count,
|
|
cluster_count: clusters.len(),
|
|
skipped_file_count,
|
|
clusters,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_scan_special_conditions(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut candidate_paths = Vec::new();
|
|
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
|
|
|
|
let file_count = candidate_paths.len();
|
|
let mut samples = Vec::new();
|
|
let mut skipped_file_count = 0usize;
|
|
for path in candidate_paths {
|
|
match load_special_conditions_scan_sample(&path) {
|
|
Ok(sample) => samples.push(sample),
|
|
Err(_) => skipped_file_count += 1,
|
|
}
|
|
}
|
|
|
|
let files_with_probe_count = samples.len();
|
|
let sample_files_with_any_enabled = samples
|
|
.iter()
|
|
.filter(|sample| sample.enabled_visible_count != 0)
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
let files_with_any_enabled_count = sample_files_with_any_enabled.len();
|
|
|
|
let mut grouped = BTreeMap::<(u8, String), Vec<String>>::new();
|
|
for sample in &samples {
|
|
for label in &sample.enabled_visible_labels {
|
|
if let Some(slot_index) = parse_special_condition_slot_index(label) {
|
|
grouped
|
|
.entry((slot_index, label.clone()))
|
|
.or_default()
|
|
.push(sample.path.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
let enabled_slot_summaries = grouped
|
|
.into_iter()
|
|
.map(
|
|
|((slot_index, label), paths)| RuntimeSpecialConditionsSlotSummary {
|
|
slot_index,
|
|
label,
|
|
file_count_enabled: paths.len(),
|
|
sample_paths: paths.into_iter().take(12).collect(),
|
|
},
|
|
)
|
|
.collect::<Vec<_>>();
|
|
|
|
let report = RuntimeSpecialConditionsScanReport {
|
|
root_path: root_path.display().to_string(),
|
|
file_count,
|
|
files_with_probe_count,
|
|
files_with_any_enabled_count,
|
|
skipped_file_count,
|
|
enabled_slot_summaries,
|
|
sample_files_with_any_enabled,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_scan_aligned_runtime_rule_band(
|
|
root_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut candidate_paths = Vec::new();
|
|
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
|
|
|
|
let file_count = candidate_paths.len();
|
|
let mut samples = Vec::new();
|
|
let mut skipped_file_count = 0usize;
|
|
for path in candidate_paths {
|
|
match load_aligned_runtime_rule_band_scan_sample(&path) {
|
|
Ok(sample) => samples.push(sample),
|
|
Err(_) => skipped_file_count += 1,
|
|
}
|
|
}
|
|
|
|
let files_with_probe_count = samples.len();
|
|
let files_with_any_nonzero_count = samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_band_indices.is_empty())
|
|
.count();
|
|
|
|
let mut grouped = BTreeMap::<String, Vec<RuntimeAlignedRuntimeRuleBandScanSample>>::new();
|
|
for sample in samples {
|
|
grouped
|
|
.entry(sample.profile_family.clone())
|
|
.or_default()
|
|
.push(sample);
|
|
}
|
|
|
|
let family_summaries = grouped
|
|
.into_iter()
|
|
.map(|(profile_family, samples)| {
|
|
let file_count = samples.len();
|
|
let files_with_any_nonzero_count = samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_band_indices.is_empty())
|
|
.count();
|
|
let source_kinds = samples
|
|
.iter()
|
|
.map(|sample| sample.source_kind.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
let distinct_nonzero_index_set_count = samples
|
|
.iter()
|
|
.map(|sample| sample.nonzero_band_indices.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.len();
|
|
|
|
let stable_band_indices = if samples.is_empty() {
|
|
BTreeSet::new()
|
|
} else {
|
|
let mut stable = samples[0]
|
|
.nonzero_band_indices
|
|
.iter()
|
|
.copied()
|
|
.collect::<BTreeSet<_>>();
|
|
for sample in samples.iter().skip(1) {
|
|
let current = sample
|
|
.nonzero_band_indices
|
|
.iter()
|
|
.copied()
|
|
.collect::<BTreeSet<_>>();
|
|
stable = stable.intersection(¤t).copied().collect();
|
|
}
|
|
stable
|
|
};
|
|
|
|
let mut band_values = BTreeMap::<usize, BTreeSet<String>>::new();
|
|
let mut band_counts = BTreeMap::<usize, usize>::new();
|
|
for sample in &samples {
|
|
for band_index in &sample.nonzero_band_indices {
|
|
*band_counts.entry(*band_index).or_default() += 1;
|
|
}
|
|
for (band_index, value_hex) in &sample.values_by_band_index {
|
|
band_values
|
|
.entry(*band_index)
|
|
.or_default()
|
|
.insert(value_hex.clone());
|
|
}
|
|
}
|
|
|
|
let offset_summaries = band_counts
|
|
.into_iter()
|
|
.map(
|
|
|(band_index, count)| RuntimeAlignedRuntimeRuleBandOffsetSummary {
|
|
band_index,
|
|
relative_offset_hex: format!("0x{:x}", band_index * 4),
|
|
lane_kind: aligned_runtime_rule_lane_kind(band_index).to_string(),
|
|
known_label: aligned_runtime_rule_known_label(band_index)
|
|
.map(str::to_string),
|
|
file_count_present: count,
|
|
distinct_value_count: band_values
|
|
.get(&band_index)
|
|
.map(BTreeSet::len)
|
|
.unwrap_or(0),
|
|
sample_value_hexes: band_values
|
|
.get(&band_index)
|
|
.map(|values| values.iter().take(8).cloned().collect())
|
|
.unwrap_or_default(),
|
|
},
|
|
)
|
|
.collect::<Vec<_>>();
|
|
|
|
RuntimeAlignedRuntimeRuleBandFamilySummary {
|
|
profile_family,
|
|
source_kinds,
|
|
file_count,
|
|
files_with_any_nonzero_count,
|
|
distinct_nonzero_index_set_count,
|
|
stable_nonzero_band_indices: stable_band_indices.into_iter().collect(),
|
|
union_nonzero_band_indices: band_values.keys().copied().collect(),
|
|
offset_summaries,
|
|
sample_paths: samples
|
|
.iter()
|
|
.take(12)
|
|
.map(|sample| sample.path.clone())
|
|
.collect(),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let report = RuntimeAlignedRuntimeRuleBandScanReport {
|
|
root_path: root_path.display().to_string(),
|
|
file_count,
|
|
files_with_probe_count,
|
|
files_with_any_nonzero_count,
|
|
skipped_file_count,
|
|
family_summaries,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_scan_post_special_conditions_scalars(
|
|
root_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut candidate_paths = Vec::new();
|
|
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
|
|
|
|
let file_count = candidate_paths.len();
|
|
let mut samples = Vec::new();
|
|
let mut skipped_file_count = 0usize;
|
|
for path in candidate_paths {
|
|
match load_post_special_conditions_scalar_scan_sample(&path) {
|
|
Ok(sample) => samples.push(sample),
|
|
Err(_) => skipped_file_count += 1,
|
|
}
|
|
}
|
|
|
|
let files_with_probe_count = samples.len();
|
|
let files_with_any_nonzero_count = samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
|
|
.count();
|
|
|
|
let mut grouped = BTreeMap::<String, Vec<RuntimePostSpecialConditionsScalarScanSample>>::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::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
let distinct_nonzero_offset_set_count = samples
|
|
.iter()
|
|
.map(|sample| sample.nonzero_relative_offsets.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.len();
|
|
|
|
let stable_offsets = if samples.is_empty() {
|
|
BTreeSet::new()
|
|
} else {
|
|
let mut stable = samples[0]
|
|
.nonzero_relative_offsets
|
|
.iter()
|
|
.copied()
|
|
.collect::<BTreeSet<_>>();
|
|
for sample in samples.iter().skip(1) {
|
|
let current = sample
|
|
.nonzero_relative_offsets
|
|
.iter()
|
|
.copied()
|
|
.collect::<BTreeSet<_>>();
|
|
stable = stable.intersection(¤t).copied().collect();
|
|
}
|
|
stable
|
|
};
|
|
|
|
let mut offset_values = BTreeMap::<usize, BTreeSet<String>>::new();
|
|
let mut offset_counts = BTreeMap::<usize, usize>::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::<Vec<_>>();
|
|
|
|
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::<Vec<_>>();
|
|
|
|
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<dyn std::error::Error>> {
|
|
let mut candidate_paths = Vec::new();
|
|
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
|
|
|
|
let file_count = candidate_paths.len();
|
|
let mut samples = Vec::new();
|
|
let mut skipped_file_count = 0usize;
|
|
for path in candidate_paths {
|
|
match load_post_special_conditions_tail_scan_sample(&path) {
|
|
Ok(sample) => samples.push(sample),
|
|
Err(_) => skipped_file_count += 1,
|
|
}
|
|
}
|
|
|
|
let files_with_probe_count = samples.len();
|
|
let files_with_any_nonzero_count = samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
|
|
.count();
|
|
|
|
let mut grouped = BTreeMap::<String, Vec<RuntimePostSpecialConditionsTailScanSample>>::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::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
let distinct_nonzero_offset_set_count = samples
|
|
.iter()
|
|
.map(|sample| sample.nonzero_relative_offsets.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.len();
|
|
|
|
let stable_offsets = if samples.is_empty() {
|
|
BTreeSet::new()
|
|
} else {
|
|
let mut stable = samples[0]
|
|
.nonzero_relative_offsets
|
|
.iter()
|
|
.copied()
|
|
.collect::<BTreeSet<_>>();
|
|
for sample in samples.iter().skip(1) {
|
|
let current = sample
|
|
.nonzero_relative_offsets
|
|
.iter()
|
|
.copied()
|
|
.collect::<BTreeSet<_>>();
|
|
stable = stable.intersection(¤t).copied().collect();
|
|
}
|
|
stable
|
|
};
|
|
|
|
let mut offset_values = BTreeMap::<usize, BTreeSet<String>>::new();
|
|
let mut offset_counts = BTreeMap::<usize, usize>::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::<Vec<_>>();
|
|
|
|
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::<Vec<_>>();
|
|
|
|
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<dyn std::error::Error>> {
|
|
let mut candidate_paths = Vec::new();
|
|
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
|
|
|
|
let file_count = candidate_paths.len();
|
|
let mut samples = Vec::new();
|
|
let mut skipped_file_count = 0usize;
|
|
for path in candidate_paths {
|
|
match load_recipe_book_line_scan_sample(&path) {
|
|
Ok(sample) => samples.push(sample),
|
|
Err(_) => skipped_file_count += 1,
|
|
}
|
|
}
|
|
|
|
let files_with_probe_count = samples.len();
|
|
let files_with_any_nonzero_modes_count = samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_mode_paths.is_empty())
|
|
.count();
|
|
let files_with_any_nonzero_supplied_tokens_count = samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_supplied_token_paths.is_empty())
|
|
.count();
|
|
let files_with_any_nonzero_demanded_tokens_count = samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_demanded_token_paths.is_empty())
|
|
.count();
|
|
|
|
let mut grouped = BTreeMap::<String, Vec<RuntimeRecipeBookLineScanSample>>::new();
|
|
for sample in samples {
|
|
grouped
|
|
.entry(sample.profile_family.clone())
|
|
.or_default()
|
|
.push(sample);
|
|
}
|
|
|
|
let family_summaries = grouped
|
|
.into_iter()
|
|
.map(
|
|
|(profile_family, samples)| RuntimeRecipeBookLineFamilySummary {
|
|
profile_family,
|
|
source_kinds: samples
|
|
.iter()
|
|
.map(|sample| sample.source_kind.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect(),
|
|
file_count: samples.len(),
|
|
files_with_any_nonzero_modes_count: samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_mode_paths.is_empty())
|
|
.count(),
|
|
files_with_any_nonzero_supplied_tokens_count: samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_supplied_token_paths.is_empty())
|
|
.count(),
|
|
files_with_any_nonzero_demanded_tokens_count: samples
|
|
.iter()
|
|
.filter(|sample| !sample.nonzero_demanded_token_paths.is_empty())
|
|
.count(),
|
|
stable_nonzero_mode_paths: intersect_nonzero_recipe_line_paths(
|
|
samples.iter().map(|sample| &sample.nonzero_mode_paths),
|
|
),
|
|
stable_nonzero_supplied_token_paths: intersect_nonzero_recipe_line_paths(
|
|
samples
|
|
.iter()
|
|
.map(|sample| &sample.nonzero_supplied_token_paths),
|
|
),
|
|
stable_nonzero_demanded_token_paths: intersect_nonzero_recipe_line_paths(
|
|
samples
|
|
.iter()
|
|
.map(|sample| &sample.nonzero_demanded_token_paths),
|
|
),
|
|
mode_summaries: build_recipe_line_field_summaries(
|
|
samples.iter().map(|sample| &sample.nonzero_mode_paths),
|
|
),
|
|
supplied_token_summaries: build_recipe_line_field_summaries(
|
|
samples
|
|
.iter()
|
|
.map(|sample| &sample.nonzero_supplied_token_paths),
|
|
),
|
|
demanded_token_summaries: build_recipe_line_field_summaries(
|
|
samples
|
|
.iter()
|
|
.map(|sample| &sample.nonzero_demanded_token_paths),
|
|
),
|
|
sample_paths: samples
|
|
.iter()
|
|
.take(12)
|
|
.map(|sample| sample.path.clone())
|
|
.collect(),
|
|
},
|
|
)
|
|
.collect::<Vec<_>>();
|
|
|
|
let report = RuntimeRecipeBookLineScanReport {
|
|
root_path: root_path.display().to_string(),
|
|
file_count,
|
|
files_with_probe_count,
|
|
files_with_any_nonzero_modes_count,
|
|
files_with_any_nonzero_supplied_tokens_count,
|
|
files_with_any_nonzero_demanded_tokens_count,
|
|
skipped_file_count,
|
|
family_summaries,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn run_runtime_export_profile_block(
|
|
smp_path: &Path,
|
|
output_path: &Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(smp_path)?;
|
|
let document = build_profile_block_export_document(smp_path, &inspection)?;
|
|
let bytes = serde_json::to_vec_pretty(&document)?;
|
|
fs::write(output_path, bytes)?;
|
|
let report = RuntimeProfileBlockExportReport {
|
|
output_path: output_path.display().to_string(),
|
|
profile_kind: document.profile_kind,
|
|
profile_family: document.profile_family,
|
|
};
|
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn load_classic_profile_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeClassicProfileSample, Box<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(smp_path)?;
|
|
let probe = inspection.classic_rehydrate_profile_probe.ok_or_else(|| {
|
|
format!(
|
|
"{} did not expose a classic rehydrate packed-profile block",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
|
|
Ok(RuntimeClassicProfileSample {
|
|
path: smp_path.display().to_string(),
|
|
profile_family: probe.profile_family,
|
|
progress_32dc_offset: probe.progress_32dc_offset,
|
|
progress_3714_offset: probe.progress_3714_offset,
|
|
progress_3715_offset: probe.progress_3715_offset,
|
|
packed_profile_offset: probe.packed_profile_offset,
|
|
packed_profile_len: probe.packed_profile_len,
|
|
packed_profile_block: probe.packed_profile_block,
|
|
})
|
|
}
|
|
|
|
fn load_rt3_105_profile_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeRt3105ProfileSample, Box<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(smp_path)?;
|
|
let probe = inspection.rt3_105_packed_profile_probe.ok_or_else(|| {
|
|
format!(
|
|
"{} did not expose an RT3 1.05 packed-profile block",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
|
|
Ok(RuntimeRt3105ProfileSample {
|
|
path: smp_path.display().to_string(),
|
|
profile_family: probe.profile_family,
|
|
packed_profile_offset: probe.packed_profile_offset,
|
|
packed_profile_len: probe.packed_profile_len,
|
|
packed_profile_block: probe.packed_profile_block,
|
|
})
|
|
}
|
|
|
|
fn load_candidate_table_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeCandidateTableSample, Box<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(smp_path)?;
|
|
let probe = inspection.rt3_105_save_name_table_probe.ok_or_else(|| {
|
|
format!(
|
|
"{} did not expose an RT3 1.05 candidate-availability table",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
|
|
Ok(RuntimeCandidateTableSample {
|
|
path: smp_path.display().to_string(),
|
|
profile_family: probe.profile_family,
|
|
source_kind: probe.source_kind,
|
|
semantic_family: probe.semantic_family,
|
|
header_word_0_hex: probe.header_word_0_hex,
|
|
header_word_1_hex: probe.header_word_1_hex,
|
|
header_word_2_hex: probe.header_word_2_hex,
|
|
observed_entry_count: probe.observed_entry_count,
|
|
zero_trailer_entry_count: probe.zero_trailer_entry_count,
|
|
nonzero_trailer_entry_count: probe.nonzero_trailer_entry_count,
|
|
zero_trailer_entry_names: probe.zero_trailer_entry_names,
|
|
footer_progress_word_0_hex: probe.footer_progress_word_0_hex,
|
|
footer_progress_word_1_hex: probe.footer_progress_word_1_hex,
|
|
availability_by_name: probe
|
|
.entries
|
|
.into_iter()
|
|
.map(|entry| (entry.text, entry.availability_dword))
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
fn load_candidate_table_inspection_report(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeCandidateTableInspectionReport, Box<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(smp_path)?;
|
|
if let Some(probe) = inspection.rt3_105_save_name_table_probe {
|
|
return Ok(RuntimeCandidateTableInspectionReport {
|
|
path: smp_path.display().to_string(),
|
|
profile_family: probe.profile_family,
|
|
source_kind: probe.source_kind,
|
|
semantic_family: probe.semantic_family,
|
|
header_word_0_hex: probe.header_word_0_hex,
|
|
header_word_1_hex: probe.header_word_1_hex,
|
|
header_word_2_hex: probe.header_word_2_hex,
|
|
observed_entry_capacity: probe.observed_entry_capacity,
|
|
observed_entry_count: probe.observed_entry_count,
|
|
zero_trailer_entry_count: probe.zero_trailer_entry_count,
|
|
nonzero_trailer_entry_count: probe.nonzero_trailer_entry_count,
|
|
zero_trailer_entry_names: probe.zero_trailer_entry_names,
|
|
entries: probe
|
|
.entries
|
|
.into_iter()
|
|
.map(|entry| RuntimeCandidateTableEntrySample {
|
|
index: entry.index,
|
|
offset: entry.offset,
|
|
text: entry.text,
|
|
availability_dword: entry.availability_dword,
|
|
availability_dword_hex: entry.availability_dword_hex,
|
|
trailer_word: entry.trailer_word,
|
|
trailer_word_hex: entry.trailer_word_hex,
|
|
})
|
|
.collect(),
|
|
});
|
|
}
|
|
|
|
let bytes = fs::read(smp_path)?;
|
|
let header_offset = 0x6a70usize;
|
|
let entries_offset = 0x6ad1usize;
|
|
let block_end_offset = 0x73c0usize;
|
|
let entry_stride = 0x22usize;
|
|
if bytes.len() < block_end_offset
|
|
|| !matches_candidate_table_header_bytes(&bytes, header_offset)
|
|
{
|
|
return Err(format!(
|
|
"{} did not expose an RT3 1.05 candidate-availability table",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let observed_entry_capacity = read_u32_le(&bytes, header_offset + 0x1c)
|
|
.ok_or_else(|| format!("{} is missing candidate table capacity", smp_path.display()))?
|
|
as usize;
|
|
let observed_entry_count = read_u32_le(&bytes, header_offset + 0x20)
|
|
.ok_or_else(|| format!("{} is missing candidate table count", smp_path.display()))?
|
|
as usize;
|
|
if observed_entry_capacity < observed_entry_count {
|
|
return Err(format!(
|
|
"{} has invalid candidate table capacity/count {observed_entry_capacity}/{observed_entry_count}",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
let entries_end_offset = entries_offset
|
|
.checked_add(
|
|
observed_entry_count
|
|
.checked_mul(entry_stride)
|
|
.ok_or("candidate table length overflow")?,
|
|
)
|
|
.ok_or("candidate table end overflow")?;
|
|
if entries_end_offset > block_end_offset {
|
|
return Err(format!(
|
|
"{} candidate table overruns fixed block end",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let mut zero_trailer_entry_names = Vec::new();
|
|
let mut entries = Vec::new();
|
|
for index in 0..observed_entry_count {
|
|
let offset = entries_offset + index * entry_stride;
|
|
let chunk = &bytes[offset..offset + entry_stride];
|
|
let nul_index = chunk
|
|
.iter()
|
|
.position(|byte| *byte == 0)
|
|
.unwrap_or(entry_stride - 4);
|
|
let text = std::str::from_utf8(&chunk[..nul_index]).map_err(|_| {
|
|
format!(
|
|
"{} contains invalid UTF-8 in candidate table",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
let availability_dword =
|
|
read_u32_le(&bytes, offset + entry_stride - 4).ok_or_else(|| {
|
|
format!(
|
|
"{} is missing candidate availability dword",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
if availability_dword == 0 {
|
|
zero_trailer_entry_names.push(text.to_string());
|
|
}
|
|
entries.push(RuntimeCandidateTableEntrySample {
|
|
index,
|
|
offset,
|
|
text: text.to_string(),
|
|
availability_dword,
|
|
availability_dword_hex: format!("0x{availability_dword:08x}"),
|
|
trailer_word: availability_dword,
|
|
trailer_word_hex: format!("0x{availability_dword:08x}"),
|
|
});
|
|
}
|
|
|
|
Ok(RuntimeCandidateTableInspectionReport {
|
|
path: smp_path.display().to_string(),
|
|
profile_family: classify_candidate_table_header_profile(
|
|
smp_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.map(|ext| ext.to_ascii_lowercase()),
|
|
&bytes,
|
|
),
|
|
source_kind: match smp_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.map(|ext| ext.to_ascii_lowercase())
|
|
.as_deref()
|
|
{
|
|
Some("gmp") => "map-fixed-catalog-range",
|
|
Some("gms") => "save-fixed-catalog-range",
|
|
_ => "fixed-catalog-range",
|
|
}
|
|
.to_string(),
|
|
semantic_family: "scenario-named-candidate-availability-table".to_string(),
|
|
header_word_0_hex: format!(
|
|
"0x{:08x}",
|
|
read_u32_le(&bytes, header_offset).ok_or("missing candidate header word 0")?
|
|
),
|
|
header_word_1_hex: format!(
|
|
"0x{:08x}",
|
|
read_u32_le(&bytes, header_offset + 4).ok_or("missing candidate header word 1")?
|
|
),
|
|
header_word_2_hex: format!(
|
|
"0x{:08x}",
|
|
read_u32_le(&bytes, header_offset + 8).ok_or("missing candidate header word 2")?
|
|
),
|
|
observed_entry_capacity,
|
|
observed_entry_count,
|
|
zero_trailer_entry_count: zero_trailer_entry_names.len(),
|
|
nonzero_trailer_entry_count: observed_entry_count - zero_trailer_entry_names.len(),
|
|
zero_trailer_entry_names,
|
|
entries,
|
|
})
|
|
}
|
|
|
|
fn load_recipe_book_line_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeRecipeBookLineSample, Box<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(smp_path)?;
|
|
let probe = inspection.recipe_book_summary_probe.ok_or_else(|| {
|
|
format!(
|
|
"{} did not expose a grounded recipe-book summary block",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
|
|
let mut book_head_kind_by_index = BTreeMap::new();
|
|
let mut book_line_area_kind_by_index = BTreeMap::new();
|
|
let mut max_annual_production_word_hex_by_book = BTreeMap::new();
|
|
let mut line_kind_by_path = BTreeMap::new();
|
|
let mut mode_word_hex_by_path = BTreeMap::new();
|
|
let mut annual_amount_word_hex_by_path = BTreeMap::new();
|
|
let mut supplied_cargo_token_word_hex_by_path = BTreeMap::new();
|
|
let mut demanded_cargo_token_word_hex_by_path = BTreeMap::new();
|
|
|
|
for book in &probe.books {
|
|
let book_key = format!("book{:02}", book.book_index);
|
|
book_head_kind_by_index.insert(book_key.clone(), book.head_kind.clone());
|
|
book_line_area_kind_by_index.insert(book_key.clone(), book.line_area_kind.clone());
|
|
max_annual_production_word_hex_by_book.insert(
|
|
book_key.clone(),
|
|
book.max_annual_production_word_hex.clone(),
|
|
);
|
|
for line in &book.lines {
|
|
let line_key = format!("{book_key}.line{:02}", line.line_index);
|
|
line_kind_by_path.insert(line_key.clone(), line.line_kind.clone());
|
|
mode_word_hex_by_path.insert(line_key.clone(), line.mode_word_hex.clone());
|
|
annual_amount_word_hex_by_path
|
|
.insert(line_key.clone(), line.annual_amount_word_hex.clone());
|
|
supplied_cargo_token_word_hex_by_path
|
|
.insert(line_key.clone(), line.supplied_cargo_token_word_hex.clone());
|
|
demanded_cargo_token_word_hex_by_path
|
|
.insert(line_key.clone(), line.demanded_cargo_token_word_hex.clone());
|
|
}
|
|
}
|
|
|
|
Ok(RuntimeRecipeBookLineSample {
|
|
path: smp_path.display().to_string(),
|
|
profile_family: probe.profile_family,
|
|
source_kind: probe.source_kind,
|
|
book_count: probe.book_count,
|
|
book_stride_hex: probe.book_stride_hex,
|
|
line_count: probe.line_count,
|
|
line_stride_hex: probe.line_stride_hex,
|
|
book_head_kind_by_index,
|
|
book_line_area_kind_by_index,
|
|
max_annual_production_word_hex_by_book,
|
|
line_kind_by_path,
|
|
mode_word_hex_by_path,
|
|
annual_amount_word_hex_by_path,
|
|
supplied_cargo_token_word_hex_by_path,
|
|
demanded_cargo_token_word_hex_by_path,
|
|
})
|
|
}
|
|
|
|
fn load_recipe_book_line_scan_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeRecipeBookLineScanSample, Box<dyn std::error::Error>> {
|
|
let sample = load_recipe_book_line_sample(smp_path)?;
|
|
Ok(RuntimeRecipeBookLineScanSample {
|
|
path: sample.path,
|
|
profile_family: sample.profile_family,
|
|
source_kind: sample.source_kind,
|
|
nonzero_mode_paths: sample
|
|
.mode_word_hex_by_path
|
|
.into_iter()
|
|
.filter(|(_, value)| value != "0x00000000")
|
|
.collect(),
|
|
nonzero_supplied_token_paths: sample
|
|
.supplied_cargo_token_word_hex_by_path
|
|
.into_iter()
|
|
.filter(|(_, value)| value != "0x00000000")
|
|
.collect(),
|
|
nonzero_demanded_token_paths: sample
|
|
.demanded_cargo_token_word_hex_by_path
|
|
.into_iter()
|
|
.filter(|(_, value)| value != "0x00000000")
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
fn load_setup_payload_core_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeSetupPayloadCoreSample, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(smp_path)?;
|
|
let extension = smp_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.map(|ext| ext.to_ascii_lowercase())
|
|
.unwrap_or_default();
|
|
let inferred_profile_family =
|
|
classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
|
|
let candidate_header_word_0 = read_u32_le(&bytes, 0x6a70);
|
|
let candidate_header_word_1 = read_u32_le(&bytes, 0x6a74);
|
|
|
|
Ok(RuntimeSetupPayloadCoreSample {
|
|
path: smp_path.display().to_string(),
|
|
file_extension: extension,
|
|
inferred_profile_family,
|
|
payload_word_0x14: read_u16_le(&bytes, 0x14)
|
|
.ok_or_else(|| format!("{} missing setup payload word +0x14", smp_path.display()))?,
|
|
payload_word_0x14_hex: format!(
|
|
"0x{:04x}",
|
|
read_u16_le(&bytes, 0x14).ok_or_else(|| format!(
|
|
"{} missing setup payload word +0x14",
|
|
smp_path.display()
|
|
))?
|
|
),
|
|
payload_byte_0x20: bytes
|
|
.get(0x20)
|
|
.copied()
|
|
.ok_or_else(|| format!("{} missing setup payload byte +0x20", smp_path.display()))?,
|
|
payload_byte_0x20_hex: format!(
|
|
"0x{:02x}",
|
|
bytes.get(0x20).copied().ok_or_else(|| format!(
|
|
"{} missing setup payload byte +0x20",
|
|
smp_path.display()
|
|
))?
|
|
),
|
|
marker_bytes_0x2c9_0x2d0_hex: bytes
|
|
.get(0x2c9..0x2d1)
|
|
.map(hex_encode)
|
|
.ok_or_else(|| format!("{} missing setup payload marker bytes", smp_path.display()))?,
|
|
row_category_byte_0x31a: bytes
|
|
.get(0x31a)
|
|
.copied()
|
|
.ok_or_else(|| format!("{} missing setup payload byte +0x31a", smp_path.display()))?,
|
|
row_category_byte_0x31a_hex: format!(
|
|
"0x{:02x}",
|
|
bytes.get(0x31a).copied().ok_or_else(|| format!(
|
|
"{} missing setup payload byte +0x31a",
|
|
smp_path.display()
|
|
))?
|
|
),
|
|
row_visibility_byte_0x31b: bytes
|
|
.get(0x31b)
|
|
.copied()
|
|
.ok_or_else(|| format!("{} missing setup payload byte +0x31b", smp_path.display()))?,
|
|
row_visibility_byte_0x31b_hex: format!(
|
|
"0x{:02x}",
|
|
bytes.get(0x31b).copied().ok_or_else(|| format!(
|
|
"{} missing setup payload byte +0x31b",
|
|
smp_path.display()
|
|
))?
|
|
),
|
|
row_visibility_byte_0x31c: bytes
|
|
.get(0x31c)
|
|
.copied()
|
|
.ok_or_else(|| format!("{} missing setup payload byte +0x31c", smp_path.display()))?,
|
|
row_visibility_byte_0x31c_hex: format!(
|
|
"0x{:02x}",
|
|
bytes.get(0x31c).copied().ok_or_else(|| format!(
|
|
"{} missing setup payload byte +0x31c",
|
|
smp_path.display()
|
|
))?
|
|
),
|
|
row_count_word_0x3ae: read_u16_le(&bytes, 0x3ae)
|
|
.ok_or_else(|| format!("{} missing setup payload word +0x3ae", smp_path.display()))?,
|
|
row_count_word_0x3ae_hex: format!(
|
|
"0x{:04x}",
|
|
read_u16_le(&bytes, 0x3ae).ok_or_else(|| format!(
|
|
"{} missing setup payload word +0x3ae",
|
|
smp_path.display()
|
|
))?
|
|
),
|
|
payload_word_0x3b2: read_u16_le(&bytes, 0x3b2)
|
|
.ok_or_else(|| format!("{} missing setup payload word +0x3b2", smp_path.display()))?,
|
|
payload_word_0x3b2_hex: format!(
|
|
"0x{:04x}",
|
|
read_u16_le(&bytes, 0x3b2).ok_or_else(|| format!(
|
|
"{} missing setup payload word +0x3b2",
|
|
smp_path.display()
|
|
))?
|
|
),
|
|
payload_word_0x3ba: read_u16_le(&bytes, 0x3ba)
|
|
.ok_or_else(|| format!("{} missing setup payload word +0x3ba", smp_path.display()))?,
|
|
payload_word_0x3ba_hex: format!(
|
|
"0x{:04x}",
|
|
read_u16_le(&bytes, 0x3ba).ok_or_else(|| format!(
|
|
"{} missing setup payload word +0x3ba",
|
|
smp_path.display()
|
|
))?
|
|
),
|
|
candidate_header_word_0_hex: candidate_header_word_0.map(|value| format!("0x{value:08x}")),
|
|
candidate_header_word_1_hex: candidate_header_word_1.map(|value| format!("0x{value:08x}")),
|
|
})
|
|
}
|
|
|
|
fn load_setup_launch_payload_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeSetupLaunchPayloadSample, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(smp_path)?;
|
|
let extension = smp_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.map(|ext| ext.to_ascii_lowercase())
|
|
.unwrap_or_default();
|
|
let inferred_profile_family =
|
|
classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
|
|
let launch_flag_byte_0x22 = bytes
|
|
.get(0x22)
|
|
.copied()
|
|
.ok_or_else(|| format!("{} missing setup launch byte +0x22", smp_path.display()))?;
|
|
let launch_selector_byte_0x33 = bytes
|
|
.get(0x33)
|
|
.copied()
|
|
.ok_or_else(|| format!("{} missing setup launch byte +0x33", smp_path.display()))?;
|
|
let token_block = bytes
|
|
.get(0x23..0x33)
|
|
.ok_or_else(|| format!("{} missing setup launch token block", smp_path.display()))?;
|
|
let campaign_progress_in_known_range =
|
|
(launch_flag_byte_0x22 as usize) < CAMPAIGN_SCENARIO_COUNT;
|
|
let campaign_progress_scenario_name = campaign_progress_in_known_range
|
|
.then(|| OBSERVED_CAMPAIGN_SCENARIO_NAMES[launch_flag_byte_0x22 as usize].to_string());
|
|
let campaign_progress_page_index = match launch_flag_byte_0x22 {
|
|
0..=4 => Some(1),
|
|
5..=9 => Some(2),
|
|
10..=12 => Some(3),
|
|
13..=15 => Some(4),
|
|
_ => None,
|
|
};
|
|
let campaign_selector_values = OBSERVED_CAMPAIGN_SCENARIO_NAMES
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, name)| (name.to_string(), token_block[index]))
|
|
.collect::<BTreeMap<_, _>>();
|
|
let nonzero_campaign_selector_values = campaign_selector_values
|
|
.iter()
|
|
.filter_map(|(name, value)| (*value != 0).then_some((name.clone(), *value)))
|
|
.collect::<BTreeMap<_, _>>();
|
|
|
|
Ok(RuntimeSetupLaunchPayloadSample {
|
|
path: smp_path.display().to_string(),
|
|
file_extension: extension,
|
|
inferred_profile_family,
|
|
launch_flag_byte_0x22,
|
|
launch_flag_byte_0x22_hex: format!("0x{launch_flag_byte_0x22:02x}"),
|
|
campaign_progress_in_known_range,
|
|
campaign_progress_scenario_name,
|
|
campaign_progress_page_index,
|
|
launch_selector_byte_0x33,
|
|
launch_selector_byte_0x33_hex: format!("0x{launch_selector_byte_0x33:02x}"),
|
|
launch_token_block_0x23_0x32_hex: hex_encode(token_block),
|
|
campaign_selector_values,
|
|
nonzero_campaign_selector_values,
|
|
})
|
|
}
|
|
|
|
fn load_post_special_conditions_scalar_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimePostSpecialConditionsScalarSample, Box<dyn std::error::Error>> {
|
|
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<RuntimeCandidateTableHeaderScanSample, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(smp_path)?;
|
|
let header_offset = 0x6a70usize;
|
|
let entries_offset = 0x6ad1usize;
|
|
let block_end_offset = 0x73c0usize;
|
|
let entry_stride = 0x22usize;
|
|
if bytes.len() < block_end_offset {
|
|
return Err(format!(
|
|
"{} is too small for the fixed candidate table range",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
if !matches_candidate_table_header_bytes(&bytes, header_offset) {
|
|
return Err(format!(
|
|
"{} does not contain the fixed candidate table header",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let observed_entry_capacity = read_u32_le(&bytes, header_offset + 0x1c)
|
|
.ok_or_else(|| format!("{} is missing candidate table capacity", smp_path.display()))?
|
|
as usize;
|
|
let observed_entry_count = read_u32_le(&bytes, header_offset + 0x20)
|
|
.ok_or_else(|| format!("{} is missing candidate table count", smp_path.display()))?
|
|
as usize;
|
|
if observed_entry_capacity < observed_entry_count {
|
|
return Err(format!(
|
|
"{} has invalid candidate table capacity/count {observed_entry_capacity}/{observed_entry_count}",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let entries_end_offset = entries_offset
|
|
.checked_add(
|
|
observed_entry_count
|
|
.checked_mul(entry_stride)
|
|
.ok_or("candidate table length overflow")?,
|
|
)
|
|
.ok_or("candidate table end overflow")?;
|
|
if entries_end_offset > block_end_offset {
|
|
return Err(format!(
|
|
"{} candidate table overruns fixed block end",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let mut zero_trailer_entry_names = Vec::new();
|
|
for index in 0..observed_entry_count {
|
|
let offset = entries_offset + index * entry_stride;
|
|
let chunk = &bytes[offset..offset + entry_stride];
|
|
let nul_index = chunk
|
|
.iter()
|
|
.position(|byte| *byte == 0)
|
|
.unwrap_or(entry_stride - 4);
|
|
let text = std::str::from_utf8(&chunk[..nul_index]).map_err(|_| {
|
|
format!(
|
|
"{} contains invalid UTF-8 in candidate table",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
let availability = read_u32_le(&bytes, offset + entry_stride - 4).ok_or_else(|| {
|
|
format!(
|
|
"{} is missing candidate availability dword",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
if availability == 0 {
|
|
zero_trailer_entry_names.push(text.to_string());
|
|
}
|
|
}
|
|
|
|
let profile_family = classify_candidate_table_header_profile(
|
|
smp_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.map(|ext| ext.to_ascii_lowercase()),
|
|
&bytes,
|
|
);
|
|
let source_kind = match smp_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.map(|ext| ext.to_ascii_lowercase())
|
|
.as_deref()
|
|
{
|
|
Some("gmp") => "map-fixed-catalog-range",
|
|
Some("gms") => "save-fixed-catalog-range",
|
|
_ => "fixed-catalog-range",
|
|
}
|
|
.to_string();
|
|
|
|
Ok(RuntimeCandidateTableHeaderScanSample {
|
|
path: smp_path.display().to_string(),
|
|
profile_family,
|
|
source_kind,
|
|
header_word_0_hex: format!(
|
|
"0x{:08x}",
|
|
read_u32_le(&bytes, header_offset).ok_or("missing candidate header word 0")?
|
|
),
|
|
header_word_1_hex: format!(
|
|
"0x{:08x}",
|
|
read_u32_le(&bytes, header_offset + 4).ok_or("missing candidate header word 1")?
|
|
),
|
|
zero_trailer_entry_count: zero_trailer_entry_names.len(),
|
|
zero_trailer_entry_names,
|
|
})
|
|
}
|
|
|
|
fn load_special_conditions_scan_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimeSpecialConditionsScanSample, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(smp_path)?;
|
|
let table_len = SPECIAL_CONDITION_COUNT * 4;
|
|
let table_end = SPECIAL_CONDITIONS_OFFSET
|
|
.checked_add(table_len)
|
|
.ok_or("special-conditions table overflow")?;
|
|
if bytes.len() < table_end {
|
|
return Err(format!(
|
|
"{} is too small for the fixed special-conditions table",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let hidden_sentinel = read_u32_le(
|
|
&bytes,
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4,
|
|
)
|
|
.ok_or_else(|| {
|
|
format!(
|
|
"{} is missing the hidden special-condition sentinel",
|
|
smp_path.display()
|
|
)
|
|
})?;
|
|
if hidden_sentinel != 1 {
|
|
return Err(format!(
|
|
"{} does not match the fixed special-conditions table sentinel",
|
|
smp_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let enabled_visible_labels = (0..SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT)
|
|
.filter_map(|slot_index| {
|
|
let value = read_u32_le(&bytes, SPECIAL_CONDITIONS_OFFSET + slot_index * 4)?;
|
|
(value != 0).then(|| {
|
|
format!(
|
|
"slot {}: {}",
|
|
slot_index, SPECIAL_CONDITION_LABELS[slot_index]
|
|
)
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let extension = smp_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.map(|ext| ext.to_ascii_lowercase())
|
|
.unwrap_or_default();
|
|
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
|
|
let source_kind = match extension.as_str() {
|
|
"gmp" => "map-fixed-special-conditions-range",
|
|
"gms" => "save-fixed-special-conditions-range",
|
|
"gmx" => "sandbox-fixed-special-conditions-range",
|
|
_ => "fixed-special-conditions-range",
|
|
}
|
|
.to_string();
|
|
|
|
Ok(RuntimeSpecialConditionsScanSample {
|
|
path: smp_path.display().to_string(),
|
|
profile_family,
|
|
source_kind,
|
|
enabled_visible_count: enabled_visible_labels.len(),
|
|
enabled_visible_labels,
|
|
})
|
|
}
|
|
|
|
fn load_post_special_conditions_scalar_scan_sample(
|
|
smp_path: &Path,
|
|
) -> Result<RuntimePostSpecialConditionsScalarScanSample, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(smp_path)?;
|
|
let table_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<RuntimePostSpecialConditionsTailScanSample, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(smp_path)?;
|
|
let table_len = SPECIAL_CONDITION_COUNT * 4;
|
|
let table_end = SPECIAL_CONDITIONS_OFFSET
|
|
.checked_add(table_len)
|
|
.ok_or("special-conditions table overflow")?;
|
|
if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET || bytes.len() < table_end {
|
|
return Err(format!(
|
|
"{} is too small for the fixed 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<RuntimeAlignedRuntimeRuleBandScanSample, Box<dyn std::error::Error>> {
|
|
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<PathBuf>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let metadata = match fs::symlink_metadata(root_path) {
|
|
Ok(metadata) => metadata,
|
|
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
|
|
Err(err) => return Err(err.into()),
|
|
};
|
|
if metadata.file_type().is_symlink() {
|
|
return Ok(());
|
|
}
|
|
|
|
if root_path.is_file() {
|
|
if root_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms"))
|
|
{
|
|
out.push(root_path.to_path_buf());
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let entries = match fs::read_dir(root_path) {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
|
|
Err(err) => return Err(err.into()),
|
|
};
|
|
|
|
for entry in entries {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
collect_candidate_table_input_paths(&path, out)?;
|
|
continue;
|
|
}
|
|
if path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms"))
|
|
{
|
|
out.push(path);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn collect_special_conditions_input_paths(
|
|
root_path: &Path,
|
|
out: &mut Vec<PathBuf>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let metadata = match fs::symlink_metadata(root_path) {
|
|
Ok(metadata) => metadata,
|
|
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
|
|
Err(err) => return Err(err.into()),
|
|
};
|
|
if metadata.file_type().is_symlink() {
|
|
return Ok(());
|
|
}
|
|
|
|
if root_path.is_file() {
|
|
if root_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms" | "gmx"))
|
|
{
|
|
out.push(root_path.to_path_buf());
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let entries = match fs::read_dir(root_path) {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
|
|
Err(err) => return Err(err.into()),
|
|
};
|
|
|
|
for entry in entries {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
collect_special_conditions_input_paths(&path, out)?;
|
|
continue;
|
|
}
|
|
if path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms" | "gmx"))
|
|
{
|
|
out.push(path);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_special_condition_slot_index(label: &str) -> Option<u8> {
|
|
let suffix = label.strip_prefix("slot ")?;
|
|
let (slot_index, _) = suffix.split_once(':')?;
|
|
slot_index.parse().ok()
|
|
}
|
|
|
|
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<String> {
|
|
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<PathBuf>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let metadata = match fs::symlink_metadata(root_path) {
|
|
Ok(metadata) => metadata,
|
|
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
|
|
Err(err) => return Err(err.into()),
|
|
};
|
|
if metadata.file_type().is_symlink() {
|
|
return Ok(());
|
|
}
|
|
|
|
if root_path.is_file() {
|
|
if root_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("gmp"))
|
|
{
|
|
out.push(root_path.to_path_buf());
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let entries = match fs::read_dir(root_path) {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
|
|
Err(err) => return Err(err.into()),
|
|
};
|
|
|
|
for entry in entries {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
collect_compact_event_dispatch_cluster_input_paths(&path, out)?;
|
|
continue;
|
|
}
|
|
if path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("gmp"))
|
|
{
|
|
out.push(path);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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_condition_tuple_family(
|
|
tuples: &[RuntimeCompactEventDispatchClusterConditionTuple],
|
|
) -> String {
|
|
if tuples.is_empty() {
|
|
return "[]".to_string();
|
|
}
|
|
let parts = tuples
|
|
.iter()
|
|
.map(|tuple| match &tuple.metric {
|
|
Some(metric) => format!("{}:{}:{}", tuple.raw_condition_id, tuple.subtype, metric),
|
|
None => format!("{}:{}", tuple.raw_condition_id, tuple.subtype),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
format!("[{}]", parts.join(","))
|
|
}
|
|
|
|
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<usize> {
|
|
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<String>, bytes: &[u8]) -> String {
|
|
let word_2 = read_u32_le(bytes, 8);
|
|
let word_3 = read_u32_le(bytes, 12);
|
|
let word_5 = read_u32_le(bytes, 20);
|
|
match (extension.as_deref().unwrap_or(""), word_2, word_3, word_5) {
|
|
("gmp", Some(0x00040001), Some(0x00028000), Some(0x00000771)) => {
|
|
"rt3-105-map-container-v1".to_string()
|
|
}
|
|
("gmp", Some(0x00040001), Some(0x00018000), Some(0x00000746)) => {
|
|
"rt3-105-scenario-map-container-v1".to_string()
|
|
}
|
|
("gmp", Some(0x0001c001), Some(0x00018000), Some(0x00000754)) => {
|
|
"rt3-105-alt-map-container-v1".to_string()
|
|
}
|
|
("gms", Some(0x00040001), Some(0x00028000), Some(0x00000771)) => {
|
|
"rt3-105-save-container-v1".to_string()
|
|
}
|
|
("gms", Some(0x00040001), Some(0x00018000), Some(0x00000746)) => {
|
|
"rt3-105-scenario-save-container-v1".to_string()
|
|
}
|
|
("gms", Some(0x0001c001), Some(0x00018000), Some(0x00000754)) => {
|
|
"rt3-105-alt-save-container-v1".to_string()
|
|
}
|
|
("gmp", _, _, _) => "map-fixed-catalog-container-unknown".to_string(),
|
|
("gms", _, _, _) => "save-fixed-catalog-container-unknown".to_string(),
|
|
_ => "fixed-catalog-container-unknown".to_string(),
|
|
}
|
|
}
|
|
|
|
fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
|
|
let chunk = bytes.get(offset..offset + 4)?;
|
|
Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
|
}
|
|
|
|
fn read_u16_le(bytes: &[u8], offset: usize) -> Option<u16> {
|
|
let chunk = bytes.get(offset..offset + 2)?;
|
|
Some(u16::from_le_bytes([chunk[0], chunk[1]]))
|
|
}
|
|
|
|
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<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
|
|
let labeled_values = samples
|
|
.iter()
|
|
.map(|sample| {
|
|
(
|
|
sample.path.clone(),
|
|
serde_json::json!({
|
|
"profile_family": sample.profile_family,
|
|
"progress_32dc_offset": sample.progress_32dc_offset,
|
|
"progress_3714_offset": sample.progress_3714_offset,
|
|
"progress_3715_offset": sample.progress_3715_offset,
|
|
"packed_profile_offset": sample.packed_profile_offset,
|
|
"packed_profile_len": sample.packed_profile_len,
|
|
"packed_profile_block": sample.packed_profile_block,
|
|
}),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut differences = Vec::new();
|
|
collect_json_multi_differences("$", &labeled_values, &mut differences);
|
|
Ok(differences)
|
|
}
|
|
|
|
fn diff_rt3_105_profile_samples(
|
|
samples: &[RuntimeRt3105ProfileSample],
|
|
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
|
|
let labeled_values = samples
|
|
.iter()
|
|
.map(|sample| {
|
|
(
|
|
sample.path.clone(),
|
|
serde_json::json!({
|
|
"profile_family": sample.profile_family,
|
|
"packed_profile_offset": sample.packed_profile_offset,
|
|
"packed_profile_len": sample.packed_profile_len,
|
|
"packed_profile_block": sample.packed_profile_block,
|
|
}),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut differences = Vec::new();
|
|
collect_json_multi_differences("$", &labeled_values, &mut differences);
|
|
Ok(differences)
|
|
}
|
|
|
|
fn diff_candidate_table_samples(
|
|
samples: &[RuntimeCandidateTableSample],
|
|
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
|
|
let labeled_values = samples
|
|
.iter()
|
|
.map(|sample| {
|
|
(
|
|
sample.path.clone(),
|
|
serde_json::json!({
|
|
"profile_family": sample.profile_family,
|
|
"source_kind": sample.source_kind,
|
|
"semantic_family": sample.semantic_family,
|
|
"header_word_0_hex": sample.header_word_0_hex,
|
|
"header_word_1_hex": sample.header_word_1_hex,
|
|
"header_word_2_hex": sample.header_word_2_hex,
|
|
"observed_entry_count": sample.observed_entry_count,
|
|
"zero_trailer_entry_count": sample.zero_trailer_entry_count,
|
|
"nonzero_trailer_entry_count": sample.nonzero_trailer_entry_count,
|
|
"zero_trailer_entry_names": sample.zero_trailer_entry_names,
|
|
"footer_progress_word_0_hex": sample.footer_progress_word_0_hex,
|
|
"footer_progress_word_1_hex": sample.footer_progress_word_1_hex,
|
|
"availability_by_name": sample.availability_by_name,
|
|
}),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut differences = Vec::new();
|
|
collect_json_multi_differences("$", &labeled_values, &mut differences);
|
|
Ok(differences)
|
|
}
|
|
|
|
fn diff_recipe_book_line_samples(
|
|
samples: &[RuntimeRecipeBookLineSample],
|
|
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
|
|
let labeled_values = samples
|
|
.iter()
|
|
.map(|sample| {
|
|
(
|
|
sample.path.clone(),
|
|
serde_json::json!({
|
|
"profile_family": sample.profile_family,
|
|
"source_kind": sample.source_kind,
|
|
"book_count": sample.book_count,
|
|
"book_stride_hex": sample.book_stride_hex,
|
|
"line_count": sample.line_count,
|
|
"line_stride_hex": sample.line_stride_hex,
|
|
"book_head_kind_by_index": sample.book_head_kind_by_index,
|
|
"book_line_area_kind_by_index": sample.book_line_area_kind_by_index,
|
|
"max_annual_production_word_hex_by_book": sample.max_annual_production_word_hex_by_book,
|
|
"line_kind_by_path": sample.line_kind_by_path,
|
|
"mode_word_hex_by_path": sample.mode_word_hex_by_path,
|
|
"annual_amount_word_hex_by_path": sample.annual_amount_word_hex_by_path,
|
|
"supplied_cargo_token_word_hex_by_path": sample.supplied_cargo_token_word_hex_by_path,
|
|
"demanded_cargo_token_word_hex_by_path": sample.demanded_cargo_token_word_hex_by_path,
|
|
}),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut differences = Vec::new();
|
|
collect_json_multi_differences("$", &labeled_values, &mut differences);
|
|
Ok(differences)
|
|
}
|
|
|
|
fn diff_recipe_book_line_content_samples(
|
|
samples: &[RuntimeRecipeBookLineSample],
|
|
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
|
|
let labeled_values = samples
|
|
.iter()
|
|
.map(|sample| {
|
|
(
|
|
sample.path.clone(),
|
|
serde_json::json!({
|
|
"book_count": sample.book_count,
|
|
"book_stride_hex": sample.book_stride_hex,
|
|
"line_count": sample.line_count,
|
|
"line_stride_hex": sample.line_stride_hex,
|
|
"book_head_kind_by_index": sample.book_head_kind_by_index,
|
|
"book_line_area_kind_by_index": sample.book_line_area_kind_by_index,
|
|
"max_annual_production_word_hex_by_book": sample.max_annual_production_word_hex_by_book,
|
|
"line_kind_by_path": sample.line_kind_by_path,
|
|
"mode_word_hex_by_path": sample.mode_word_hex_by_path,
|
|
"annual_amount_word_hex_by_path": sample.annual_amount_word_hex_by_path,
|
|
"supplied_cargo_token_word_hex_by_path": sample.supplied_cargo_token_word_hex_by_path,
|
|
"demanded_cargo_token_word_hex_by_path": sample.demanded_cargo_token_word_hex_by_path,
|
|
}),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut differences = Vec::new();
|
|
collect_json_multi_differences("$", &labeled_values, &mut differences);
|
|
Ok(differences)
|
|
}
|
|
|
|
fn intersect_nonzero_recipe_line_paths<'a>(
|
|
maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
|
|
) -> Vec<String> {
|
|
let mut maps = maps.peekable();
|
|
if maps.peek().is_none() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let mut stable = maps
|
|
.next()
|
|
.map(|map| map.keys().cloned().collect::<BTreeSet<_>>())
|
|
.unwrap_or_default();
|
|
for map in maps {
|
|
let current = map.keys().cloned().collect::<BTreeSet<_>>();
|
|
stable = stable.intersection(¤t).cloned().collect();
|
|
}
|
|
stable.into_iter().collect()
|
|
}
|
|
|
|
fn build_recipe_line_field_summaries<'a>(
|
|
maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
|
|
) -> Vec<RuntimeRecipeBookLineFieldSummary> {
|
|
let mut value_sets = BTreeMap::<String, BTreeSet<String>>::new();
|
|
let mut counts = BTreeMap::<String, usize>::new();
|
|
for map in maps {
|
|
for (line_path, value_hex) in map {
|
|
*counts.entry(line_path.clone()).or_default() += 1;
|
|
value_sets
|
|
.entry(line_path.clone())
|
|
.or_default()
|
|
.insert(value_hex.clone());
|
|
}
|
|
}
|
|
|
|
counts
|
|
.into_iter()
|
|
.map(
|
|
|(line_path, file_count_present)| RuntimeRecipeBookLineFieldSummary {
|
|
line_path: line_path.clone(),
|
|
file_count_present,
|
|
distinct_value_count: value_sets.get(&line_path).map(BTreeSet::len).unwrap_or(0),
|
|
sample_value_hexes: value_sets
|
|
.get(&line_path)
|
|
.map(|values| values.iter().take(8).cloned().collect())
|
|
.unwrap_or_default(),
|
|
},
|
|
)
|
|
.collect()
|
|
}
|
|
|
|
fn diff_setup_payload_core_samples(
|
|
samples: &[RuntimeSetupPayloadCoreSample],
|
|
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
|
|
let labeled_values = samples
|
|
.iter()
|
|
.map(|sample| {
|
|
(
|
|
sample.path.clone(),
|
|
serde_json::json!({
|
|
"file_extension": sample.file_extension,
|
|
"inferred_profile_family": sample.inferred_profile_family,
|
|
"payload_word_0x14": sample.payload_word_0x14,
|
|
"payload_word_0x14_hex": sample.payload_word_0x14_hex,
|
|
"payload_byte_0x20": sample.payload_byte_0x20,
|
|
"payload_byte_0x20_hex": sample.payload_byte_0x20_hex,
|
|
"marker_bytes_0x2c9_0x2d0_hex": sample.marker_bytes_0x2c9_0x2d0_hex,
|
|
"row_category_byte_0x31a": sample.row_category_byte_0x31a,
|
|
"row_category_byte_0x31a_hex": sample.row_category_byte_0x31a_hex,
|
|
"row_visibility_byte_0x31b": sample.row_visibility_byte_0x31b,
|
|
"row_visibility_byte_0x31b_hex": sample.row_visibility_byte_0x31b_hex,
|
|
"row_visibility_byte_0x31c": sample.row_visibility_byte_0x31c,
|
|
"row_visibility_byte_0x31c_hex": sample.row_visibility_byte_0x31c_hex,
|
|
"row_count_word_0x3ae": sample.row_count_word_0x3ae,
|
|
"row_count_word_0x3ae_hex": sample.row_count_word_0x3ae_hex,
|
|
"payload_word_0x3b2": sample.payload_word_0x3b2,
|
|
"payload_word_0x3b2_hex": sample.payload_word_0x3b2_hex,
|
|
"payload_word_0x3ba": sample.payload_word_0x3ba,
|
|
"payload_word_0x3ba_hex": sample.payload_word_0x3ba_hex,
|
|
"candidate_header_word_0_hex": sample.candidate_header_word_0_hex,
|
|
"candidate_header_word_1_hex": sample.candidate_header_word_1_hex,
|
|
}),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut differences = Vec::new();
|
|
collect_json_multi_differences("$", &labeled_values, &mut differences);
|
|
Ok(differences)
|
|
}
|
|
|
|
fn diff_setup_launch_payload_samples(
|
|
samples: &[RuntimeSetupLaunchPayloadSample],
|
|
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
|
|
let labeled_values = samples
|
|
.iter()
|
|
.map(|sample| {
|
|
(
|
|
sample.path.clone(),
|
|
serde_json::json!({
|
|
"file_extension": sample.file_extension,
|
|
"inferred_profile_family": sample.inferred_profile_family,
|
|
"launch_flag_byte_0x22": sample.launch_flag_byte_0x22,
|
|
"launch_flag_byte_0x22_hex": sample.launch_flag_byte_0x22_hex,
|
|
"campaign_progress_in_known_range": sample.campaign_progress_in_known_range,
|
|
"campaign_progress_scenario_name": sample.campaign_progress_scenario_name,
|
|
"campaign_progress_page_index": sample.campaign_progress_page_index,
|
|
"launch_selector_byte_0x33": sample.launch_selector_byte_0x33,
|
|
"launch_selector_byte_0x33_hex": sample.launch_selector_byte_0x33_hex,
|
|
"launch_token_block_0x23_0x32_hex": sample.launch_token_block_0x23_0x32_hex,
|
|
"campaign_selector_values": sample.campaign_selector_values,
|
|
"nonzero_campaign_selector_values": sample.nonzero_campaign_selector_values,
|
|
}),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut differences = Vec::new();
|
|
collect_json_multi_differences("$", &labeled_values, &mut differences);
|
|
Ok(differences)
|
|
}
|
|
|
|
fn diff_post_special_conditions_scalar_samples(
|
|
samples: &[RuntimePostSpecialConditionsScalarSample],
|
|
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
|
|
let labeled_values = samples
|
|
.iter()
|
|
.map(|sample| {
|
|
(
|
|
sample.path.clone(),
|
|
serde_json::json!({
|
|
"profile_family": sample.profile_family,
|
|
"source_kind": sample.source_kind,
|
|
"nonzero_relative_offset_hexes": sample.nonzero_relative_offset_hexes,
|
|
"values_by_relative_offset_hex": sample.values_by_relative_offset_hex,
|
|
}),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut differences = Vec::new();
|
|
collect_json_multi_differences("$", &labeled_values, &mut differences);
|
|
Ok(differences)
|
|
}
|
|
|
|
fn build_profile_block_export_document(
|
|
smp_path: &Path,
|
|
inspection: &SmpInspectionReport,
|
|
) -> Result<RuntimeProfileBlockExportDocument, Box<dyn std::error::Error>> {
|
|
if let Some(probe) = &inspection.classic_rehydrate_profile_probe {
|
|
return Ok(RuntimeProfileBlockExportDocument {
|
|
source_path: smp_path.display().to_string(),
|
|
profile_kind: "classic-rehydrate-profile".to_string(),
|
|
profile_family: probe.profile_family.clone(),
|
|
payload: serde_json::to_value(probe)?,
|
|
});
|
|
}
|
|
|
|
if let Some(probe) = &inspection.rt3_105_packed_profile_probe {
|
|
return Ok(RuntimeProfileBlockExportDocument {
|
|
source_path: smp_path.display().to_string(),
|
|
profile_kind: "rt3-105-packed-profile".to_string(),
|
|
profile_family: probe.profile_family.clone(),
|
|
payload: serde_json::to_value(probe)?,
|
|
});
|
|
}
|
|
|
|
Err(format!(
|
|
"{} did not expose an exportable packed-profile block",
|
|
smp_path.display()
|
|
)
|
|
.into())
|
|
}
|
|
|
|
fn collect_json_multi_differences(
|
|
path: &str,
|
|
labeled_values: &[(String, Value)],
|
|
differences: &mut Vec<RuntimeClassicProfileDifference>,
|
|
) {
|
|
if labeled_values.is_empty() {
|
|
return;
|
|
}
|
|
|
|
if labeled_values
|
|
.iter()
|
|
.all(|(_, value)| matches!(value, Value::Object(_)))
|
|
{
|
|
let mut keys = BTreeSet::new();
|
|
for (_, value) in labeled_values {
|
|
if let Value::Object(map) = value {
|
|
keys.extend(map.keys().cloned());
|
|
}
|
|
}
|
|
|
|
for key in keys {
|
|
let next_path = format!("{path}.{key}");
|
|
let nested = labeled_values
|
|
.iter()
|
|
.map(|(label, value)| {
|
|
let nested_value = match value {
|
|
Value::Object(map) => map.get(&key).cloned().unwrap_or(Value::Null),
|
|
_ => Value::Null,
|
|
};
|
|
(label.clone(), nested_value)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
collect_json_multi_differences(&next_path, &nested, differences);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if labeled_values
|
|
.iter()
|
|
.all(|(_, value)| matches!(value, Value::Array(_)))
|
|
{
|
|
let max_len = labeled_values
|
|
.iter()
|
|
.filter_map(|(_, value)| match value {
|
|
Value::Array(items) => Some(items.len()),
|
|
_ => None,
|
|
})
|
|
.max()
|
|
.unwrap_or(0);
|
|
|
|
for index in 0..max_len {
|
|
let next_path = format!("{path}[{index}]");
|
|
let nested = labeled_values
|
|
.iter()
|
|
.map(|(label, value)| {
|
|
let nested_value = match value {
|
|
Value::Array(items) => items.get(index).cloned().unwrap_or(Value::Null),
|
|
_ => Value::Null,
|
|
};
|
|
(label.clone(), nested_value)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
collect_json_multi_differences(&next_path, &nested, differences);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let first = &labeled_values[0].1;
|
|
if labeled_values
|
|
.iter()
|
|
.skip(1)
|
|
.all(|(_, value)| value == first)
|
|
{
|
|
return;
|
|
}
|
|
|
|
differences.push(RuntimeClassicProfileDifference {
|
|
field_path: path.to_string(),
|
|
values: labeled_values
|
|
.iter()
|
|
.map(|(label, value)| RuntimeClassicProfileDifferenceValue {
|
|
path: label.clone(),
|
|
value: value.clone(),
|
|
})
|
|
.collect(),
|
|
});
|
|
}
|
|
|
|
fn print_runtime_validation_report(
|
|
report: &FixtureValidationReport,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("{}", serde_json::to_string_pretty(report)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn load_finance_outcome(path: &Path) -> Result<FinanceOutcome, Box<dyn std::error::Error>> {
|
|
let text = fs::read_to_string(path)?;
|
|
if let Ok(snapshot) = serde_json::from_str::<FinanceSnapshot>(&text) {
|
|
return Ok(snapshot.evaluate());
|
|
}
|
|
if let Ok(outcome) = serde_json::from_str::<FinanceOutcome>(&text) {
|
|
return Ok(outcome);
|
|
}
|
|
|
|
Err(format!(
|
|
"unable to parse {} as FinanceSnapshot or FinanceOutcome",
|
|
path.display()
|
|
)
|
|
.into())
|
|
}
|
|
|
|
fn diff_finance_outcomes(
|
|
left: &FinanceOutcome,
|
|
right: &FinanceOutcome,
|
|
) -> Result<FinanceDiffReport, Box<dyn std::error::Error>> {
|
|
let left_value = serde_json::to_value(left)?;
|
|
let right_value = serde_json::to_value(right)?;
|
|
let mut differences = Vec::new();
|
|
collect_json_differences("$", &left_value, &right_value, &mut differences);
|
|
|
|
Ok(FinanceDiffReport {
|
|
matches: differences.is_empty(),
|
|
difference_count: differences.len(),
|
|
differences,
|
|
})
|
|
}
|
|
|
|
fn collect_json_differences(
|
|
path: &str,
|
|
left: &Value,
|
|
right: &Value,
|
|
differences: &mut Vec<FinanceDiffEntry>,
|
|
) {
|
|
match (left, right) {
|
|
(Value::Object(left_map), Value::Object(right_map)) => {
|
|
let mut keys = BTreeSet::new();
|
|
keys.extend(left_map.keys().cloned());
|
|
keys.extend(right_map.keys().cloned());
|
|
|
|
for key in keys {
|
|
let next_path = format!("{path}.{key}");
|
|
match (left_map.get(&key), right_map.get(&key)) {
|
|
(Some(left_value), Some(right_value)) => {
|
|
collect_json_differences(&next_path, left_value, right_value, differences);
|
|
}
|
|
(left_value, right_value) => differences.push(FinanceDiffEntry {
|
|
path: next_path,
|
|
left: left_value.cloned().unwrap_or(Value::Null),
|
|
right: right_value.cloned().unwrap_or(Value::Null),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
(Value::Array(left_items), Value::Array(right_items)) => {
|
|
let max_len = left_items.len().max(right_items.len());
|
|
for index in 0..max_len {
|
|
let next_path = format!("{path}[{index}]");
|
|
match (left_items.get(index), right_items.get(index)) {
|
|
(Some(left_value), Some(right_value)) => {
|
|
collect_json_differences(&next_path, left_value, right_value, differences);
|
|
}
|
|
(left_value, right_value) => differences.push(FinanceDiffEntry {
|
|
path: next_path,
|
|
left: left_value.cloned().unwrap_or(Value::Null),
|
|
right: right_value.cloned().unwrap_or(Value::Null),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
_ if left != right => differences.push(FinanceDiffEntry {
|
|
path: path.to_string(),
|
|
left: left.clone(),
|
|
right: right.clone(),
|
|
}),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn validate_required_files(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut missing = Vec::new();
|
|
for relative in REQUIRED_EXPORTS {
|
|
let path = repo_root.join(relative);
|
|
if !path.exists() {
|
|
missing.push(path.display().to_string());
|
|
}
|
|
}
|
|
|
|
if !missing.is_empty() {
|
|
return Err(format!("missing required exports: {}", missing.join(", ")).into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_binary_summary(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let summary = load_binary_summary(&repo_root.join(BINARY_SUMMARY_PATH))?;
|
|
let actual_exe = repo_root.join(CANONICAL_EXE_PATH);
|
|
if !actual_exe.exists() {
|
|
return Err(format!("canonical exe missing: {}", actual_exe.display()).into());
|
|
}
|
|
|
|
let actual_hash = sha256_file(&actual_exe)?;
|
|
if actual_hash != summary.sha256 {
|
|
return Err(format!(
|
|
"hash mismatch for {}: summary has {}, actual file is {}",
|
|
actual_exe.display(),
|
|
summary.sha256,
|
|
actual_hash
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let docs_readme = fs::read_to_string(repo_root.join("docs/README.md"))?;
|
|
if !docs_readme.contains(&summary.sha256) {
|
|
return Err("docs/README.md does not include the canonical SHA-256".into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_function_map(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let records = load_function_map(&repo_root.join(FUNCTION_MAP_PATH))?;
|
|
let mut seen = BTreeSet::new();
|
|
|
|
for record in records {
|
|
if !(1..=5).contains(&record.confidence) {
|
|
return Err(format!(
|
|
"invalid confidence {} for {} {}",
|
|
record.confidence, record.address, record.name
|
|
)
|
|
.into());
|
|
}
|
|
|
|
if !seen.insert(record.address) {
|
|
return Err(format!("duplicate function address {}", record.address).into());
|
|
}
|
|
|
|
if record.name.trim().is_empty() {
|
|
return Err(format!("blank function name at {}", record.address).into());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_control_loop_atlas(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let atlas = fs::read_to_string(repo_root.join(CONTROL_LOOP_ATLAS_PATH))?;
|
|
for heading in REQUIRED_ATLAS_HEADINGS {
|
|
if !atlas.contains(heading) {
|
|
return Err(format!("missing atlas heading `{heading}`").into());
|
|
}
|
|
}
|
|
|
|
for marker in [
|
|
"- Roots:",
|
|
"- Trigger/Cadence:",
|
|
"- Key Dispatchers:",
|
|
"- State Anchors:",
|
|
"- Subsystem Handoffs:",
|
|
"- Evidence:",
|
|
"- Open Questions:",
|
|
] {
|
|
if !atlas.contains(marker) {
|
|
return Err(format!("atlas is missing field marker `{marker}`").into());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn sha256_file(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
|
|
let mut file = fs::File::open(path)?;
|
|
let mut hasher = Sha256::new();
|
|
let mut buffer = [0_u8; 8192];
|
|
loop {
|
|
let read = file.read(&mut buffer)?;
|
|
if read == 0 {
|
|
break;
|
|
}
|
|
hasher.update(&buffer[..read]);
|
|
}
|
|
|
|
Ok(format!("{:x}", hasher.finalize()))
|
|
}
|
|
|
|
#[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 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<T: Serialize>(stem: &str, value: &T) -> PathBuf {
|
|
let nonce = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("system time should be after epoch")
|
|
.as_nanos();
|
|
let path = std::env::temp_dir().join(format!("rrt-cli-{stem}-{nonce}.json"));
|
|
let bytes = serde_json::to_vec_pretty(value).expect("json serialization should succeed");
|
|
fs::write(&path, bytes).expect("temp json should be written");
|
|
path
|
|
}
|
|
}
|