Decode save-side placed-structure footers

This commit is contained in:
Jan Petykiewicz 2026-04-18 10:52:59 -07:00
commit f13d710556
2 changed files with 109 additions and 6 deletions

View file

@ -1719,6 +1719,15 @@ pub struct SmpSavePlacedStructureRecordTripletEntryProbe {
pub policy_reserved_dword: u32, pub policy_reserved_dword: u32,
pub policy_trailing_word: u16, pub policy_trailing_word: u16,
pub policy_trailing_word_hex: String, pub policy_trailing_word_hex: String,
pub profile_open_marker: u32,
pub profile_open_marker_hex: String,
pub profile_repeated_primary_name: String,
pub profile_repeated_secondary_name: String,
pub profile_payload_dword: u32,
pub profile_payload_dword_hex: String,
pub profile_sentinel_i32: i32,
pub profile_close_marker: u32,
pub profile_close_marker_hex: String,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -3278,7 +3287,7 @@ pub fn load_save_slice_from_report(
} }
if let Some(probe) = &report.save_placed_structure_record_triplet_probe { if let Some(probe) = &report.save_placed_structure_record_triplet_probe {
notes.push(format!( 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}).", "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={}.",
probe.record_count, probe.record_count,
probe.entries.first().map(|entry| entry.primary_name.as_str()), probe.entries.first().map(|entry| entry.primary_name.as_str()),
probe.entries.first().map(|entry| entry.secondary_name.as_str()), probe.entries.first().map(|entry| entry.secondary_name.as_str()),
@ -3286,7 +3295,8 @@ pub fn load_save_slice_from_report(
probe.entries.first().map(|entry| entry.policy_f32_lane_1).unwrap_or_default(), probe.entries.first().map(|entry| entry.policy_f32_lane_1).unwrap_or_default(),
probe.entries.first().map(|entry| entry.policy_f32_lane_2).unwrap_or_default(), 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_3).unwrap_or_default(),
probe.entries.first().map(|entry| entry.policy_f32_lane_4).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")
)); ));
} }
if let Some(roster) = &report.save_company_roster_probe { if let Some(roster) = &report.save_company_roster_probe {
@ -3699,10 +3709,11 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
} }
if let Some(triplets) = placed_structure_record_triplets.as_ref() { if let Some(triplets) = placed_structure_record_triplets.as_ref() {
notes.push(format!( notes.push(format!(
"Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}.", "Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}, first footer payload={}.",
triplets.record_count, triplets.record_count,
triplets.entries.first().map(|entry| entry.primary_name.as_str()), 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.secondary_name.as_str()),
triplets.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000")
)); ));
} }
if !company_entries.is_empty() { if !company_entries.is_empty() {
@ -10181,6 +10192,44 @@ fn parse_save_placed_structure_record_triplet_probe(
let policy_trailing_word = read_u16_at(policy_payload, 24)?; let policy_trailing_word = read_u16_at(policy_payload, 24)?;
let profile_chunk_len = let profile_chunk_len =
next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?; next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?;
let profile_payload =
records_payload.get(profile_tag_relative_offset + 4..next_record_relative_offset)?;
let profile_open_marker = read_u32_at(profile_payload, 0)?;
if profile_open_marker != 0x00005dc1 {
return None;
}
let (profile_repeated_primary_name, profile_repeated_secondary_name) =
parse_save_len_prefixed_ascii_name_pair(profile_payload.get(4..)?)?;
let mut trailer_offset = 4usize;
let repeated_primary_len = *profile_payload.get(trailer_offset)? as usize;
trailer_offset += 1 + repeated_primary_len;
while matches!(profile_payload.get(trailer_offset), Some(0)) {
trailer_offset += 1;
}
let repeated_secondary_len = *profile_payload.get(trailer_offset)? as usize;
trailer_offset += 1 + repeated_secondary_len;
let mut matched_footer = None;
for candidate_offset in [trailer_offset, trailer_offset + 1] {
if let (
Some(profile_payload_dword),
Some(profile_sentinel_i32),
Some(profile_close_marker),
) = (
read_u32_at(profile_payload, candidate_offset),
read_i32_at(profile_payload, candidate_offset + 4),
read_u32_at(profile_payload, candidate_offset + 8),
) {
if profile_close_marker == 0x00005dc2 {
matched_footer = Some((
profile_payload_dword,
profile_sentinel_i32,
profile_close_marker,
));
break;
}
}
}
let (profile_payload_dword, profile_sentinel_i32, profile_close_marker) = matched_footer?;
entries.push(SmpSavePlacedStructureRecordTripletEntryProbe { entries.push(SmpSavePlacedStructureRecordTripletEntryProbe {
record_index: index, record_index: index,
primary_name, primary_name,
@ -10198,6 +10247,15 @@ fn parse_save_placed_structure_record_triplet_probe(
policy_reserved_dword, policy_reserved_dword,
policy_trailing_word, policy_trailing_word,
policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"), policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"),
profile_open_marker,
profile_open_marker_hex: format!("0x{profile_open_marker:08x}"),
profile_repeated_primary_name,
profile_repeated_secondary_name,
profile_payload_dword,
profile_payload_dword_hex: format!("0x{profile_payload_dword:08x}"),
profile_sentinel_i32,
profile_close_marker,
profile_close_marker_hex: format!("0x{profile_close_marker:08x}"),
}); });
} }
Some(SmpSavePlacedStructureRecordTripletProbe { Some(SmpSavePlacedStructureRecordTripletProbe {
@ -17855,7 +17913,25 @@ mod tests {
cursor += 0x1e; cursor += 0x1e;
bytes[cursor..cursor + 2] bytes[cursor..cursor + 2]
.copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); .copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes());
cursor += 0x10; bytes[cursor + 4..cursor + 8].copy_from_slice(&0x5dc1u32.to_le_bytes());
let mut payload_cursor = cursor + 8;
bytes[payload_cursor] = primary.len() as u8;
bytes[payload_cursor + 1..payload_cursor + 1 + primary.len()]
.copy_from_slice(primary.as_bytes());
payload_cursor += 1 + primary.len();
bytes[payload_cursor] = 0;
payload_cursor += 1;
bytes[payload_cursor] = secondary.len() as u8;
bytes[payload_cursor + 1..payload_cursor + 1 + secondary.len()]
.copy_from_slice(secondary.as_bytes());
payload_cursor += 1 + secondary.len();
bytes[payload_cursor] = 0;
payload_cursor += 1;
bytes[payload_cursor..payload_cursor + 4].copy_from_slice(&0x0e373500u32.to_le_bytes());
bytes[payload_cursor + 4..payload_cursor + 8].copy_from_slice(&(-1i32).to_le_bytes());
bytes[payload_cursor + 8..payload_cursor + 12]
.copy_from_slice(&0x5dc2u32.to_le_bytes());
cursor += 0x18 + primary.len() + secondary.len();
} }
let header_probe = SmpSaveTaggedCollectionHeaderProbe { let header_probe = SmpSaveTaggedCollectionHeaderProbe {
@ -17887,6 +17963,27 @@ mod tests {
assert_eq!(triplet_probe.entries[0].policy_chunk_len, 0x1a); assert_eq!(triplet_probe.entries[0].policy_chunk_len, 0x1a);
assert_eq!(triplet_probe.entries[0].policy_f32_lane_4, 5.9760494); assert_eq!(triplet_probe.entries[0].policy_f32_lane_4, 5.9760494);
assert_eq!(triplet_probe.entries[0].policy_trailing_word, 0x0101); assert_eq!(triplet_probe.entries[0].policy_trailing_word, 0x0101);
assert_eq!(
triplet_probe.entries[0].profile_open_marker_hex,
"0x00005dc1"
);
assert_eq!(
triplet_probe.entries[0].profile_repeated_primary_name,
"StationA"
);
assert_eq!(
triplet_probe.entries[0].profile_repeated_secondary_name,
"StationSetA"
);
assert_eq!(
triplet_probe.entries[0].profile_payload_dword_hex,
"0x0e373500"
);
assert_eq!(triplet_probe.entries[0].profile_sentinel_i32, -1);
assert_eq!(
triplet_probe.entries[0].profile_close_marker_hex,
"0x00005dc2"
);
assert_eq!(triplet_probe.entries[1].primary_name, "StationB"); assert_eq!(triplet_probe.entries[1].primary_name, "StationB");
assert_eq!(triplet_probe.entries[1].secondary_name, "StationSetB"); assert_eq!(triplet_probe.entries[1].secondary_name, "StationSetB");
assert_eq!(triplet_probe.entries[1].policy_f32_lane_1, 1200.0); assert_eq!(triplet_probe.entries[1].policy_f32_lane_1, 1200.0);

View file

@ -18,7 +18,9 @@ Working rule:
or class discriminator that can drive shellless city-connection service. or class discriminator that can drive shellless city-connection service.
- Reconstruct the save-side placed-structure collection body on top of the newly grounded - Reconstruct the save-side placed-structure collection body on top of the newly grounded
`0x36b1/0x36b2/0x36b3` header seam so the blocked city-connection / linked-transit branch can `0x36b1/0x36b2/0x36b3` header seam so the blocked city-connection / linked-transit branch can
stop depending on atlas-only placed-structure and local-runtime refresh notes. stop depending on atlas-only placed-structure and local-runtime refresh notes, especially the
semantics of the now-grounded compact `0x55f3` footer dword/status lane and any deeper side
buffers beyond the repeated `0x55f1/0x55f2/0x55f3` triplet envelope.
- Extend shellless clock advancement so more periodic-company service branches consume owned - Extend shellless clock advancement so more periodic-company service branches consume owned
runtime time state directly instead of only the explicit periodic service command. runtime time state directly instead of only the explicit periodic service command.
- Keep widening selected-year world-owner state only when a full owning reader/rebuild family is - Keep widening selected-year world-owner state only when a full owning reader/rebuild family is
@ -69,6 +71,10 @@ Working rule:
live-id/count headers, fixed `0x22`-byte rows, profile names, and trailing weight scalars, so live-id/count headers, fixed `0x22`-byte rows, profile names, and trailing weight scalars, so
the remaining region work is on the unresolved payload fields above that collection rather than the remaining region work is on the unresolved payload fields above that collection rather than
on the profile subcollection itself. on the profile subcollection itself.
- The placed-structure tagged save stream now also exposes repeated `0x55f1/0x55f2/0x55f3`
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.
- Stepped calendar progression now also refreshes save-world owner time fields, including packed - 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. year, packed tuple words, absolute counter, and the derived selected-year gap scalar.
- Automatic year-rollover calendar stepping now invokes periodic-boundary service. - Automatic year-rollover calendar stepping now invokes periodic-boundary service.