Ground farm growth buckets in placed-structure saves

This commit is contained in:
Jan Petykiewicz 2026-04-18 11:00:28 -07:00
commit 4cec28e092
2 changed files with 60 additions and 4 deletions

View file

@ -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<u8>,
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<u8>) {
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];

View file

@ -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.