From 8861074c1b57c01350bb4efa2ab408ac41b7ea72 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 08:35:37 -0700 Subject: [PATCH] Expose save-side region record triplets --- crates/rrt-runtime/src/smp.rs | 213 ++++++++++++++++++++++++++++++++++ docs/rehost-queue.md | 13 ++- 2 files changed, 222 insertions(+), 4 deletions(-) diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index bfef98f..d706e63 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -152,6 +152,9 @@ const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize = INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4; const SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX: usize = 16; const SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT: usize = 3; +const SAVE_REGION_RECORD_NAME_TAG: u16 = 0x55f1; +const SAVE_REGION_RECORD_POLICY_TAG: u16 = 0x55f2; +const SAVE_REGION_RECORD_PROFILE_TAG: u16 = 0x55f3; const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001"; const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1"; const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1"; @@ -1645,6 +1648,30 @@ pub struct SmpSaveTrainCollectionDirectoryProbe { pub evidence: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveRegionRecordTripletEntryProbe { + pub record_index: usize, + pub name: String, + pub name_tag_relative_offset: usize, + pub policy_tag_relative_offset: usize, + pub profile_tag_relative_offset: usize, + pub policy_chunk_len: usize, + pub profile_chunk_len: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveRegionRecordTripletProbe { + pub profile_family: String, + pub source_kind: String, + pub semantic_family: String, + pub records_tag_offset: usize, + pub close_tag_offset: usize, + pub record_count: usize, + #[serde(default)] + pub entries: Vec, + pub evidence: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SmpRt3105SaveNameTableProbe { pub profile_family: String, @@ -2585,6 +2612,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport { #[serde(default)] pub region_collection_header: Option, #[serde(default)] + pub region_record_triplets: Option, + #[serde(default)] pub placed_structure_collection_header: Option, #[serde(default)] pub company_entries: Vec, @@ -2848,6 +2877,7 @@ pub struct SmpInspectionReport { pub save_train_collection_header_probe: Option, pub save_train_collection_directory_probe: Option, pub save_region_collection_header_probe: Option, + pub save_region_record_triplet_probe: Option, pub save_placed_structure_collection_header_probe: Option, #[serde(default)] pub save_company_roster_probe: Option, @@ -3143,6 +3173,13 @@ pub fn load_save_slice_from_report( probe.close_tag_offset )); } + if let Some(probe) = &report.save_region_record_triplet_probe { + notes.push(format!( + "Raw save tagged region records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets in the records span; first name={:?}.", + probe.record_count, + probe.entries.first().map(|entry| entry.name.as_str()) + )); + } if let Some(probe) = &report.save_placed_structure_collection_header_probe { notes.push(format!( "Raw save tagged placed-structure header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", @@ -3215,6 +3252,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( let world_economic_tuning = report.save_world_economic_tuning_probe.clone(); let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone(); let train_collection_directory = report.save_train_collection_directory_probe.clone(); + let region_record_triplets = report.save_region_record_triplet_probe.clone(); let company_header_probe = report.save_company_collection_header_probe.as_ref(); let chairman_header_probe = report .save_chairman_profile_collection_header_probe @@ -3525,6 +3563,13 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( header.live_record_count, header.live_id_bound, header.direct_record_stride )); } + if let Some(triplets) = region_record_triplets.as_ref() { + notes.push(format!( + "Region analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first serialized region name={:?}.", + triplets.record_count, + triplets.entries.first().map(|entry| entry.name.as_str()) + )); + } if let Some(header) = report .save_placed_structure_collection_header_probe .as_ref() @@ -3578,6 +3623,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( train_collection_header: report.save_train_collection_header_probe.clone(), train_collection_directory, region_collection_header: report.save_region_collection_header_probe.clone(), + region_record_triplets, placed_structure_collection_header: report .save_placed_structure_collection_header_probe .clone(), @@ -7593,6 +7639,8 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option) -> Sm file_extension_hint.as_deref(), container_profile.as_ref(), ); + let save_region_record_triplet_probe = + parse_save_region_record_triplet_probe(bytes, save_region_collection_header_probe.as_ref()); let save_placed_structure_collection_header_probe = parse_save_placed_structure_collection_header_probe( bytes, @@ -7765,6 +7813,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option) -> Sm save_train_collection_header_probe, save_train_collection_directory_probe, save_region_collection_header_probe, + save_region_record_triplet_probe, save_placed_structure_collection_header_probe, save_company_roster_probe, save_chairman_profile_table_probe, @@ -9850,6 +9899,76 @@ fn parse_save_region_collection_header_probe( .then_some(probe) } +fn parse_save_region_record_triplet_probe( + bytes: &[u8], + header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, +) -> Option { + let header_probe = header_probe?; + if header_probe.source_kind != "save-region-tagged-header-counts" { + return None; + } + let records_payload = + bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?; + let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG); + let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG); + let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG); + let record_count = header_probe.live_record_count as usize; + if name_offsets.len() != record_count + || policy_offsets.len() != record_count + || profile_offsets.len() != record_count + { + return None; + } + let mut entries = Vec::with_capacity(record_count); + for index in 0..record_count { + let name_tag_relative_offset = name_offsets[index]; + let policy_tag_relative_offset = policy_offsets[index]; + let profile_tag_relative_offset = profile_offsets[index]; + let next_record_relative_offset = name_offsets + .get(index + 1) + .copied() + .unwrap_or(records_payload.len()); + if !(name_tag_relative_offset < policy_tag_relative_offset + && policy_tag_relative_offset < profile_tag_relative_offset + && profile_tag_relative_offset < next_record_relative_offset) + { + return None; + } + let name_payload = + records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?; + let name = parse_save_len_prefixed_ascii_name(name_payload)?; + let policy_chunk_len = + profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?; + let profile_chunk_len = + next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?; + entries.push(SmpSaveRegionRecordTripletEntryProbe { + record_index: index, + name, + name_tag_relative_offset, + policy_tag_relative_offset, + profile_tag_relative_offset, + policy_chunk_len, + profile_chunk_len, + }); + } + Some(SmpSaveRegionRecordTripletProbe { + profile_family: header_probe.profile_family.clone(), + source_kind: "save-region-record-triplets".to_string(), + semantic_family: "scenario-save-region-record-triplets".to_string(), + records_tag_offset: header_probe.records_tag_offset, + close_tag_offset: header_probe.close_tag_offset, + record_count, + entries, + evidence: vec![ + "save-side region records in the non-direct Marker09 family are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the records span".to_string(), + format!( + "decoded {} region record triplets with one len-prefixed name chunk, one fixed policy chunk, and one trailing profile payload chunk per record", + record_count + ), + ], + }) +} + fn parse_save_placed_structure_collection_header_probe( bytes: &[u8], file_extension_hint: Option<&str>, @@ -9993,6 +10112,13 @@ fn parse_save_tagged_collection_header_probe( }) } +fn parse_save_len_prefixed_ascii_name(bytes: &[u8]) -> Option { + let len = *bytes.first()? as usize; + let text_bytes = bytes.get(1..1 + len)?; + let text = std::str::from_utf8(text_bytes).ok()?.trim_end_matches('\0'); + Some(text.to_string()) +} + fn parse_rt3_105_save_name_table_probe( bytes: &[u8], file_extension_hint: Option<&str>, @@ -16809,6 +16935,35 @@ mod tests { ], evidence: vec![], }); + report.save_region_record_triplet_probe = Some(SmpSaveRegionRecordTripletProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-region-record-triplets".to_string(), + semantic_family: "scenario-save-region-record-triplets".to_string(), + records_tag_offset: 0x5100, + close_tag_offset: 0x5200, + record_count: 2, + entries: vec![ + SmpSaveRegionRecordTripletEntryProbe { + record_index: 0, + name: "Marker09".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0x10, + profile_tag_relative_offset: 0x2e, + policy_chunk_len: 0x1a, + profile_chunk_len: 0x40, + }, + SmpSaveRegionRecordTripletEntryProbe { + record_index: 1, + name: "Marker10".to_string(), + name_tag_relative_offset: 0x6e, + policy_tag_relative_offset: 0x7e, + profile_tag_relative_offset: 0x9c, + policy_chunk_len: 0x1a, + profile_chunk_len: 0x20, + }, + ], + evidence: vec![], + }); report.save_placed_structure_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { profile_family: "rt3-105-save-container-v1".to_string(), @@ -16916,6 +17071,11 @@ mod tests { note.contains("tagged region header reports live_record_count=145") }) ); + assert!(slice.notes.iter().any(|note| { + note.contains( + "tagged region records also expose 2 repeated 0x55f1/0x55f2/0x55f3 triplets", + ) + })); assert!(slice.notes.iter().any(|note| { note.contains("tagged placed-structure header reports live_record_count=2026") })); @@ -17133,6 +17293,59 @@ mod tests { assert_eq!(directory_probe.entries[2].next_live_entry_id, 0); } + #[test] + fn parses_region_record_triplet_probe_from_marker09_records() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x140usize; + let close_tag_offset = 0x260usize; + bytes[metadata_tag_offset..metadata_tag_offset + 4] + .copy_from_slice(&0x00005209u32.to_le_bytes()); + bytes[records_tag_offset..records_tag_offset + 4] + .copy_from_slice(&0x0000520au32.to_le_bytes()); + bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes()); + let mut cursor = records_tag_offset + 4; + for name in ["Marker09", "Marker10"] { + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes()); + bytes[cursor + 4] = name.len() as u8; + bytes[cursor + 5..cursor + 5 + name.len()].copy_from_slice(name.as_bytes()); + cursor += 0x10; + bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes()); + cursor += 0x1e; + bytes[cursor..cursor + 2] + .copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes()); + cursor += 0x40; + } + + let header_probe = SmpSaveTaggedCollectionHeaderProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-region-tagged-header-counts".to_string(), + semantic_family: "scenario-save-region-header-counts".to_string(), + metadata_tag_offset, + records_tag_offset, + close_tag_offset, + direct_collection_flag: 0, + direct_collection_flag_hex: "0x00000000".to_string(), + direct_record_stride: 0x06, + direct_record_stride_hex: "0x00000006".to_string(), + live_id_bound: 0x96, + live_id_bound_hex: "0x00000096".to_string(), + live_record_count: 2, + live_record_count_hex: "0x00000002".to_string(), + header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x02], + header_hex_words: vec![], + evidence: vec![], + }; + let triplet_probe = parse_save_region_record_triplet_probe(&bytes, Some(&header_probe)) + .expect("region triplet probe should parse"); + + assert_eq!(triplet_probe.record_count, 2); + assert_eq!(triplet_probe.entries[0].name, "Marker09"); + assert_eq!(triplet_probe.entries[0].policy_tag_relative_offset, 0x10); + assert_eq!(triplet_probe.entries[0].profile_tag_relative_offset, 0x2e); + assert_eq!(triplet_probe.entries[1].name, "Marker10"); + } + #[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 98ad7ba..4af4325 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -10,10 +10,12 @@ Working rule: ## Next - Reconstruct the save-side region record body on top of the newly corrected non-direct tagged - region seam (`0x5209/0x520a/0x520b`, stride hint `0x06`, `Marker09` record stems), especially - the pending bonus lane `[region+0x276]`, completion latch `[region+0x302]`, one-shot notice - latch `[region+0x316]`, severity/source lane `[region+0x25e]`, and any stable region-id or - class discriminator that can drive shellless city-connection service. + region seam (`0x5209/0x520a/0x520b`, stride hint `0x06`, `Marker09` record stems) and its now + grounded repeated `0x55f1/0x55f2/0x55f3` record-triplet envelope, especially the fixed `0x55f2` + policy row behind `[region+0x272/+0x25a/+0x25e]`, the pending bonus lane `[region+0x276]`, + completion latch `[region+0x302]`, one-shot notice latch `[region+0x316]`, severity/source lane + `[region+0x25e]`, and any stable region-id 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. @@ -56,6 +58,9 @@ Working rule: at metadata dword `16`, while the actual region family is the larger non-direct `Marker09` collection with live_id/count `0x96/0x91`; the tagged placed-structure header (`0x36b1/0x36b2/0x36b3`) remains grounded alongside them. +- That same corrected region seam now also exposes repeated `0x55f1/0x55f2/0x55f3` serialized + record triplets with len-prefixed names plus fixed policy/profile chunk lengths, so the next + city-connection pass can target the real record envelope instead of another blind scan. - 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.