diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index a9dbb0c..1a7b8ef 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -1726,6 +1726,8 @@ pub struct SmpSavePlacedStructureRecordTripletEntryProbe { pub profile_payload_dword: u32, pub profile_payload_dword_hex: String, pub profile_sentinel_i32: i32, + pub profile_status_kind: String, + pub farm_growth_stage_index: Option, pub profile_close_marker: u32, pub profile_close_marker_hex: String, } @@ -3287,7 +3289,7 @@ pub fn load_save_slice_from_report( } if let Some(probe) = &report.save_placed_structure_record_triplet_probe { notes.push(format!( - "Raw save tagged placed-structure records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets; first stems={:?}/{:?}, first policy lanes=({:.3}, {:.3}, {:.3}, {:.3}, {:.3}), first footer payload={}.", + "Raw save tagged placed-structure records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets; first stems={:?}/{:?}, first policy lanes=({:.3}, {:.3}, {:.3}, {:.3}, {:.3}), first footer payload={}, first footer status kind={:?}.", probe.record_count, probe.entries.first().map(|entry| entry.primary_name.as_str()), probe.entries.first().map(|entry| entry.secondary_name.as_str()), @@ -3296,7 +3298,8 @@ pub fn load_save_slice_from_report( probe.entries.first().map(|entry| entry.policy_f32_lane_2).unwrap_or_default(), probe.entries.first().map(|entry| entry.policy_f32_lane_3).unwrap_or_default(), probe.entries.first().map(|entry| entry.policy_f32_lane_4).unwrap_or_default(), - probe.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000") + probe.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000"), + probe.entries.first().map(|entry| entry.profile_status_kind.as_str()) )); } if let Some(roster) = &report.save_company_roster_probe { @@ -3709,11 +3712,12 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( } if let Some(triplets) = placed_structure_record_triplets.as_ref() { notes.push(format!( - "Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}, first footer payload={}.", + "Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}, first footer payload={}, first footer status kind={:?}.", triplets.record_count, triplets.entries.first().map(|entry| entry.primary_name.as_str()), triplets.entries.first().map(|entry| entry.secondary_name.as_str()), - triplets.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000") + triplets.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000"), + triplets.entries.first().map(|entry| entry.profile_status_kind.as_str()) )); } if !company_entries.is_empty() { @@ -10230,6 +10234,12 @@ fn parse_save_placed_structure_record_triplet_probe( } } let (profile_payload_dword, profile_sentinel_i32, profile_close_marker) = matched_footer?; + let (profile_status_kind, farm_growth_stage_index) = + derive_save_placed_structure_profile_status( + &primary_name, + &secondary_name, + profile_sentinel_i32, + ); entries.push(SmpSavePlacedStructureRecordTripletEntryProbe { record_index: index, primary_name, @@ -10254,10 +10264,16 @@ fn parse_save_placed_structure_record_triplet_probe( profile_payload_dword, profile_payload_dword_hex: format!("0x{profile_payload_dword:08x}"), profile_sentinel_i32, + profile_status_kind: profile_status_kind.to_string(), + farm_growth_stage_index, profile_close_marker, profile_close_marker_hex: format!("0x{profile_close_marker:08x}"), }); } + let farm_growth_stage_entry_count = entries + .iter() + .filter(|entry| entry.farm_growth_stage_index.is_some()) + .count(); Some(SmpSavePlacedStructureRecordTripletProbe { profile_family: header_probe.profile_family.clone(), source_kind: "save-placed-structure-record-triplets".to_string(), @@ -10270,10 +10286,28 @@ fn parse_save_placed_structure_record_triplet_probe( "save-side placed-structure records are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the tagged records span".to_string(), "the 0x55f1 chunk currently exposes two len-prefixed structure-name stems before the fixed 0x55f2 policy row".to_string(), "each fixed placed-structure 0x55f2 policy chunk currently decodes as five f32-like lanes, one reserved dword, and one trailing u16 word".to_string(), + format!( + "the compact 0x55f3 footer status lane behaves like a farm growth-stage bucket on grounded saves: {farm_growth_stage_entry_count} entries expose nonnegative 0..11 values and all observed non-farm families stay at -1" + ), ], }) } +fn derive_save_placed_structure_profile_status( + primary_name: &str, + secondary_name: &str, + raw_status: i32, +) -> (&'static str, Option) { + let looks_like_farm = primary_name.starts_with("Farm") || secondary_name.contains("Farm"); + if raw_status == -1 { + return ("unset", None); + } + if looks_like_farm && (0..=11).contains(&raw_status) { + return ("farm_growth_stage_bucket", Some(raw_status as u8)); + } + ("opaque_nondefault", None) +} + fn parse_save_placed_structure_collection_header_probe( bytes: &[u8], file_extension_hint: Option<&str>, @@ -17980,6 +18014,8 @@ mod tests { "0x0e373500" ); assert_eq!(triplet_probe.entries[0].profile_sentinel_i32, -1); + assert_eq!(triplet_probe.entries[0].profile_status_kind, "unset"); + assert_eq!(triplet_probe.entries[0].farm_growth_stage_index, None); assert_eq!( triplet_probe.entries[0].profile_close_marker_hex, "0x00005dc2" @@ -17989,6 +18025,22 @@ mod tests { assert_eq!(triplet_probe.entries[1].policy_f32_lane_1, 1200.0); } + #[test] + fn derives_placed_structure_farm_growth_stage_from_nonnegative_status() { + assert_eq!( + derive_save_placed_structure_profile_status("FarmCorn", "FarmSet", 4), + ("farm_growth_stage_bucket", Some(4)) + ); + assert_eq!( + derive_save_placed_structure_profile_status("StationA", "StationSetA", -1), + ("unset", None) + ); + assert_eq!( + derive_save_placed_structure_profile_status("StationA", "StationSetA", 4), + ("opaque_nondefault", None) + ); + } + #[test] fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() { let mut bytes = vec![0u8; 0x400]; diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index af2014d..0a3650e 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -75,6 +75,10 @@ Working rule: triplets with dual name stems, a fixed five-`f32` policy row, and a compact `0x5dc1...0x5dc2` footer carrying one raw `u32` payload lane plus one live `i32` status lane, so the remaining placed-structure work is semantic closure of those owned fields rather than envelope discovery. +- That compact placed-structure `i32` footer status lane is now partially grounded as owned + semantics too: observed non-farm families stay at `-1`, while farm families use nonnegative + `0..11` buckets that are now exported as farm growth-stage indices instead of opaque raw status + residue. - Stepped calendar progression now also refreshes save-world owner time fields, including packed year, packed tuple words, absolute counter, and the derived selected-year gap scalar. - Automatic year-rollover calendar stepping now invokes periodic-boundary service.