Probe save-side region and structure headers

This commit is contained in:
Jan Petykiewicz 2026-04-18 08:10:44 -07:00
commit ddbdddc5ec
5 changed files with 287 additions and 5 deletions

View file

@ -2545,6 +2545,10 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
#[serde(default)]
pub world_finance_neighborhood: Option<SmpSaveWorldFinanceNeighborhoodProbe>,
#[serde(default)]
pub region_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)]
pub placed_structure_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)]
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
#[serde(default)]
pub chairman_entries: Vec<SmpSaveChairmanRecordAnalysisEntry>,
@ -2803,6 +2807,8 @@ pub struct SmpInspectionReport {
pub save_world_finance_neighborhood_probe: Option<SmpSaveWorldFinanceNeighborhoodProbe>,
pub save_company_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_chairman_profile_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_region_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_placed_structure_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)]
pub save_company_roster_probe: Option<SmpLoadedCompanyRoster>,
#[serde(default)]
@ -3066,6 +3072,28 @@ pub fn load_save_slice_from_report(
probe.close_tag_offset
));
}
if let Some(probe) = &report.save_region_collection_header_probe {
notes.push(format!(
"Raw save tagged region header reports live_record_count={} and live_id_bound={} with direct_record_stride=0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
probe.live_record_count,
probe.live_id_bound,
probe.direct_record_stride,
probe.metadata_tag_offset,
probe.records_tag_offset,
probe.close_tag_offset
));
}
if let Some(probe) = &report.save_placed_structure_collection_header_probe {
notes.push(format!(
"Raw save tagged placed-structure header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
probe.live_record_count,
probe.live_id_bound,
probe.direct_record_stride,
probe.metadata_tag_offset,
probe.records_tag_offset,
probe.close_tag_offset
));
}
if let Some(roster) = &report.save_company_roster_probe {
notes.push(format!(
"Raw save inspection reconstructed {} company direct records from the tagged company collection.",
@ -3415,6 +3443,21 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
.to_string(),
);
}
if let Some(header) = report.save_region_collection_header_probe.as_ref() {
notes.push(format!(
"Region analysis now also exports the tagged region collection header: live_record_count={} live_id_bound={} direct_record_stride=0x{:x}.",
header.live_record_count, header.live_id_bound, header.direct_record_stride
));
}
if let Some(header) = report
.save_placed_structure_collection_header_probe
.as_ref()
{
notes.push(format!(
"Placed-structure analysis now also exports the tagged collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.",
header.live_record_count, header.live_id_bound, header.direct_record_stride
));
}
if !company_entries.is_empty() {
notes.push(
"Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string(),
@ -3456,6 +3499,10 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
world_issue_37,
world_economic_tuning,
world_finance_neighborhood,
region_collection_header: report.save_region_collection_header_probe.clone(),
placed_structure_collection_header: report
.save_placed_structure_collection_header_probe
.clone(),
company_entries,
chairman_entries,
notes,
@ -7454,6 +7501,17 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_region_collection_header_probe = parse_save_region_collection_header_probe(
bytes,
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_placed_structure_collection_header_probe =
parse_save_placed_structure_collection_header_probe(
bytes,
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_company_roster_probe = parse_save_company_roster_probe(
bytes,
save_company_collection_header_probe.as_ref(),
@ -7617,6 +7675,8 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
save_world_finance_neighborhood_probe,
save_company_collection_header_probe,
save_chairman_profile_collection_header_probe,
save_region_collection_header_probe,
save_placed_structure_collection_header_probe,
save_company_roster_probe,
save_chairman_profile_table_probe,
rt3_105_save_name_table_probe,
@ -9555,6 +9615,65 @@ fn parse_save_chairman_profile_collection_header_probe(
)
}
fn parse_save_region_collection_header_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
container_profile: Option<&SmpContainerProfile>,
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
parse_save_tagged_collection_header_probe(
bytes,
file_extension_hint,
container_profile,
0x00005209,
0x0000520a,
0x0000520b,
"save-region-tagged-header-counts",
"scenario-save-region-header-counts",
|header| {
header.direct_collection_flag == 1
&& header.direct_record_stride >= 0x100
&& header.direct_record_stride <= 0x400
&& header.live_id_bound >= 0x10
&& header.live_id_bound <= 0x100
&& header.live_record_count >= 1
&& header.live_record_count <= header.live_id_bound
},
vec![
"save-side live region collection shares tagged header family 0x5209/0x520a/0x520b with other indexed direct-record bundles".to_string(),
"the grounded region-side candidate is the smaller direct-record family with stride 0x1d5 and live_id_bound/count in the city-region range, not the larger chairman/profile family".to_string(),
],
)
}
fn parse_save_placed_structure_collection_header_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
container_profile: Option<&SmpContainerProfile>,
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
parse_save_tagged_collection_header_probe(
bytes,
file_extension_hint,
container_profile,
0x000036b1,
0x000036b2,
0x000036b3,
"save-placed-structure-tagged-header-counts",
"scenario-save-placed-structure-header-counts",
|header| {
header.direct_collection_flag == 0
&& header.direct_record_stride >= 1
&& header.direct_record_stride <= 0x20
&& header.live_id_bound >= 0x100
&& header.live_record_count >= 0x100
&& header.live_record_count <= header.live_id_bound
},
vec![
"save-side placed-structure collection uses tagged family 0x36b1/0x36b2/0x36b3 beneath the wider local-runtime and route-entry rebuild owners".to_string(),
"current evidence only grounds header-level placed-structure collection counts here; direct record-body reconstruction still needs the later per-entry load/save slot study.".to_string(),
],
)
}
#[derive(Clone, Copy)]
struct IndexedCollectionHeaderSummary {
metadata_tag_offset: usize,
@ -16396,6 +16515,59 @@ mod tests {
],
evidence: vec![],
});
report.save_region_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-region-tagged-header-counts".to_string(),
semantic_family: "scenario-save-region-header-counts".to_string(),
metadata_tag_offset: 0x3000,
records_tag_offset: 0x3100,
close_tag_offset: 0x3200,
direct_collection_flag: 1,
direct_collection_flag_hex: "0x00000001".to_string(),
direct_record_stride: 0x1d5,
direct_record_stride_hex: "0x000001d5".to_string(),
live_id_bound: 0x32,
live_id_bound_hex: "0x00000032".to_string(),
live_record_count: 0x14,
live_record_count_hex: "0x00000014".to_string(),
header_words: vec![1, 0x1d5, 0x32, 0x14, 0x32, 0x14],
header_hex_words: vec![
"0x00000001".to_string(),
"0x000001d5".to_string(),
"0x00000032".to_string(),
"0x00000014".to_string(),
"0x00000032".to_string(),
"0x00000014".to_string(),
],
evidence: vec![],
});
report.save_placed_structure_collection_header_probe =
Some(SmpSaveTaggedCollectionHeaderProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-placed-structure-tagged-header-counts".to_string(),
semantic_family: "scenario-save-placed-structure-header-counts".to_string(),
metadata_tag_offset: 0x4000,
records_tag_offset: 0x4100,
close_tag_offset: 0x4200,
direct_collection_flag: 0,
direct_collection_flag_hex: "0x00000000".to_string(),
direct_record_stride: 0x06,
direct_record_stride_hex: "0x00000006".to_string(),
live_id_bound: 0x7ee,
live_id_bound_hex: "0x000007ee".to_string(),
live_record_count: 0x7ea,
live_record_count_hex: "0x000007ea".to_string(),
header_words: vec![0, 6, 0x0a, 0x14, 0x7ee, 0x7ea],
header_hex_words: vec![
"0x00000000".to_string(),
"0x00000006".to_string(),
"0x0000000a".to_string(),
"0x00000014".to_string(),
"0x000007ee".to_string(),
"0x000007ea".to_string(),
],
evidence: vec![],
});
let slice = load_save_slice_from_report(&report).expect("save slice");
@ -16462,6 +16634,15 @@ mod tests {
assert!(slice.notes.iter().any(|note| {
note.contains("tagged chairman/profile header reports live_record_count=2")
}));
assert!(
slice
.notes
.iter()
.any(|note| note.contains("tagged region header reports live_record_count=20"))
);
assert!(slice.notes.iter().any(|note| {
note.contains("tagged placed-structure header reports live_record_count=2026")
}));
}
#[test]
@ -16540,6 +16721,84 @@ mod tests {
assert_eq!(probe.live_record_count, 2);
}
#[test]
fn parses_region_tagged_collection_header_probe_from_exact_u32_tags() {
let mut bytes = vec![0u8; 0x400];
let metadata_tag_offset = 0x40usize;
let records_tag_offset = 0x140usize;
let close_tag_offset = 0x180usize;
bytes[metadata_tag_offset..metadata_tag_offset + 4]
.copy_from_slice(&0x00005209u32.to_le_bytes());
bytes[records_tag_offset..records_tag_offset + 4]
.copy_from_slice(&0x0000520au32.to_le_bytes());
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
let header_words = [
1u32, 0x1d5, 0x32, 0x14, 0x32, 0x14, 0x14, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
];
for (index, word) in header_words.into_iter().enumerate() {
let offset = metadata_tag_offset + 4 + index * 4;
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
let probe = parse_save_region_collection_header_probe(
&bytes,
Some("gms"),
Some(&SmpContainerProfile {
profile_family: "rt3-105-save-container-v1".to_string(),
profile_evidence: vec![],
is_known_profile: true,
}),
)
.expect("region header probe should parse");
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
assert_eq!(probe.records_tag_offset, records_tag_offset);
assert_eq!(probe.close_tag_offset, close_tag_offset);
assert_eq!(probe.direct_collection_flag, 1);
assert_eq!(probe.direct_record_stride, 0x1d5);
assert_eq!(probe.live_id_bound, 0x32);
assert_eq!(probe.live_record_count, 0x14);
}
#[test]
fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() {
let mut bytes = vec![0u8; 0x400];
let metadata_tag_offset = 0x40usize;
let records_tag_offset = 0x140usize;
let close_tag_offset = 0x180usize;
bytes[metadata_tag_offset..metadata_tag_offset + 4]
.copy_from_slice(&0x000036b1u32.to_le_bytes());
bytes[records_tag_offset..records_tag_offset + 4]
.copy_from_slice(&0x000036b2u32.to_le_bytes());
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000036b3u32.to_le_bytes());
let header_words = [
0u32, 0x06, 0x0a, 0x14, 0x7ee, 0x7ea, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
for (index, word) in header_words.into_iter().enumerate() {
let offset = metadata_tag_offset + 4 + index * 4;
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
let probe = parse_save_placed_structure_collection_header_probe(
&bytes,
Some("gms"),
Some(&SmpContainerProfile {
profile_family: "rt3-105-save-container-v1".to_string(),
profile_evidence: vec![],
is_known_profile: true,
}),
)
.expect("placed structure header probe should parse");
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
assert_eq!(probe.records_tag_offset, records_tag_offset);
assert_eq!(probe.close_tag_offset, close_tag_offset);
assert_eq!(probe.direct_collection_flag, 0);
assert_eq!(probe.direct_record_stride, 0x06);
assert_eq!(probe.live_id_bound, 0x7ee);
assert_eq!(probe.live_record_count, 0x7ea);
}
#[test]
fn parses_save_company_roster_probe_from_direct_records() {
let metadata_tag_offset = 0x40usize;