Ground widened add-building descriptor names

This commit is contained in:
Jan Petykiewicz 2026-04-19 02:01:26 -07:00
commit a3df447186
6 changed files with 287 additions and 15 deletions

View file

@ -125,6 +125,9 @@ enum Command {
RuntimeInspectSmp {
smp_path: PathBuf,
},
RuntimeInspectCandidateTable {
smp_path: PathBuf,
},
RuntimeInspectCompactEventDispatchCluster {
root_path: PathBuf,
},
@ -521,6 +524,34 @@ struct RuntimeCandidateTableSample {
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,
@ -946,6 +977,9 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
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)?;
}
@ -1167,6 +1201,13 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
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" =>
@ -1459,7 +1500,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-compact-event-dispatch-cluster <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-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 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-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(),
),
}
@ -2246,6 +2287,12 @@ fn run_runtime_compare_candidate_table(
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>> {
@ -3119,6 +3166,161 @@ fn load_candidate_table_sample(
})
}
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>> {

View file

@ -32,6 +32,7 @@ pub const REQUIRED_EXPORTS: &[&str] = &[
"artifacts/exports/rt3-1.06/pending-template-store-management.md",
"artifacts/exports/rt3-1.06/event-effects-table.json",
"artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json",
"artifacts/exports/rt3-1.06/event-effects-building-bindings.json",
"artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json",
"artifacts/exports/rt3-1.06/economy-cargo-sources.json",
"artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json",