Load region triplets into save slices

This commit is contained in:
Jan Petykiewicz 2026-04-19 03:26:50 -07:00
commit 9a4dd5d8d4
5 changed files with 478 additions and 16 deletions

View file

@ -1749,6 +1749,49 @@ pub struct SmpSaveRegionRecordTripletProbe {
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpLoadedRegionProfileEntry {
pub entry_index: usize,
pub name: String,
pub trailing_weight_f32: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpLoadedRegionProfileCollection {
pub direct_collection_flag: u32,
pub entry_stride: u32,
pub live_id_bound: u32,
pub live_record_count: u32,
pub trailing_padding_len: usize,
#[serde(default)]
pub entries: Vec<SmpLoadedRegionProfileEntry>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpLoadedRegionEntry {
pub record_index: usize,
pub name: String,
pub pre_name_prefix_len: usize,
pub policy_leading_f32_0: f32,
pub policy_leading_f32_1: f32,
pub policy_leading_f32_2: f32,
#[serde(default)]
pub policy_reserved_dwords: Vec<u32>,
pub policy_trailing_word: u16,
pub policy_trailing_word_hex: String,
#[serde(default)]
pub profile_collection: Option<SmpLoadedRegionProfileCollection>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpLoadedRegionCollection {
pub source_kind: String,
pub semantic_family: String,
pub observed_entry_count: usize,
#[serde(default)]
pub entries: Vec<SmpLoadedRegionEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveRegionQueuedNoticeRecordEntryProbe {
pub node_base_offset: usize,
@ -4058,7 +4101,7 @@ enum RealGroupedTargetSubject {
WholeGame,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpLoadedSaveSlice {
pub file_extension_hint: Option<String>,
pub container_profile_family: Option<String>,
@ -4086,6 +4129,8 @@ pub struct SmpLoadedSaveSlice {
#[serde(default)]
pub chairman_profile_table: Option<SmpLoadedChairmanProfileTable>,
#[serde(default)]
pub region_collection: Option<SmpLoadedRegionCollection>,
#[serde(default)]
pub placed_structure_collection: Option<SmpLoadedPlacedStructureCollection>,
#[serde(default)]
pub placed_structure_dynamic_side_buffer_summary:
@ -6909,6 +6954,49 @@ fn derive_loaded_placed_structure_collection_from_probe(
}
}
fn derive_loaded_region_collection_from_probe(
probe: &SmpSaveRegionRecordTripletProbe,
) -> SmpLoadedRegionCollection {
SmpLoadedRegionCollection {
source_kind: probe.source_kind.clone(),
semantic_family: "scenario-save-region-triplet-collection".to_string(),
observed_entry_count: probe.record_count,
entries: probe
.entries
.iter()
.map(|entry| SmpLoadedRegionEntry {
record_index: entry.record_index,
name: entry.name.clone(),
pre_name_prefix_len: entry.pre_name_prefix_len,
policy_leading_f32_0: entry.policy_leading_f32_0,
policy_leading_f32_1: entry.policy_leading_f32_1,
policy_leading_f32_2: entry.policy_leading_f32_2,
policy_reserved_dwords: entry.policy_reserved_dwords.clone(),
policy_trailing_word: entry.policy_trailing_word,
policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(),
profile_collection: entry.profile_collection.as_ref().map(|collection| {
SmpLoadedRegionProfileCollection {
direct_collection_flag: collection.direct_collection_flag,
entry_stride: collection.entry_stride,
live_id_bound: collection.live_id_bound,
live_record_count: collection.live_record_count,
trailing_padding_len: collection.trailing_padding_len,
entries: collection
.entries
.iter()
.map(|entry| SmpLoadedRegionProfileEntry {
entry_index: entry.entry_index,
name: entry.name.clone(),
trailing_weight_f32: entry.trailing_weight_f32,
})
.collect(),
}
}),
})
.collect(),
}
}
fn derive_loaded_placed_structure_dynamic_side_buffer_summary(
probe: &SmpSavePlacedStructureDynamicSideBufferProbe,
alignment: Option<&SmpSavePlacedStructureDynamicSideBufferAlignmentProbe>,
@ -7082,6 +7170,10 @@ pub fn load_save_slice_from_report(
)
})
});
let region_collection = report
.save_region_record_triplet_probe
.as_ref()
.map(derive_loaded_region_collection_from_probe);
let placed_structure_collection = report
.save_placed_structure_record_triplet_probe
.as_ref()
@ -7246,6 +7338,36 @@ pub fn load_save_slice_from_report(
})
));
}
if let Some(collection) = &region_collection {
let total_profile_rows = collection
.entries
.iter()
.map(|entry| {
entry
.profile_collection
.as_ref()
.map(|collection| collection.entries.len())
.unwrap_or_default()
})
.sum::<usize>();
let nonzero_prefix_count = collection
.entries
.iter()
.filter(|entry| entry.pre_name_prefix_len != 0)
.count();
let nonzero_reserved_count = collection
.entries
.iter()
.filter(|entry| entry.policy_reserved_dwords.iter().any(|raw| *raw != 0))
.count();
notes.push(format!(
"Save-slice projection now carries {} loaded region triplet rows as first-class context, with {} embedded profile rows, {} rows with nonzero pre-name prefixes, and {} rows with nonzero reserved policy dwords.",
collection.observed_entry_count,
total_profile_rows,
nonzero_prefix_count,
nonzero_reserved_count
));
}
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}.",
@ -7392,6 +7514,7 @@ pub fn load_save_slice_from_report(
world_locomotive_policy_state,
company_roster,
chairman_profile_table,
region_collection,
placed_structure_collection,
placed_structure_dynamic_side_buffer_summary,
special_conditions_table,
@ -25441,6 +25564,100 @@ mod tests {
None,
None,
);
report.save_region_record_triplet_probe = Some(SmpSaveRegionRecordTripletProbe {
profile_family: "rt3-classic-save-container-v1".to_string(),
source_kind: "save-region-record-triplets".to_string(),
semantic_family: "scenario-save-region-record-triplets".to_string(),
records_tag_offset: 0x3400,
close_tag_offset: 0x3500,
record_count: 2,
entries: vec![
SmpSaveRegionRecordTripletEntryProbe {
record_index: 0,
name: "Marker09".to_string(),
record_payload_relative_offset: 0,
record_payload_relative_offset_hex: "0x0".to_string(),
name_tag_relative_offset: 0,
policy_tag_relative_offset: 0x10,
profile_tag_relative_offset: 0x2e,
pre_name_prefix_len: 0,
pre_name_prefix_hex_bytes: Vec::new(),
pre_name_prefix_dword_candidates: Vec::new(),
policy_chunk_len: 0x1a,
profile_chunk_len: 0x40,
policy_leading_f32_0: 368.0,
policy_leading_f32_1: 0.0,
policy_leading_f32_2: 92.0,
policy_reserved_dwords: vec![0, 0, 0],
policy_reserved_dword_candidates: Vec::new(),
policy_trailing_word: 1,
policy_trailing_word_hex: "0x0001".to_string(),
profile_collection: Some(SmpSaveRegionProfileCollectionProbe {
direct_collection_flag: 1,
entry_stride: 0x22,
live_id_bound: 18,
live_record_count: 17,
entry_start_relative_offset: 0x4d,
trailing_padding_len: 2,
entries: vec![
SmpSaveRegionProfileEntryProbe {
entry_index: 0,
row_relative_offset: 0x4d,
name: "House".to_string(),
trailing_weight_f32: 0.2,
},
SmpSaveRegionProfileEntryProbe {
entry_index: 1,
row_relative_offset: 0x6f,
name: "Farm Corn".to_string(),
trailing_weight_f32: 0.2,
},
],
}),
},
SmpSaveRegionRecordTripletEntryProbe {
record_index: 1,
name: "Marker10".to_string(),
record_payload_relative_offset: 0x6e,
record_payload_relative_offset_hex: "0x6e".to_string(),
name_tag_relative_offset: 0x76,
policy_tag_relative_offset: 0x86,
profile_tag_relative_offset: 0xa4,
pre_name_prefix_len: 8,
pre_name_prefix_hex_bytes: vec![
"0xaa".to_string(),
"0xbb".to_string(),
"0xcc".to_string(),
"0xdd".to_string(),
],
pre_name_prefix_dword_candidates: Vec::new(),
policy_chunk_len: 0x1a,
profile_chunk_len: 0x20,
policy_leading_f32_0: 552.0,
policy_leading_f32_1: 0.0,
policy_leading_f32_2: 276.0,
policy_reserved_dwords: vec![0, 4, 0],
policy_reserved_dword_candidates: Vec::new(),
policy_trailing_word: 1,
policy_trailing_word_hex: "0x0001".to_string(),
profile_collection: Some(SmpSaveRegionProfileCollectionProbe {
direct_collection_flag: 1,
entry_stride: 0x22,
live_id_bound: 26,
live_record_count: 24,
entry_start_relative_offset: 0x50,
trailing_padding_len: 0,
entries: vec![SmpSaveRegionProfileEntryProbe {
entry_index: 0,
row_relative_offset: 0x50,
name: "Farm Corn".to_string(),
trailing_weight_f32: 0.2,
}],
}),
},
],
evidence: vec![],
});
report.save_placed_structure_record_triplet_probe =
Some(SmpSavePlacedStructureRecordTripletProbe {
profile_family: "rt3-classic-save-container-v1".to_string(),
@ -25604,6 +25821,24 @@ mod tests {
evidence: vec![],
});
let slice = load_save_slice_from_report(&report).expect("classic save slice");
let region_collection = slice
.region_collection
.expect("region collection should project");
assert_eq!(region_collection.source_kind, "save-region-record-triplets");
assert_eq!(region_collection.observed_entry_count, 2);
assert_eq!(region_collection.entries[0].name, "Marker09");
assert_eq!(region_collection.entries[1].pre_name_prefix_len, 8);
assert_eq!(
region_collection.entries[1].policy_reserved_dwords,
vec![0, 4, 0]
);
assert_eq!(
region_collection.entries[0]
.profile_collection
.as_ref()
.map(|collection| collection.entries.len()),
Some(2)
);
let collection = slice
.placed_structure_collection
.expect("placed structure collection should project");
@ -25629,6 +25864,10 @@ mod tests {
side_buffer_summary.triplet_alignment_side_buffer_only_name_pair_count,
0
);
assert!(slice.notes.iter().any(|line| {
line.contains("loaded region triplet rows as first-class context")
&& line.contains("3 embedded profile rows")
}));
assert!(slice.notes.iter().any(|line| {
line.contains("placed-structure triplet rows as first-class context")
&& line.contains("2")