Scan numbered candidate table run families

This commit is contained in:
Jan Petykiewicz 2026-04-19 14:28:43 -07:00
commit dd973ad44b
3 changed files with 264 additions and 1 deletions

View file

@ -246,6 +246,9 @@ enum Command {
RuntimeScanCandidateTableHeaders { RuntimeScanCandidateTableHeaders {
root_path: PathBuf, root_path: PathBuf,
}, },
RuntimeScanCandidateTableNamedRuns {
root_path: PathBuf,
},
RuntimeScanSpecialConditions { RuntimeScanSpecialConditions {
root_path: PathBuf, root_path: PathBuf,
}, },
@ -857,6 +860,41 @@ struct RuntimeCandidateTableHeaderScanSample {
zero_trailer_entry_names: Vec<String>, zero_trailer_entry_names: Vec<String>,
} }
#[derive(Debug, Clone, Serialize)]
struct RuntimeCandidateTableNamedRun {
prefix: String,
start_index: usize,
end_index: usize,
count: usize,
first_name: String,
last_name: String,
start_offset: usize,
end_offset: usize,
distinct_trailer_hex_words: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
struct RuntimeCandidateTableNamedRunScanSample {
path: String,
profile_family: String,
source_kind: String,
observed_entry_count: usize,
port_runs: Vec<RuntimeCandidateTableNamedRun>,
warehouse_runs: Vec<RuntimeCandidateTableNamedRun>,
}
#[derive(Debug, Serialize)]
struct RuntimeCandidateTableNamedRunScanReport {
root_path: String,
file_count: usize,
files_with_probe_count: usize,
files_with_any_numbered_port_runs_count: usize,
files_with_any_numbered_warehouse_runs_count: usize,
files_with_both_numbered_run_families_count: usize,
skipped_file_count: usize,
samples: Vec<RuntimeCandidateTableNamedRunScanSample>,
}
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
struct RuntimeSpecialConditionsScanSample { struct RuntimeSpecialConditionsScanSample {
path: String, path: String,
@ -1220,6 +1258,9 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
Command::RuntimeScanCandidateTableHeaders { root_path } => { Command::RuntimeScanCandidateTableHeaders { root_path } => {
run_runtime_scan_candidate_table_headers(&root_path)?; run_runtime_scan_candidate_table_headers(&root_path)?;
} }
Command::RuntimeScanCandidateTableNamedRuns { root_path } => {
run_runtime_scan_candidate_table_named_runs(&root_path)?;
}
Command::RuntimeScanSpecialConditions { root_path } => { Command::RuntimeScanSpecialConditions { root_path } => {
run_runtime_scan_special_conditions(&root_path)?; run_runtime_scan_special_conditions(&root_path)?;
} }
@ -1603,6 +1644,13 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
root_path: PathBuf::from(root_path), root_path: PathBuf::from(root_path),
}) })
} }
[command, subcommand, root_path]
if command == "runtime" && subcommand == "scan-candidate-table-named-runs" =>
{
Ok(Command::RuntimeScanCandidateTableNamedRuns {
root_path: PathBuf::from(root_path),
})
}
[command, subcommand, root_path] [command, subcommand, root_path]
if command == "runtime" && subcommand == "scan-special-conditions" => if command == "runtime" && subcommand == "scan-special-conditions" =>
{ {
@ -1647,7 +1695,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
}) })
} }
_ => Err( _ => 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 inspect-map-title-hints <maps-dir> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime inspect-save-company-chairman <file.smp> | runtime inspect-save-placed-structure-triplets <file.smp> | runtime compare-region-fixed-row-runs <left.gms> <right.gms> | runtime inspect-periodic-company-service-trace <file.smp> | runtime inspect-region-service-trace <file.smp> | runtime inspect-infrastructure-asset-trace <file.smp> | runtime inspect-save-region-queued-notice-records <file.smp> | runtime inspect-placed-structure-dynamic-side-buffer <file.smp> | runtime inspect-unclassified-save-collections <file.smp> | runtime 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>]" "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 inspect-map-title-hints <maps-dir> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime inspect-save-company-chairman <file.smp> | runtime inspect-save-placed-structure-triplets <file.smp> | runtime compare-region-fixed-row-runs <left.gms> <right.gms> | runtime inspect-periodic-company-service-trace <file.smp> | runtime inspect-region-service-trace <file.smp> | runtime inspect-infrastructure-asset-trace <file.smp> | runtime inspect-save-region-queued-notice-records <file.smp> | runtime inspect-placed-structure-dynamic-side-buffer <file.smp> | runtime inspect-unclassified-save-collections <file.smp> | runtime 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-candidate-table-named-runs <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
.into(), .into(),
), ),
} }
@ -2938,6 +2986,50 @@ fn run_runtime_scan_candidate_table_headers(
Ok(()) Ok(())
} }
fn run_runtime_scan_candidate_table_named_runs(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_candidate_table_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_candidate_table_named_run_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_numbered_port_runs_count = samples
.iter()
.filter(|sample| !sample.port_runs.is_empty())
.count();
let files_with_any_numbered_warehouse_runs_count = samples
.iter()
.filter(|sample| !sample.warehouse_runs.is_empty())
.count();
let files_with_both_numbered_run_families_count = samples
.iter()
.filter(|sample| !sample.port_runs.is_empty() && !sample.warehouse_runs.is_empty())
.count();
let report = RuntimeCandidateTableNamedRunScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_numbered_port_runs_count,
files_with_any_numbered_warehouse_runs_count,
files_with_both_numbered_run_families_count,
skipped_file_count,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
fn run_runtime_scan_special_conditions(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> { fn run_runtime_scan_special_conditions(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new(); let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?; collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
@ -4152,6 +4244,23 @@ fn load_candidate_table_header_scan_sample(
}) })
} }
fn load_candidate_table_named_run_scan_sample(
smp_path: &Path,
) -> Result<RuntimeCandidateTableNamedRunScanSample, Box<dyn std::error::Error>> {
let report = load_candidate_table_inspection_report(smp_path)?;
let port_runs = collect_numbered_candidate_name_runs(&report.entries, "Port");
let warehouse_runs = collect_numbered_candidate_name_runs(&report.entries, "Warehouse");
Ok(RuntimeCandidateTableNamedRunScanSample {
path: report.path,
profile_family: report.profile_family,
source_kind: report.source_kind,
observed_entry_count: report.observed_entry_count,
port_runs,
warehouse_runs,
})
}
fn load_special_conditions_scan_sample( fn load_special_conditions_scan_sample(
smp_path: &Path, smp_path: &Path,
) -> Result<RuntimeSpecialConditionsScanSample, Box<dyn std::error::Error>> { ) -> Result<RuntimeSpecialConditionsScanSample, Box<dyn std::error::Error>> {
@ -4495,6 +4604,61 @@ fn collect_candidate_table_input_paths(
Ok(()) Ok(())
} }
fn collect_numbered_candidate_name_runs(
entries: &[RuntimeCandidateTableEntrySample],
prefix: &str,
) -> Vec<RuntimeCandidateTableNamedRun> {
let mut numbered_entries = entries
.iter()
.filter_map(|entry| {
parse_numbered_candidate_name(&entry.text, prefix).map(|number| (entry, number))
})
.collect::<Vec<_>>();
numbered_entries.sort_by_key(|(entry, number)| (entry.index, *number));
let mut runs = Vec::new();
let mut cursor = 0usize;
while cursor < numbered_entries.len() {
let (first_entry, first_number) = numbered_entries[cursor];
let mut last_entry = first_entry;
let mut last_number = first_number;
let mut distinct_trailer_hex_words = BTreeSet::from([first_entry.trailer_word_hex.clone()]);
let mut next = cursor + 1;
while next < numbered_entries.len() {
let (entry, number) = numbered_entries[next];
if entry.index != last_entry.index + 1 || number != last_number + 1 {
break;
}
distinct_trailer_hex_words.insert(entry.trailer_word_hex.clone());
last_entry = entry;
last_number = number;
next += 1;
}
runs.push(RuntimeCandidateTableNamedRun {
prefix: prefix.to_string(),
start_index: first_entry.index,
end_index: last_entry.index,
count: next - cursor,
first_name: first_entry.text.clone(),
last_name: last_entry.text.clone(),
start_offset: first_entry.offset,
end_offset: last_entry.offset,
distinct_trailer_hex_words: distinct_trailer_hex_words.into_iter().collect(),
});
cursor = next;
}
runs
}
fn parse_numbered_candidate_name(text: &str, prefix: &str) -> Option<usize> {
let digits = text.strip_prefix(prefix)?;
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
digits.parse().ok()
}
fn collect_special_conditions_input_paths( fn collect_special_conditions_input_paths(
root_path: &Path, root_path: &Path,
out: &mut Vec<PathBuf>, out: &mut Vec<PathBuf>,
@ -6551,6 +6715,83 @@ mod tests {
); );
} }
#[test]
fn collects_numbered_candidate_name_runs_by_prefix() {
let entries = vec![
RuntimeCandidateTableEntrySample {
index: 35,
offset: 28535,
text: "Port00".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 43,
offset: 28807,
text: "Warehouse00".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 45,
offset: 28875,
text: "Port01".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 46,
offset: 28909,
text: "Port02".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 56,
offset: 29249,
text: "Warehouse01".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 57,
offset: 29283,
text: "Warehouse02".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
];
let port_runs = collect_numbered_candidate_name_runs(&entries, "Port");
let warehouse_runs = collect_numbered_candidate_name_runs(&entries, "Warehouse");
assert_eq!(port_runs.len(), 2);
assert_eq!(port_runs[0].first_name, "Port00");
assert_eq!(port_runs[0].count, 1);
assert_eq!(port_runs[1].first_name, "Port01");
assert_eq!(port_runs[1].last_name, "Port02");
assert_eq!(port_runs[1].count, 2);
assert_eq!(warehouse_runs.len(), 2);
assert_eq!(warehouse_runs[0].first_name, "Warehouse00");
assert_eq!(warehouse_runs[0].count, 1);
assert_eq!(warehouse_runs[1].first_name, "Warehouse01");
assert_eq!(warehouse_runs[1].last_name, "Warehouse02");
assert_eq!(warehouse_runs[1].count, 2);
}
#[test] #[test]
fn diffs_recipe_book_line_samples_across_multiple_files() { fn diffs_recipe_book_line_samples_across_multiple_files() {
let sample_a = RuntimeRecipeBookLineSample { let sample_a = RuntimeRecipeBookLineSample {

View file

@ -1244,6 +1244,17 @@
problem is no longer whether those names exist as stable scenario rows; it is how that fixed problem is no longer whether those names exist as stable scenario rows; it is how that fixed
candidate-table cluster is projected into the later aux-record bank and then into the live clone candidate-table cluster is projected into the later aux-record bank and then into the live clone
families. families.
The root scan narrows the real corpus for that question too. `runtime scan-candidate-table-headers
rt3_wineprefix/drive_c/rt3/maps` shows `37` probe-bearing shipped maps and `4` skips, while the
narrower `runtime scan-candidate-table-named-runs` command confirms that `Louisiana.gmp` and
`Dutchlantis.gmp` share the same split shape:
isolated `Port00` at row `35`, contiguous `Port01..11` at rows `45..55`, isolated
`Warehouse00` at row `43`, and contiguous `Warehouse01..11` at rows `56..66`. Raw string
presence is broader than that actual fixed-candidate-table seam too: `Port00` appears in all
`41` shipped `.gmp` files, but `Central Pacific.gmp`, `Italy.gmp`, `Tex-Mex.gmp`, and
`Texas Tea.gmp` do not expose the fixed candidate-table header at all. So the next Tier-2
source pass should target the `37` probe-bearing maps rather than the noisier full
string-bearing map corpus.
The direct `+0xba/+0xbb` writer census now rules out a broad false lead too. The obvious new The direct `+0xba/+0xbb` writer census now rules out a broad false lead too. The obvious new
stores at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side stores at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side
portrait/string refresh helpers over a different id-keyed collection rooted through portrait/string refresh helpers over a different id-keyed collection rooted through

View file

@ -755,6 +755,17 @@ Working rule:
is no longer whether those names exist as stable scenario rows; it is how that stable is no longer whether those names exist as stable scenario rows; it is how that stable
candidate-table cluster is projected into the later aux-record bank and then into the live candidate-table cluster is projected into the later aux-record bank and then into the live
clone families. clone families.
The new root scan sharpens that boundary further. `runtime scan-candidate-table-headers
rt3_wineprefix/drive_c/rt3/maps` shows `37` probe-bearing shipped maps and `4` skips, while
the narrower `runtime scan-candidate-table-named-runs` command confirms that
`Louisiana.gmp` and `Dutchlantis.gmp` share the same split shape:
`Port00` as an isolated run at row `35`, `Port01..11` as one contiguous run at rows `45..55`,
`Warehouse00` isolated at row `43`, and `Warehouse01..11` contiguous at rows `56..66`.
Raw map-string presence is broader than that actual candidate-table seam too:
`Port00` appears in all `41` shipped `.gmp` files, but `Central Pacific.gmp`, `Italy.gmp`,
`Tex-Mex.gmp`, and `Texas Tea.gmp` do not expose the fixed candidate-table header at all. So
the next Tier-2 source pass should target the `37` probe-bearing maps rather than the noisier
full string-bearing map corpus.
The direct `+0xba/+0xbb` writer census is narrower now too. The obvious newly surfaced stores The direct `+0xba/+0xbb` writer census is narrower now too. The obvious newly surfaced stores
at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side portrait/string at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side portrait/string
refresh helpers: they walk a separate id-keyed collection through `0x0053f830`, free and refresh helpers: they walk a separate id-keyed collection through `0x0053f830`, free and