diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 12c09b9..a9dbb0c 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -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); diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index b1a294a..af2014d 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -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.