Compare building source assets to live bindings

This commit is contained in:
Jan Petykiewicz 2026-04-19 03:02:11 -07:00
commit 7a26063656
5 changed files with 307 additions and 14 deletions

View file

@ -26,7 +26,7 @@ use rrt_runtime::{
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, inspect_campaign_exe_file,
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,
@ -190,6 +190,7 @@ enum Command {
},
RuntimeInspectBuildingTypeSources {
building_types_dir: PathBuf,
bindings_path: Option<PathBuf>,
},
RuntimeInspectCargoSkins {
cargo_skin_pk4_path: PathBuf,
@ -1089,8 +1090,14 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
Command::RuntimeInspectCargoTypes { cargo_types_dir } => {
run_runtime_inspect_cargo_types(&cargo_types_dir)?;
}
Command::RuntimeInspectBuildingTypeSources { building_types_dir } => {
run_runtime_inspect_building_type_sources(&building_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,
@ -1395,6 +1402,15 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
{
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]
@ -1563,7 +1579,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
})
}
_ => 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> | 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>]"
"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(),
),
}
@ -2225,10 +2241,11 @@ fn run_runtime_inspect_cargo_types(
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(building_types_dir)?,
inspection: inspect_building_types_dir_with_bindings(building_types_dir, bindings_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())

View file

@ -27,12 +27,23 @@ pub struct BuildingTypeSourceEntry {
pub file_names: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildingTypeNamedBindingComparison {
pub bindings_path: String,
pub named_binding_count: usize,
pub shared_canonical_stem_count: usize,
pub binding_only_canonical_stems: Vec<String>,
pub source_only_canonical_stems: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildingTypeSourceReport {
pub directory_path: String,
pub bca_file_count: usize,
pub bty_file_count: usize,
pub unique_canonical_stem_count: usize,
#[serde(default)]
pub named_binding_comparison: Option<BuildingTypeNamedBindingComparison>,
pub notes: Vec<String>,
pub files: Vec<BuildingTypeSourceFile>,
pub entries: Vec<BuildingTypeSourceEntry>,
@ -40,6 +51,13 @@ pub struct BuildingTypeSourceReport {
pub fn inspect_building_types_dir(
path: &Path,
) -> Result<BuildingTypeSourceReport, Box<dyn std::error::Error>> {
inspect_building_types_dir_with_bindings(path, None)
}
pub fn inspect_building_types_dir_with_bindings(
path: &Path,
bindings_path: Option<&Path>,
) -> Result<BuildingTypeSourceReport, Box<dyn std::error::Error>> {
let mut files = Vec::new();
for entry in fs::read_dir(path)? {
@ -129,20 +147,70 @@ pub fn inspect_building_types_dir(
"This report is an offline asset-pool view only; it does not by itself assign live candidate ids or prove scenario candidate-table availability.".to_string(),
];
let named_binding_comparison = if let Some(bindings_path) = bindings_path {
Some(load_named_binding_comparison(bindings_path, &entries)?)
} else {
None
};
Ok(BuildingTypeSourceReport {
directory_path: path.display().to_string(),
bca_file_count,
bty_file_count,
unique_canonical_stem_count: entries.len(),
named_binding_comparison,
notes,
files,
entries,
})
}
fn load_named_binding_comparison(
bindings_path: &Path,
entries: &[BuildingTypeSourceEntry],
) -> Result<BuildingTypeNamedBindingComparison, Box<dyn std::error::Error>> {
let artifact =
serde_json::from_str::<BuildingBindingArtifact>(&fs::read_to_string(bindings_path)?)?;
let named_binding_stems = artifact
.bindings
.into_iter()
.filter_map(|binding| binding.candidate_name)
.map(|candidate_name| canonicalize_building_stem(&candidate_name))
.collect::<BTreeSet<_>>();
let source_stems = entries
.iter()
.map(|entry| entry.canonical_stem.clone())
.collect::<BTreeSet<_>>();
Ok(BuildingTypeNamedBindingComparison {
bindings_path: bindings_path.display().to_string(),
named_binding_count: named_binding_stems.len(),
shared_canonical_stem_count: named_binding_stems.intersection(&source_stems).count(),
binding_only_canonical_stems: named_binding_stems
.difference(&source_stems)
.cloned()
.collect(),
source_only_canonical_stems: source_stems
.difference(&named_binding_stems)
.cloned()
.collect(),
})
}
fn canonicalize_building_stem(stem: &str) -> String {
stem.chars()
.filter(|ch| !matches!(ch, ' ' | '_' | '-'))
.flat_map(|ch| ch.to_lowercase())
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct BuildingBindingArtifact {
bindings: Vec<BuildingBindingRow>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct BuildingBindingRow {
#[serde(default)]
candidate_name: Option<String>,
}

View file

@ -12,8 +12,9 @@ pub mod summary;
pub mod win;
pub use building::{
BuildingTypeSourceEntry, BuildingTypeSourceFile, BuildingTypeSourceKind,
BuildingTypeSourceReport, inspect_building_types_dir,
BuildingTypeNamedBindingComparison, BuildingTypeSourceEntry, BuildingTypeSourceFile,
BuildingTypeSourceKind, BuildingTypeSourceReport, inspect_building_types_dir,
inspect_building_types_dir_with_bindings,
};
pub use calendar::{CalendarPoint, MONTH_SLOTS_PER_YEAR, PHASE_SLOTS_PER_MONTH, TICKS_PER_PHASE};
pub use campaign_exe::{