From 86511f9670060aba08a3b43153a3bf2a93f5e536 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 08:26:58 -0700 Subject: [PATCH] Correct save train and region collection probes --- README.md | 11 +- crates/rrt-runtime/src/smp.rs | 308 ++++++++++++++++++++++++---------- docs/README.md | 11 +- docs/rehost-queue.md | 27 +-- docs/runtime-rehost-plan.md | 11 +- 5 files changed, 253 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index ad515ba..c52124e 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,12 @@ electric-only periodic-company override rewrites the world route-preference byte company preference, ending it restores the base world byte, and runtime service state now carries both the active and last applied override instead of treating the route-preference lane as a reader-only bridge. -Save inspection now also exposes the tagged live region header -`0x5209/0x520a/0x520b` plus its live-entry directory rooted at metadata dword `16`, and the -tagged placed-structure header `0x36b1/0x36b2/0x36b3`, as first-class owner seams, so the -remaining city-connection / linked-transit blocker is record-body reconstruction rather than -missing save-side collection identity. +Save inspection now also separates the shared `0x5209/0x520a/0x520b` save family correctly: the +smaller direct `0x1d5` collection is the live train family and now exposes a live-entry +directory rooted at metadata dword `16`, while the actual region collection is the larger +non-direct `Marker09` family. The tagged placed-structure header `0x36b1/0x36b2/0x36b3` is +grounded alongside them, so the remaining city-connection / linked-transit blocker is +record-body reconstruction rather than missing save-side collection identity. That same seam now also derives the current live coupon burden directly from owned bond slots, so later finance service work can consume a runtime reader instead of recomputing from scattered raw fields. diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 2340c93..bfef98f 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -1614,7 +1614,7 @@ pub struct SmpSaveTaggedCollectionHeaderProbe { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveRegionCollectionDirectoryEntryProbe { +pub struct SmpSaveTrainCollectionDirectoryEntryProbe { pub live_entry_id: u32, pub payload_relative_offset: u32, pub payload_relative_offset_hex: String, @@ -1626,7 +1626,7 @@ pub struct SmpSaveRegionCollectionDirectoryEntryProbe { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SmpSaveRegionCollectionDirectoryProbe { +pub struct SmpSaveTrainCollectionDirectoryProbe { pub profile_family: String, pub source_kind: String, pub semantic_family: String, @@ -1641,7 +1641,7 @@ pub struct SmpSaveRegionCollectionDirectoryProbe { pub chain_head_live_entry_id: Option, #[serde(default)] pub chain_tail_live_entry_id: Option, - pub entries: Vec, + pub entries: Vec, pub evidence: Vec, } @@ -2579,9 +2579,11 @@ pub struct SmpSaveCompanyChairmanAnalysisReport { #[serde(default)] pub world_finance_neighborhood: Option, #[serde(default)] - pub region_collection_header: Option, + pub train_collection_header: Option, #[serde(default)] - pub region_collection_directory: Option, + pub train_collection_directory: Option, + #[serde(default)] + pub region_collection_header: Option, #[serde(default)] pub placed_structure_collection_header: Option, #[serde(default)] @@ -2843,8 +2845,9 @@ pub struct SmpInspectionReport { pub save_world_finance_neighborhood_probe: Option, pub save_company_collection_header_probe: Option, pub save_chairman_profile_collection_header_probe: Option, + pub save_train_collection_header_probe: Option, + pub save_train_collection_directory_probe: Option, pub save_region_collection_header_probe: Option, - pub save_region_collection_directory_probe: Option, pub save_placed_structure_collection_header_probe: Option, #[serde(default)] pub save_company_roster_probe: Option, @@ -3109,9 +3112,9 @@ pub fn load_save_slice_from_report( probe.close_tag_offset )); } - if let Some(probe) = &report.save_region_collection_header_probe { + if let Some(probe) = &report.save_train_collection_header_probe { notes.push(format!( - "Raw save tagged region header reports live_record_count={} and live_id_bound={} with direct_record_stride=0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", + "Raw save tagged train header reports live_record_count={} and live_id_bound={} with direct_record_stride=0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", probe.live_record_count, probe.live_id_bound, probe.direct_record_stride, @@ -3120,15 +3123,26 @@ pub fn load_save_slice_from_report( probe.close_tag_offset )); } - if let Some(probe) = &report.save_region_collection_directory_probe { + if let Some(probe) = &report.save_train_collection_directory_probe { notes.push(format!( - "Raw save tagged region metadata also exposes a live-entry directory with {} entries rooted at metadata dword {} (head={:?}, tail={:?}).", + "Raw save tagged train metadata also exposes a live-entry directory with {} entries rooted at metadata dword {} (head={:?}, tail={:?}).", probe.entries.len(), probe.directory_root_dword_index, probe.chain_head_live_entry_id, probe.chain_tail_live_entry_id )); } + if let Some(probe) = &report.save_region_collection_header_probe { + notes.push(format!( + "Raw save tagged region header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", + probe.live_record_count, + probe.live_id_bound, + probe.direct_record_stride, + probe.metadata_tag_offset, + probe.records_tag_offset, + probe.close_tag_offset + )); + } 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}.", @@ -3200,7 +3214,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( let world_issue_37 = report.save_world_issue_37_probe.clone(); let world_economic_tuning = report.save_world_economic_tuning_probe.clone(); let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone(); - let region_collection_directory = report.save_region_collection_directory_probe.clone(); + let train_collection_directory = report.save_train_collection_directory_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 @@ -3490,21 +3504,27 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( .to_string(), ); } - if let Some(header) = report.save_region_collection_header_probe.as_ref() { + if let Some(header) = report.save_train_collection_header_probe.as_ref() { notes.push(format!( - "Region analysis now also exports the tagged region collection header: live_record_count={} live_id_bound={} direct_record_stride=0x{:x}.", + "Train analysis now also exports the tagged train collection header: live_record_count={} live_id_bound={} direct_record_stride=0x{:x}.", header.live_record_count, header.live_id_bound, header.direct_record_stride )); } - if let Some(directory) = region_collection_directory.as_ref() { + if let Some(directory) = train_collection_directory.as_ref() { notes.push(format!( - "Region analysis now also exports the tagged live-entry directory rooted at metadata dword {}: {} entries chained from {:?} to {:?}.", + "Train analysis now also exports the tagged live-entry directory rooted at metadata dword {}: {} entries chained from {:?} to {:?}.", directory.directory_root_dword_index, directory.entries.len(), directory.chain_head_live_entry_id, directory.chain_tail_live_entry_id )); } + if let Some(header) = report.save_region_collection_header_probe.as_ref() { + notes.push(format!( + "Region analysis now also exports the non-direct tagged region collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.", + header.live_record_count, header.live_id_bound, header.direct_record_stride + )); + } if let Some(header) = report .save_placed_structure_collection_header_probe .as_ref() @@ -3555,8 +3575,9 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( world_issue_37, world_economic_tuning, world_finance_neighborhood, + train_collection_header: report.save_train_collection_header_probe.clone(), + train_collection_directory, region_collection_header: report.save_region_collection_header_probe.clone(), - region_collection_directory, placed_structure_collection_header: report .save_placed_structure_collection_header_probe .clone(), @@ -7558,14 +7579,19 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option) -> Sm file_extension_hint.as_deref(), container_profile.as_ref(), ); - let save_region_collection_header_probe = parse_save_region_collection_header_probe( + let save_train_collection_header_probe = parse_save_train_collection_header_probe( bytes, file_extension_hint.as_deref(), container_profile.as_ref(), ); - let save_region_collection_directory_probe = parse_save_region_collection_directory_probe( + let save_train_collection_directory_probe = parse_save_train_collection_directory_probe( bytes, - save_region_collection_header_probe.as_ref(), + save_train_collection_header_probe.as_ref(), + ); + let save_region_collection_header_probe = parse_save_region_collection_header_probe( + bytes, + file_extension_hint.as_deref(), + container_profile.as_ref(), ); let save_placed_structure_collection_header_probe = parse_save_placed_structure_collection_header_probe( @@ -7736,8 +7762,9 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option) -> Sm save_world_finance_neighborhood_probe, save_company_collection_header_probe, save_chairman_profile_collection_header_probe, + save_train_collection_header_probe, + save_train_collection_directory_probe, save_region_collection_header_probe, - save_region_collection_directory_probe, save_placed_structure_collection_header_probe, save_company_roster_probe, save_chairman_profile_table_probe, @@ -9677,7 +9704,7 @@ fn parse_save_chairman_profile_collection_header_probe( ) } -fn parse_save_region_collection_header_probe( +fn parse_save_train_collection_header_probe( bytes: &[u8], file_extension_hint: Option<&str>, container_profile: Option<&SmpContainerProfile>, @@ -9689,8 +9716,8 @@ fn parse_save_region_collection_header_probe( 0x00005209, 0x0000520a, 0x0000520b, - "save-region-tagged-header-counts", - "scenario-save-region-header-counts", + "save-train-tagged-header-counts", + "scenario-save-train-header-counts", |header| { header.direct_collection_flag == 1 && header.direct_record_stride >= 0x100 @@ -9701,18 +9728,18 @@ fn parse_save_region_collection_header_probe( && header.live_record_count <= header.live_id_bound }, vec![ - "save-side live region collection shares tagged header family 0x5209/0x520a/0x520b with other indexed direct-record bundles".to_string(), - "the grounded region-side candidate is the smaller direct-record family with stride 0x1d5 and live_id_bound/count in the city-region range, not the larger chairman/profile family".to_string(), + "save-side live train collection shares tagged header family 0x5209/0x520a/0x520b with other indexed direct-record bundles".to_string(), + "the grounded train-side candidate is the smaller direct-record family with stride 0x1d5 whose metadata payload carries Train N labels, distinct from the larger chairman/profile family and the non-direct region family".to_string(), ], ) } -fn parse_save_region_collection_directory_probe( +fn parse_save_train_collection_directory_probe( bytes: &[u8], header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, -) -> Option { +) -> Option { let header_probe = header_probe?; - if header_probe.source_kind != "save-region-tagged-header-counts" { + if header_probe.source_kind != "save-train-tagged-header-counts" { return None; } let metadata_payload = @@ -9733,7 +9760,7 @@ fn parse_save_region_collection_directory_probe( let payload_relative_offset = read_u32_at(directory_bytes, entry_offset)?; let previous_live_entry_id = read_u32_at(directory_bytes, entry_offset + 4)?; let next_live_entry_id = read_u32_at(directory_bytes, entry_offset + 8)?; - entries.push(SmpSaveRegionCollectionDirectoryEntryProbe { + entries.push(SmpSaveTrainCollectionDirectoryEntryProbe { live_entry_id: (index + 1) as u32, payload_relative_offset, payload_relative_offset_hex: format!("0x{payload_relative_offset:08x}"), @@ -9758,10 +9785,10 @@ fn parse_save_region_collection_directory_probe( let monotonic_offsets = entries .windows(2) .all(|window| window[0].payload_relative_offset < window[1].payload_relative_offset); - Some(SmpSaveRegionCollectionDirectoryProbe { + Some(SmpSaveTrainCollectionDirectoryProbe { profile_family: header_probe.profile_family.clone(), - source_kind: "save-region-live-directory".to_string(), - semantic_family: "scenario-save-region-live-directory".to_string(), + source_kind: "save-train-live-directory".to_string(), + semantic_family: "scenario-save-train-live-directory".to_string(), metadata_tag_offset: header_probe.metadata_tag_offset, records_tag_offset: header_probe.records_tag_offset, close_tag_offset: header_probe.close_tag_offset, @@ -9773,9 +9800,9 @@ fn parse_save_region_collection_directory_probe( chain_tail_live_entry_id, entries, evidence: vec![ - "save-side region metadata payload exposes a live-entry directory immediately after the first 16 dwords, before the records tag".to_string(), + "save-side train metadata payload exposes a live-entry directory immediately after the first 16 dwords, with payload-relative offsets pointing into the later records span".to_string(), format!( - "region live directory decodes {} triplets of (payload_relative_offset, prev_live_entry_id, next_live_entry_id) from metadata dword index {}", + "train live directory decodes {} triplets of (payload_relative_offset, prev_live_entry_id, next_live_entry_id) from metadata dword index {}", header_probe.live_record_count, SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX ), @@ -9787,6 +9814,42 @@ fn parse_save_region_collection_directory_probe( }) } +fn parse_save_region_collection_header_probe( + bytes: &[u8], + file_extension_hint: Option<&str>, + container_profile: Option<&SmpContainerProfile>, +) -> Option { + let probe = parse_save_tagged_collection_header_probe( + bytes, + file_extension_hint, + container_profile, + 0x00005209, + 0x0000520a, + 0x0000520b, + "save-region-tagged-header-counts", + "scenario-save-region-header-counts", + |header| { + header.direct_collection_flag == 0 + && header.direct_record_stride == 0x06 + && header.live_id_bound >= 0x80 + && header.live_id_bound <= 0x200 + && header.live_record_count >= 0x80 + && header.live_record_count <= header.live_id_bound + }, + vec![ + "save-side live region collection shares tagged header family 0x5209/0x520a/0x520b with trains and chairman profiles, but uses the larger non-direct indexed family".to_string(), + "the grounded region-side candidate is the non-direct 0x5209 family with live_id_bound/count in the 0x96/0x91 range and Marker09-style default stems in the records span, distinct from the smaller direct train family".to_string(), + ], + )?; + let records_preview = bytes + .get(probe.records_tag_offset + 4..probe.close_tag_offset) + .unwrap_or(&[]); + records_preview + .windows("Marker09".len()) + .any(|window| window == b"Marker09") + .then_some(probe) +} + fn parse_save_placed_structure_collection_header_probe( bytes: &[u8], file_extension_hint: Option<&str>, @@ -16657,10 +16720,10 @@ mod tests { ], evidence: vec![], }); - report.save_region_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { + report.save_train_collection_header_probe = Some(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(), + source_kind: "save-train-tagged-header-counts".to_string(), + semantic_family: "scenario-save-train-header-counts".to_string(), metadata_tag_offset: 0x3000, records_tag_offset: 0x3100, close_tag_offset: 0x3200, @@ -16683,44 +16746,69 @@ mod tests { ], evidence: vec![], }); - report.save_region_collection_directory_probe = - Some(SmpSaveRegionCollectionDirectoryProbe { - profile_family: "rt3-105-save-container-v1".to_string(), - source_kind: "save-region-live-directory".to_string(), - semantic_family: "scenario-save-region-live-directory".to_string(), - metadata_tag_offset: 0x3000, - records_tag_offset: 0x3100, - close_tag_offset: 0x3200, - directory_root_dword_index: 16, - directory_entry_dword_count: 3, - live_record_count: 0x14, - live_id_bound: 0x32, - chain_head_live_entry_id: Some(1), - chain_tail_live_entry_id: Some(20), - entries: vec![ - SmpSaveRegionCollectionDirectoryEntryProbe { - live_entry_id: 1, - payload_relative_offset: 0x2af8, - payload_relative_offset_hex: "0x00002af8".to_string(), - payload_absolute_offset: 0x5afc, - previous_live_entry_id: 0, - previous_live_entry_id_hex: "0x00000000".to_string(), - next_live_entry_id: 2, - next_live_entry_id_hex: "0x00000002".to_string(), - }, - SmpSaveRegionCollectionDirectoryEntryProbe { - live_entry_id: 2, - payload_relative_offset: 0x2ee0, - payload_relative_offset_hex: "0x00002ee0".to_string(), - payload_absolute_offset: 0x5ee4, - previous_live_entry_id: 1, - previous_live_entry_id_hex: "0x00000001".to_string(), - next_live_entry_id: 0, - next_live_entry_id_hex: "0x00000000".to_string(), - }, - ], - evidence: vec![], - }); + report.save_train_collection_directory_probe = Some(SmpSaveTrainCollectionDirectoryProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-train-live-directory".to_string(), + semantic_family: "scenario-save-train-live-directory".to_string(), + metadata_tag_offset: 0x3000, + records_tag_offset: 0x3100, + close_tag_offset: 0x3200, + directory_root_dword_index: 16, + directory_entry_dword_count: 3, + live_record_count: 0x14, + live_id_bound: 0x32, + chain_head_live_entry_id: Some(1), + chain_tail_live_entry_id: Some(20), + entries: vec![ + SmpSaveTrainCollectionDirectoryEntryProbe { + live_entry_id: 1, + payload_relative_offset: 0x2af8, + payload_relative_offset_hex: "0x00002af8".to_string(), + payload_absolute_offset: 0x5afc, + previous_live_entry_id: 0, + previous_live_entry_id_hex: "0x00000000".to_string(), + next_live_entry_id: 2, + next_live_entry_id_hex: "0x00000002".to_string(), + }, + SmpSaveTrainCollectionDirectoryEntryProbe { + live_entry_id: 2, + payload_relative_offset: 0x2ee0, + payload_relative_offset_hex: "0x00002ee0".to_string(), + payload_absolute_offset: 0x5ee4, + previous_live_entry_id: 1, + previous_live_entry_id_hex: "0x00000001".to_string(), + next_live_entry_id: 0, + next_live_entry_id_hex: "0x00000000".to_string(), + }, + ], + evidence: vec![], + }); + report.save_region_collection_header_probe = Some(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: 0x5000, + records_tag_offset: 0x5100, + close_tag_offset: 0x5200, + 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: 0x91, + live_record_count_hex: "0x00000091".to_string(), + header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x91], + header_hex_words: vec![ + "0x00000000".to_string(), + "0x00000006".to_string(), + "0x0000000a".to_string(), + "0x00000014".to_string(), + "0x00000096".to_string(), + "0x00000091".to_string(), + ], + evidence: vec![], + }); report.save_placed_structure_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe { profile_family: "rt3-105-save-container-v1".to_string(), @@ -16818,11 +16906,16 @@ mod tests { slice .notes .iter() - .any(|note| note.contains("tagged region header reports live_record_count=20")) + .any(|note| note.contains("tagged train header reports live_record_count=20")) ); assert!(slice.notes.iter().any(|note| { - note.contains("tagged region metadata also exposes a live-entry directory") + note.contains("tagged train metadata also exposes a live-entry directory") })); + assert!( + slice.notes.iter().any(|note| { + note.contains("tagged region header reports live_record_count=145") + }) + ); assert!(slice.notes.iter().any(|note| { note.contains("tagged placed-structure header reports live_record_count=2026") })); @@ -16905,7 +16998,7 @@ mod tests { } #[test] - fn parses_region_tagged_collection_header_probe_from_exact_u32_tags() { + fn parses_train_tagged_collection_header_probe_from_exact_u32_tags() { let mut bytes = vec![0u8; 0x400]; let metadata_tag_offset = 0x40usize; let records_tag_offset = 0x140usize; @@ -16923,6 +17016,47 @@ mod tests { bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); } + let probe = parse_save_train_collection_header_probe( + &bytes, + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-105-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + ) + .expect("train header probe should parse"); + + assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); + assert_eq!(probe.records_tag_offset, records_tag_offset); + assert_eq!(probe.close_tag_offset, close_tag_offset); + assert_eq!(probe.direct_collection_flag, 1); + assert_eq!(probe.direct_record_stride, 0x1d5); + assert_eq!(probe.live_id_bound, 0x32); + assert_eq!(probe.live_record_count, 0x14); + } + + #[test] + fn parses_region_tagged_collection_header_probe_from_marker09_family() { + let mut bytes = vec![0u8; 0x400]; + let metadata_tag_offset = 0x40usize; + let records_tag_offset = 0x180usize; + let close_tag_offset = 0x1c0usize; + 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 header_words = [ + 0u32, 0x06, 0x0a, 0x14, 0x96, 0x91, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + for (index, word) in header_words.into_iter().enumerate() { + let offset = metadata_tag_offset + 4 + index * 4; + bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); + } + let marker_offset = records_tag_offset + 4 + 0x20; + bytes[marker_offset..marker_offset + 8].copy_from_slice(b"Marker09"); + let probe = parse_save_region_collection_header_probe( &bytes, Some("gms"), @@ -16937,14 +17071,14 @@ mod tests { assert_eq!(probe.metadata_tag_offset, metadata_tag_offset); assert_eq!(probe.records_tag_offset, records_tag_offset); assert_eq!(probe.close_tag_offset, close_tag_offset); - assert_eq!(probe.direct_collection_flag, 1); - assert_eq!(probe.direct_record_stride, 0x1d5); - assert_eq!(probe.live_id_bound, 0x32); - assert_eq!(probe.live_record_count, 0x14); + assert_eq!(probe.direct_collection_flag, 0); + assert_eq!(probe.direct_record_stride, 0x06); + assert_eq!(probe.live_id_bound, 0x96); + assert_eq!(probe.live_record_count, 0x91); } #[test] - fn parses_region_collection_directory_probe_from_tagged_metadata_triplets() { + fn parses_train_collection_directory_probe_from_tagged_metadata_triplets() { let mut bytes = vec![0u8; 0x400]; let metadata_tag_offset = 0x40usize; let records_tag_offset = 0x180usize; @@ -16971,7 +17105,7 @@ mod tests { bytes[base + 8..base + 12].copy_from_slice(&next.to_le_bytes()); } - let header_probe = parse_save_region_collection_header_probe( + let header_probe = parse_save_train_collection_header_probe( &bytes, Some("gms"), Some(&SmpContainerProfile { @@ -16980,10 +17114,10 @@ mod tests { is_known_profile: true, }), ) - .expect("region header probe should parse"); + .expect("train header probe should parse"); let directory_probe = - parse_save_region_collection_directory_probe(&bytes, Some(&header_probe)) - .expect("region directory probe should parse"); + parse_save_train_collection_directory_probe(&bytes, Some(&header_probe)) + .expect("train directory probe should parse"); assert_eq!(directory_probe.directory_root_dword_index, 16); assert_eq!(directory_probe.live_record_count, 3); diff --git a/docs/README.md b/docs/README.md index cca18e7..2183b93 100644 --- a/docs/README.md +++ b/docs/README.md @@ -191,11 +191,12 @@ The highest-value next passes are now: electric-only periodic-company override rewrites the world route-preference byte to the effective company preference, ending it restores the base world byte, and runtime service state now carries both the active and last applied override -- save inspection now also exposes the tagged live region header `0x5209/0x520a/0x520b` plus its - live-entry directory rooted at metadata dword `16`, and the tagged placed-structure header - `0x36b1/0x36b2/0x36b3`, as first-class owner seams, so the remaining city-connection / - linked-transit blocker is record-body reconstruction rather than missing save-side collection - identity +- save inspection now also separates the shared `0x5209/0x520a/0x520b` save family correctly: the + smaller direct `0x1d5` collection is the live train family and now exposes a live-entry + directory rooted at metadata dword `16`, while the actual region collection is the larger + non-direct `Marker09` family; the tagged placed-structure header `0x36b1/0x36b2/0x36b3` + remains grounded alongside them, so the remaining city-connection / linked-transit blocker is + record-body reconstruction rather than missing save-side collection identity - the project rule on the remaining closure work is now explicit too: when one runtime-facing field is still ambiguous, prefer rehosting the owning source state or real reader/setter family first instead of guessing another derived leaf field from neighboring raw offsets; the checked-in diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index 0b16f37..98ad7ba 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -9,11 +9,11 @@ Working rule: ## Next -- Reconstruct the save-side region record body on top of the newly grounded tagged region - header-plus-directory seam, 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. +- 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. - 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. @@ -44,17 +44,18 @@ Working rule: - Any candidate slice that requires guessing rather than rehosting owning state or real reader/setter families stays blocked until a better owner seam is grounded. - The city-connection announcement / linked-transit roster-maintenance branch is still blocked at - the record-body level, not the collection-identity level: the runtime now has a tagged region - header-plus-directory seam and a tagged placed-structure header seam, but it does not yet - reconstruct the live region or placed-structure record bodies those service owners need. + the record-body level, not the collection-identity level: the runtime now has a corrected + non-direct tagged region seam, a tagged train header-plus-directory seam, and a tagged + placed-structure header seam, but it does not yet reconstruct the live region or + placed-structure record bodies those service owners need. ## Recently Done -- Save inspection now exposes the tagged region collection header (`0x5209/0x520a/0x520b`, - stride `0x1d5`) plus the live-entry directory rooted at metadata dword `16`, and the tagged - placed-structure collection header (`0x36b1/0x36b2/0x36b3`) as first-class owner seams, so the - blocked city-connection / linked-transit queue now has grounded save-side collection counts, - ids, and region payload offsets to build on instead of only atlas notes. +- Save inspection now splits the shared `0x5209/0x520a/0x520b` family correctly: the smaller + direct `0x1d5` collection is the live train family and now exposes a live-entry directory rooted + 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. - 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. diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index e95de95..6d014bd 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -269,11 +269,12 @@ electric-only periodic-company override rewrites `[world+0x4c74]` to the effecti preference for the active service pass, ending the override restores the base world byte, and runtime service state now carries both the active and last applied override instead of leaving the route-preference seam as a pure reader note. -Save inspection now also exposes the tagged live region header `0x5209/0x520a/0x520b` plus its -live-entry directory rooted at metadata dword `16`, and the tagged placed-structure header -`0x36b1/0x36b2/0x36b3`, as first-class owner seams, so the remaining city-connection / -linked-transit blocker is record-body reconstruction rather than missing save-side collection -identity. +Save inspection now also separates the shared `0x5209/0x520a/0x520b` save family correctly: the +smaller direct `0x1d5` collection is the live train family and now exposes a live-entry +directory rooted at metadata dword `16`, while the actual region collection is the larger +non-direct `Marker09` family. The tagged placed-structure header `0x36b1/0x36b2/0x36b3` remains +grounded alongside them, so the remaining city-connection / linked-transit blocker is record-body +reconstruction rather than missing save-side collection identity. That same seam now also carries the fixed-world building-density growth setting plus the linked chairman personality byte, which is enough to rehost the annual stock-repurchase gate on owned save/runtime state instead of another threshold-only note. The stock-capital issue branch now