Load placed-structure side-buffer summaries into save slices

This commit is contained in:
Jan Petykiewicz 2026-04-19 03:18:42 -07:00
commit 777e11e230
5 changed files with 337 additions and 15 deletions

View file

@ -2019,6 +2019,26 @@ pub struct SmpSavePlacedStructureDynamicSideBufferProbe {
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPlacedStructureDynamicSideBufferSummary {
pub source_kind: String,
pub semantic_family: String,
pub observed_entry_count: u32,
pub owner_shared_dword_hex: String,
pub unique_embedded_name_pair_count: usize,
pub decoded_embedded_name_row_count: usize,
pub first_prefix_leading_dword_hex: String,
pub first_prefix_trailing_word_hex: String,
pub first_prefix_separator_byte_hex: String,
pub triplet_alignment_overlap_count: usize,
pub triplet_alignment_side_buffer_only_name_pair_count: usize,
#[serde(default)]
pub compact_prefix_pattern_summaries:
Vec<SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary>,
#[serde(default)]
pub name_pair_summaries: Vec<SmpSavePlacedStructureDynamicSideBufferNamePairSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSavePlacedStructureDynamicSideBufferSampleEntry {
pub sample_index: usize,
@ -4067,6 +4087,9 @@ pub struct SmpLoadedSaveSlice {
pub chairman_profile_table: Option<SmpLoadedChairmanProfileTable>,
#[serde(default)]
pub placed_structure_collection: Option<SmpLoadedPlacedStructureCollection>,
#[serde(default)]
pub placed_structure_dynamic_side_buffer_summary:
Option<SmpLoadedPlacedStructureDynamicSideBufferSummary>,
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>,
pub notes: Vec<String>,
@ -6886,6 +6909,35 @@ fn derive_loaded_placed_structure_collection_from_probe(
}
}
fn derive_loaded_placed_structure_dynamic_side_buffer_summary(
probe: &SmpSavePlacedStructureDynamicSideBufferProbe,
alignment: Option<&SmpSavePlacedStructureDynamicSideBufferAlignmentProbe>,
) -> SmpLoadedPlacedStructureDynamicSideBufferSummary {
SmpLoadedPlacedStructureDynamicSideBufferSummary {
source_kind: probe.source_kind.clone(),
semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-summary".to_string(),
observed_entry_count: probe.live_record_count,
owner_shared_dword_hex: probe.owner_shared_dword_hex.clone(),
unique_embedded_name_pair_count: probe.unique_embedded_name_pair_count,
decoded_embedded_name_row_count: probe.decoded_embedded_name_row_count,
first_prefix_leading_dword_hex: probe.prefix_leading_dword_hex.clone(),
first_prefix_trailing_word_hex: probe.prefix_trailing_word_hex.clone(),
first_prefix_separator_byte_hex: probe.prefix_separator_byte_hex.clone(),
triplet_alignment_overlap_count: alignment
.map(|alignment| alignment.overlapping_name_pair_count)
.unwrap_or_default(),
triplet_alignment_side_buffer_only_name_pair_count: alignment
.map(|alignment| {
alignment
.unique_side_buffer_name_pair_count
.saturating_sub(alignment.overlapping_name_pair_count)
})
.unwrap_or_default(),
compact_prefix_pattern_summaries: probe.compact_prefix_pattern_summaries.clone(),
name_pair_summaries: probe.name_pair_summaries.clone(),
}
}
pub fn load_save_slice_file(path: &Path) -> Result<SmpLoadedSaveSlice, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(path)?;
load_save_slice_from_report(&inspection)
@ -7049,6 +7101,22 @@ pub fn load_save_slice_from_report(
let placed_structure_dynamic_side_buffer_probe = report
.save_placed_structure_dynamic_side_buffer_probe
.clone();
let placed_structure_dynamic_side_buffer_alignment = report
.save_placed_structure_dynamic_side_buffer_probe
.as_ref()
.zip(report.save_placed_structure_record_triplet_probe.as_ref())
.map(|(side_buffer, triplets)| {
summarize_placed_structure_dynamic_side_buffer_alignment(side_buffer, triplets)
});
let placed_structure_dynamic_side_buffer_summary = report
.save_placed_structure_dynamic_side_buffer_probe
.as_ref()
.map(|probe| {
derive_loaded_placed_structure_dynamic_side_buffer_summary(
probe,
placed_structure_dynamic_side_buffer_alignment.as_ref(),
)
});
let mut notes = summary.notes.clone();
if let Some(probe) = &report.save_world_selection_context_probe {
notes.push(format!(
@ -7285,6 +7353,14 @@ pub fn load_save_slice_from_report(
);
}
}
if let Some(summary) = &placed_structure_dynamic_side_buffer_summary {
notes.push(format!(
"Save-slice projection now also carries the placed-structure dynamic side-buffer summary with {} decoded name rows, {} unique name pairs, and {} overlapping triplet name pairs.",
summary.decoded_embedded_name_row_count,
summary.unique_embedded_name_pair_count,
summary.triplet_alignment_overlap_count
));
}
if let Some(roster) = &report.save_company_roster_probe {
notes.push(format!(
"Raw save inspection reconstructed {} company direct records from the tagged company collection.",
@ -7317,6 +7393,7 @@ pub fn load_save_slice_from_report(
company_roster,
chairman_profile_table,
placed_structure_collection,
placed_structure_dynamic_side_buffer_summary,
special_conditions_table,
event_runtime_collection: report.event_runtime_collection_summary.clone(),
notes,
@ -25446,7 +25523,86 @@ mod tests {
],
evidence: vec![],
});
report.save_placed_structure_dynamic_side_buffer_probe =
Some(SmpSavePlacedStructureDynamicSideBufferProbe {
profile_family: "rt3-classic-save-container-v1".to_string(),
source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(),
semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records"
.to_string(),
metadata_tag_offset: 0x3800,
records_tag_offset: 0x3900,
close_tag_offset: 0x3d00,
records_span_len: 0x400,
direct_record_stride: 6,
direct_record_stride_hex: "0x6".to_string(),
live_id_bound: 0x80,
live_id_bound_hex: "0x00000080".to_string(),
live_record_count: 118,
live_record_count_hex: "0x00000076".to_string(),
owner_shared_dword: 0xff0000ff,
owner_shared_dword_hex: "0xff0000ff".to_string(),
owner_shared_dword_relative_offset: 0,
owner_shared_dword_matches_first_compact_prefix_leading_dword: true,
first_record_child_count_after_owner_shared: Some(1),
first_record_child_count_after_owner_shared_hex: Some("0x0001".to_string()),
first_record_saved_primary_child_byte_after_owner_shared: Some(0xff),
first_record_saved_primary_child_byte_after_owner_shared_hex: Some(
"0xff".to_string(),
),
first_record_first_name_tag_relative_offset_after_owner_shared: Some(3),
prefix_leading_dword: 0xff0000ff,
prefix_leading_dword_hex: "0xff0000ff".to_string(),
prefix_trailing_word: 1,
prefix_trailing_word_hex: "0x0001".to_string(),
prefix_separator_byte: 0xff,
prefix_separator_byte_hex: "0xff".to_string(),
first_embedded_name_tag_relative_offset: 3,
embedded_name_tag_count: 118,
decoded_embedded_name_row_count: 118,
decoded_embedded_name_row_with_tertiary_name_count: 0,
unique_compact_prefix_pattern_count: 4,
prefix_leading_dword_matching_embedded_profile_tag_count: 0,
unique_embedded_name_pair_count: 9,
first_embedded_primary_name: Some("StationA".to_string()),
first_embedded_secondary_name: Some("StationSetA".to_string()),
first_embedded_tertiary_name: None,
embedded_name_row_samples: vec![],
compact_prefix_pattern_summaries: vec![
SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary {
prefix_leading_dword: 0xff0000ff,
prefix_leading_dword_hex: "0xff0000ff".to_string(),
prefix_trailing_word: 1,
prefix_trailing_word_hex: "0x0001".to_string(),
prefix_separator_byte: 0xff,
prefix_separator_byte_hex: "0xff".to_string(),
count: 62,
first_name_tag_relative_offset: 3,
prefix_leading_dword_matches_embedded_profile_tag: false,
section_like_primary_name_count: 12,
cap_like_primary_name_count: 21,
other_primary_name_count: 29,
first_primary_name: Some("StationA".to_string()),
first_secondary_name: Some("StationSetA".to_string()),
},
],
name_pair_summaries: vec![SmpSavePlacedStructureDynamicSideBufferNamePairSummary {
primary_name: "StationA".to_string(),
secondary_name: "StationSetA".to_string(),
count: 14,
first_name_tag_relative_offset: 3,
unique_compact_prefix_pattern_count: 1,
dominant_prefix_leading_dword: 0xff0000ff,
dominant_prefix_leading_dword_hex: "0xff0000ff".to_string(),
dominant_prefix_trailing_word: 1,
dominant_prefix_trailing_word_hex: "0x0001".to_string(),
dominant_prefix_separator_byte: 0xff,
dominant_prefix_separator_byte_hex: "0xff".to_string(),
dominant_prefix_count: 14,
}],
payload_envelope_summary: None,
live_entry_prelude_summary: None,
evidence: vec![],
});
let slice = load_save_slice_from_report(&report).expect("classic save slice");
let collection = slice
.placed_structure_collection
@ -25459,10 +25615,28 @@ mod tests {
assert_eq!(collection.entries[0].primary_name, "FarmCorn");
assert_eq!(collection.entries[0].farm_growth_stage_index, Some(4));
assert_eq!(collection.entries[1].profile_companion_byte_u8, Some(7));
let side_buffer_summary = slice
.placed_structure_dynamic_side_buffer_summary
.expect("side-buffer summary should project");
assert_eq!(
side_buffer_summary.source_kind,
"save-placed-structure-dynamic-side-buffer-records"
);
assert_eq!(side_buffer_summary.observed_entry_count, 118);
assert_eq!(side_buffer_summary.unique_embedded_name_pair_count, 9);
assert_eq!(side_buffer_summary.triplet_alignment_overlap_count, 1);
assert_eq!(
side_buffer_summary.triplet_alignment_side_buffer_only_name_pair_count,
0
);
assert!(slice.notes.iter().any(|line| {
line.contains("placed-structure triplet rows as first-class context")
&& line.contains("2")
}));
assert!(slice.notes.iter().any(|line| {
line.contains("placed-structure dynamic side-buffer summary")
&& line.contains("118 decoded name rows")
}));
}
#[test]