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_trailing_word: u16,
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)]
@ -3278,7 +3287,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}).",
"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.entries.first().map(|entry| entry.primary_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_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.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 {
@ -3699,10 +3709,11 @@ 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={:?}/{:?}.",
"Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}, first footer payload={}.",
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.secondary_name.as_str()),
triplets.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000")
));
}
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 profile_chunk_len =
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 {
record_index: index,
primary_name,
@ -10198,6 +10247,15 @@ fn parse_save_placed_structure_record_triplet_probe(
policy_reserved_dword,
policy_trailing_word,
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 {
@ -17855,7 +17913,25 @@ mod tests {
cursor += 0x1e;
bytes[cursor..cursor + 2]
.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 {
@ -17887,6 +17963,27 @@ mod tests {
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_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].secondary_name, "StationSetB");
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.
- 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
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
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
@ -69,6 +71,10 @@ Working rule:
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
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
year, packed tuple words, absolute counter, and the derived selected-year gap scalar.
- Automatic year-rollover calendar stepping now invokes periodic-boundary service.