Load placed-structure triplets into save slices

This commit is contained in:
Jan Petykiewicz 2026-04-19 03:12:53 -07:00
commit 7abd582aea
5 changed files with 433 additions and 1 deletions

View file

@ -1928,6 +1928,33 @@ pub struct SmpSavePlacedStructureRecordTripletProbe {
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPlacedStructureEntry {
pub record_index: usize,
pub primary_name: String,
pub secondary_name: String,
pub policy_trailing_word: u16,
pub policy_trailing_word_hex: String,
pub profile_payload_dword: u32,
pub profile_payload_dword_hex: String,
pub profile_status_kind: String,
#[serde(default)]
pub farm_growth_stage_index: Option<u8>,
#[serde(default)]
pub profile_companion_byte_u8: Option<u8>,
#[serde(default)]
pub profile_companion_byte_hex: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPlacedStructureCollection {
pub source_kind: String,
pub semantic_family: String,
pub observed_entry_count: usize,
#[serde(default)]
pub entries: Vec<SmpLoadedPlacedStructureEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSavePlacedStructureDynamicSideBufferProbe {
pub profile_family: String,
@ -4038,6 +4065,8 @@ pub struct SmpLoadedSaveSlice {
pub company_roster: Option<SmpLoadedCompanyRoster>,
#[serde(default)]
pub chairman_profile_table: Option<SmpLoadedChairmanProfileTable>,
#[serde(default)]
pub placed_structure_collection: Option<SmpLoadedPlacedStructureCollection>,
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>,
pub notes: Vec<String>,
@ -6830,6 +6859,33 @@ pub fn inspect_smp_bytes(bytes: &[u8]) -> SmpInspectionReport {
inspect_bundle_bytes(bytes, None)
}
fn derive_loaded_placed_structure_collection_from_probe(
probe: &SmpSavePlacedStructureRecordTripletProbe,
) -> SmpLoadedPlacedStructureCollection {
SmpLoadedPlacedStructureCollection {
source_kind: probe.source_kind.clone(),
semantic_family: "scenario-save-placed-structure-triplet-collection".to_string(),
observed_entry_count: probe.record_count,
entries: probe
.entries
.iter()
.map(|entry| SmpLoadedPlacedStructureEntry {
record_index: entry.record_index,
primary_name: entry.primary_name.clone(),
secondary_name: entry.secondary_name.clone(),
policy_trailing_word: entry.policy_trailing_word,
policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(),
profile_payload_dword: entry.profile_payload_dword,
profile_payload_dword_hex: entry.profile_payload_dword_hex.clone(),
profile_status_kind: entry.profile_status_kind.clone(),
farm_growth_stage_index: entry.farm_growth_stage_index,
profile_companion_byte_u8: entry.profile_companion_byte_u8,
profile_companion_byte_hex: entry.profile_companion_byte_hex.clone(),
})
.collect(),
}
}
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)
@ -6974,6 +7030,10 @@ pub fn load_save_slice_from_report(
)
})
});
let placed_structure_collection = report
.save_placed_structure_record_triplet_probe
.as_ref()
.map(derive_loaded_placed_structure_collection_from_probe);
let special_conditions_table =
report
.special_conditions_probe
@ -7144,6 +7204,22 @@ pub fn load_save_slice_from_report(
probe.entries.first().map(|entry| entry.profile_status_kind.as_str())
));
}
if let Some(collection) = &placed_structure_collection {
let farm_growth_stage_count = collection
.entries
.iter()
.filter(|entry| entry.farm_growth_stage_index.is_some())
.count();
let opaque_status_count = collection
.entries
.iter()
.filter(|entry| entry.profile_status_kind != "unset")
.count();
notes.push(format!(
"Save-slice projection now carries {} loaded placed-structure triplet rows as first-class context, with {} farm growth-stage rows and {} non-default footer-status rows.",
collection.observed_entry_count, farm_growth_stage_count, opaque_status_count
));
}
if let Some(probe) = &placed_structure_dynamic_side_buffer_probe {
let dominant_pattern = probe.compact_prefix_pattern_summaries.first();
let payload_envelope_summary = probe.payload_envelope_summary.as_ref();
@ -7240,6 +7316,7 @@ pub fn load_save_slice_from_report(
world_locomotive_policy_state,
company_roster,
chairman_profile_table,
placed_structure_collection,
special_conditions_table,
event_runtime_collection: report.event_runtime_collection_summary.clone(),
notes,
@ -25240,6 +25317,154 @@ mod tests {
);
}
#[test]
fn loads_placed_structure_collection_from_report() {
let mut report = inspect_smp_bytes(&[]);
let classic_probe = SmpClassicRehydrateProfileProbe {
profile_family: "rt3-classic-save-container-v1".to_string(),
progress_32dc_offset: 0x76e8,
progress_3714_offset: 0x76ec,
progress_3715_offset: 0x77f8,
packed_profile_offset: 0x76f0,
packed_profile_len: 0x108,
packed_profile_len_hex: "0x108".to_string(),
packed_profile_block: SmpClassicPackedProfileBlock {
relative_len: 0x108,
relative_len_hex: "0x108".to_string(),
leading_word_0: 3,
leading_word_0_hex: "0x00000003".to_string(),
trailing_zero_word_count_after_leading_word: 3,
map_path_offset: 0x13,
map_path: Some("British Isles.gmp".to_string()),
display_name_offset: 0x46,
display_name: Some("British Isles".to_string()),
profile_byte_0x77: 0,
profile_byte_0x77_hex: "0x00".to_string(),
profile_byte_0x82: 0,
profile_byte_0x82_hex: "0x00".to_string(),
profile_byte_0x97: 0,
profile_byte_0x97_hex: "0x00".to_string(),
profile_byte_0xc5: 0,
profile_byte_0xc5_hex: "0x00".to_string(),
stable_nonzero_words: vec![],
},
ascii_runs: vec![],
};
report.classic_rehydrate_profile_probe = Some(classic_probe.clone());
report.save_load_summary = build_save_load_summary(
Some("gms"),
Some(&SmpContainerProfile {
profile_family: "rt3-classic-save-container-v1".to_string(),
profile_evidence: vec![],
is_known_profile: true,
}),
None,
None,
Some(&classic_probe),
None,
None,
);
report.save_placed_structure_record_triplet_probe =
Some(SmpSavePlacedStructureRecordTripletProbe {
profile_family: "rt3-classic-save-container-v1".to_string(),
source_kind: "save-placed-structure-record-triplets".to_string(),
semantic_family: "scenario-save-placed-structure-record-triplets".to_string(),
records_tag_offset: 0x3600,
close_tag_offset: 0x3800,
record_count: 2,
entries: vec![
SmpSavePlacedStructureRecordTripletEntryProbe {
record_index: 0,
primary_name: "FarmCorn".to_string(),
secondary_name: "FarmSet".to_string(),
name_tag_relative_offset: 0,
policy_tag_relative_offset: 0x10,
profile_tag_relative_offset: 0x2e,
policy_chunk_len: 0x1a,
profile_chunk_len: 0x10,
policy_f32_lane_0: 1.0,
policy_f32_lane_1: 2.0,
policy_f32_lane_2: 3.0,
policy_f32_lane_3: 4.0,
policy_f32_lane_4: 5.0,
policy_reserved_dword: 0,
policy_trailing_word: 1,
policy_trailing_word_hex: "0x0001".to_string(),
profile_open_marker: 0x00005dc1,
profile_open_marker_hex: "0x00005dc1".to_string(),
profile_repeated_primary_name: "FarmCorn".to_string(),
profile_repeated_secondary_name: "FarmSet".to_string(),
profile_footer_relative_offset: 0x08,
profile_footer_relative_offset_hex: "0x8".to_string(),
profile_pre_footer_padding_len: 1,
profile_pre_footer_padding_hex_bytes: vec!["0x00".to_string()],
profile_companion_byte_u8: Some(0),
profile_companion_byte_hex: Some("0x00".to_string()),
profile_payload_dword: 0,
profile_payload_dword_hex: "0x00000000".to_string(),
profile_sentinel_i32: 4,
profile_status_kind: "farm_growth_stage_bucket".to_string(),
farm_growth_stage_index: Some(4),
profile_close_marker: 0x00005dc2,
profile_close_marker_hex: "0x00005dc2".to_string(),
},
SmpSavePlacedStructureRecordTripletEntryProbe {
record_index: 1,
primary_name: "StationA".to_string(),
secondary_name: "StationSetA".to_string(),
name_tag_relative_offset: 0x40,
policy_tag_relative_offset: 0x50,
profile_tag_relative_offset: 0x6e,
policy_chunk_len: 0x1a,
profile_chunk_len: 0x10,
policy_f32_lane_0: 0.0,
policy_f32_lane_1: 0.0,
policy_f32_lane_2: 0.0,
policy_f32_lane_3: 0.0,
policy_f32_lane_4: 0.0,
policy_reserved_dword: 0,
policy_trailing_word: 1,
policy_trailing_word_hex: "0x0001".to_string(),
profile_open_marker: 0x00005dc1,
profile_open_marker_hex: "0x00005dc1".to_string(),
profile_repeated_primary_name: "StationA".to_string(),
profile_repeated_secondary_name: "StationSetA".to_string(),
profile_footer_relative_offset: 0x08,
profile_footer_relative_offset_hex: "0x8".to_string(),
profile_pre_footer_padding_len: 1,
profile_pre_footer_padding_hex_bytes: vec!["0x07".to_string()],
profile_companion_byte_u8: Some(7),
profile_companion_byte_hex: Some("0x07".to_string()),
profile_payload_dword: 0x00005dc1,
profile_payload_dword_hex: "0x00005dc1".to_string(),
profile_sentinel_i32: 0,
profile_status_kind: "opaque_nondefault".to_string(),
farm_growth_stage_index: None,
profile_close_marker: 0x00005dc2,
profile_close_marker_hex: "0x00005dc2".to_string(),
},
],
evidence: vec![],
});
let slice = load_save_slice_from_report(&report).expect("classic save slice");
let collection = slice
.placed_structure_collection
.expect("placed structure collection should project");
assert_eq!(
collection.source_kind,
"save-placed-structure-record-triplets"
);
assert_eq!(collection.observed_entry_count, 2);
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));
assert!(slice.notes.iter().any(|line| {
line.contains("placed-structure triplet rows as first-class context")
&& line.contains("2")
}));
}
#[test]
fn loads_rt3_105_save_slice_from_report() {
let mut report = inspect_smp_bytes(&[]);