From 1525703cd16daf5d6a2e84fcfdbffefed54e5412 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 15:18:13 -0700 Subject: [PATCH] Expand raw save company-chairman analysis surfaces --- README.md | 3 +- crates/rrt-runtime/src/lib.rs | 7 +- crates/rrt-runtime/src/smp.rs | 214 ++++++++++++++++++++++++++++++++-- docs/README.md | 3 +- docs/runtime-rehost-plan.md | 6 +- 5 files changed, 219 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a5a4431..b5dc373 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ company debt, and company track-laying capacity are grounded directly from save broader company finance/governance scalars and controller-kind reconstruction still remain conservative defaults until their raw lanes are pinned more strongly. The offline runtime analysis surface also now exposes `runtime inspect-save-company-chairman ` for those remaining raw -company/chairman scalar candidates. A checked-in +company/chairman scalar candidates, including fixed-world chairman slot / role-gate context, +explicit company dword candidate windows, and richer chairman qword cache views. A checked-in `EventEffects` export now exists too in `artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now exists beside it in `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`. Recovered diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index e7b3366..ce13701 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -83,9 +83,10 @@ pub use smp::{ SmpRuntimeAnchorCycleBlock, SmpRuntimePostSpanHeaderCandidate, SmpRuntimePostSpanProbe, SmpRuntimeTrailerBlock, SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock, SmpSaveChairmanRecordAnalysisEntry, SmpSaveCompanyChairmanAnalysisReport, - SmpSaveCompanyRecordAnalysisEntry, SmpSaveLoadCandidateTableSummary, SmpSaveLoadSummary, - SmpSaveScalarCandidate, SmpSaveTaggedCollectionHeaderProbe, SmpSecondaryVariantProbe, - SmpSharedHeader, SmpSpecialConditionEntry, SmpSpecialConditionsProbe, + SmpSaveCompanyRecordAnalysisEntry, SmpSaveDwordCandidate, SmpSaveLoadCandidateTableSummary, + SmpSaveLoadSummary, SmpSaveScalarCandidate, SmpSaveTaggedCollectionHeaderProbe, + SmpSaveWorldSelectionRoleAnalysis, SmpSaveWorldSelectionRoleAnalysisEntry, + SmpSecondaryVariantProbe, SmpSharedHeader, SmpSpecialConditionEntry, SmpSpecialConditionsProbe, inspect_save_company_and_chairman_analysis_bytes, inspect_save_company_and_chairman_analysis_file, inspect_smp_bytes, inspect_smp_file, load_save_slice_file, load_save_slice_from_report, diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 45ae186..7bfac6b 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -2185,9 +2185,41 @@ pub struct SmpLoadedChairmanProfileTable { pub struct SmpSaveScalarCandidate { pub relative_offset: usize, pub relative_offset_hex: String, + pub raw_u64: u64, + pub raw_u64_hex: String, + pub value_i64: i64, pub value_f64: f64, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmpSaveDwordCandidate { + pub label: String, + pub relative_offset: usize, + pub relative_offset_hex: String, + pub raw_u32: u32, + pub raw_u32_hex: String, + pub value_i32: i32, + pub value_f32: f32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveWorldSelectionRoleAnalysisEntry { + pub slot_index: usize, + pub selector_byte: u8, + pub selector_byte_hex: String, + pub role_gate_byte: u8, + pub role_gate_byte_hex: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpSaveWorldSelectionRoleAnalysis { + pub selected_company_id: u32, + pub selected_chairman_profile_id: u32, + pub campaign_override_flag: u8, + pub campaign_override_flag_hex: String, + pub chairman_slots: Vec, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SmpSaveCompanyRecordAnalysisEntry { pub company_id: u32, @@ -2211,6 +2243,10 @@ pub struct SmpSaveCompanyRecordAnalysisEntry { pub linked_transit_latch: bool, pub merger_cooldown_year: u32, pub takeover_cooldown_year: u32, + #[serde(default)] + pub scalar_dword_candidates: Vec, + #[serde(default)] + pub post_capacity_dword_candidates: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -2235,6 +2271,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport { #[serde(default)] pub selected_chairman_profile_id: Option, #[serde(default)] + pub world_selection_context: Option, + #[serde(default)] pub company_entries: Vec, #[serde(default)] pub chairman_entries: Vec, @@ -2758,6 +2796,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( report: &SmpInspectionReport, ) -> Option { let selection_probe = report.save_world_selection_context_probe.as_ref(); + let world_selection_context = selection_probe.map(build_save_world_selection_role_analysis); let company_header_probe = report.save_company_collection_header_probe.as_ref(); let chairman_header_probe = report .save_chairman_profile_collection_header_probe @@ -2840,6 +2879,18 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( &bytes, record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET, )?; + let scalar_dword_candidates = SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS + .iter() + .map(|(label, relative_offset)| { + build_save_dword_candidate(&bytes, record_offset, label, *relative_offset) + }) + .collect::>>()?; + let post_capacity_dword_candidates = SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS + .iter() + .map(|(label, relative_offset)| { + build_save_dword_candidate(&bytes, record_offset, label, *relative_offset) + }) + .collect::>>()?; entries.push(SmpSaveCompanyRecordAnalysisEntry { company_id, name, @@ -2860,6 +2911,8 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( linked_transit_latch, merger_cooldown_year, takeover_cooldown_year, + scalar_dword_candidates, + post_capacity_dword_candidates, }); } entries @@ -2907,12 +2960,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS .iter() .map(|relative_offset| { - let value_f64 = read_f64_at(&bytes, record_offset + relative_offset)?; - Some(SmpSaveScalarCandidate { - relative_offset: *relative_offset, - relative_offset_hex: format!("0x{relative_offset:x}"), - value_f64, - }) + build_save_qword_candidate(&bytes, record_offset, *relative_offset) }) .collect::>>()?; entries.push(SmpSaveChairmanRecordAnalysisEntry { @@ -2931,6 +2979,12 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( }; let mut notes = Vec::new(); + if world_selection_context.is_some() { + notes.push( + "World selection context now exports the grounded chairman-slot selector bytes and per-slot role-gate bytes from the fixed save-side 0x32c8 world block." + .to_string(), + ); + } if !company_entries.is_empty() { notes.push( "Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string(), @@ -2938,10 +2992,13 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( notes.push( "Company available_track_laying_capacity is derived from the grounded tail dword [company+0x7680], with negative values treated as the unlimited sentinel.".to_string(), ); + notes.push( + "Company scalar_dword_candidates expose the current checked-in raw save windows around support/share-price/calendar lanes, and post_capacity_dword_candidates expose the immediate dwords after [company+0x7680] for deeper track-count and record-tail analysis.".to_string(), + ); } if !chairman_entries.is_empty() { notes.push( - "Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9] for further purchasing-power analysis.".to_string(), + "Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9], now including raw qword hex and signed/f64 views for further purchasing-power analysis.".to_string(), ); } @@ -2954,6 +3011,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( selected_company_id: selection_probe.map(|probe| probe.selected_company_id), selected_chairman_profile_id: selection_probe .map(|probe| probe.selected_chairman_profile_id), + world_selection_context, company_entries, chairman_entries, notes, @@ -3089,6 +3147,23 @@ const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET: usize = 0x0d59; const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56; const SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET: usize = 0x0d7b; const SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET: usize = 0x7680; +const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 7] = [ + ("mutable_support_scalar", 0x4f), + ("young_company_support_scalar", 0x57), + ("support_progress_word", 0x0d07), + ("recent_per_share_subscore", 0x0d19), + ("cached_share_price", 0x0d7b), + ("current_issue_calendar_word", 0x16b), + ("prior_issue_calendar_word", 0x173), +]; +const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [(&str, usize); 6] = [ + ("post_capacity_word_1", 0x7684), + ("post_capacity_word_2", 0x7688), + ("post_capacity_word_3", 0x768c), + ("post_capacity_word_4", 0x7690), + ("post_capacity_word_5", 0x7694), + ("post_capacity_word_6", 0x7698), +]; const SAVE_COMPANY_RECORD_START_SCAN_LIMIT: usize = 0x120; const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08; @@ -3296,6 +3371,69 @@ fn parse_save_company_available_track_laying_capacity( } } +fn build_save_dword_candidate( + bytes: &[u8], + record_offset: usize, + label: &str, + relative_offset: usize, +) -> Option { + let raw_u32 = read_u32_at(bytes, record_offset + relative_offset)?; + Some(SmpSaveDwordCandidate { + label: label.to_string(), + relative_offset, + relative_offset_hex: format!("0x{relative_offset:x}"), + raw_u32, + raw_u32_hex: format!("0x{raw_u32:08x}"), + value_i32: raw_u32 as i32, + value_f32: f32::from_bits(raw_u32), + }) +} + +fn build_save_qword_candidate( + bytes: &[u8], + record_offset: usize, + relative_offset: usize, +) -> Option { + let raw_u64 = read_u64_at(bytes, record_offset + relative_offset)?; + Some(SmpSaveScalarCandidate { + relative_offset, + relative_offset_hex: format!("0x{relative_offset:x}"), + raw_u64, + raw_u64_hex: format!("0x{raw_u64:016x}"), + value_i64: raw_u64 as i64, + value_f64: f64::from_bits(raw_u64), + }) +} + +fn build_save_world_selection_role_analysis( + probe: &SmpSaveWorldSelectionContextProbe, +) -> SmpSaveWorldSelectionRoleAnalysis { + let chairman_slots = probe + .chairman_slot_selectors + .iter() + .copied() + .zip(probe.chairman_role_gate_bytes.iter().copied()) + .enumerate() + .map(|(slot_index, (selector_byte, role_gate_byte))| { + SmpSaveWorldSelectionRoleAnalysisEntry { + slot_index, + selector_byte, + selector_byte_hex: format!("0x{selector_byte:02x}"), + role_gate_byte, + role_gate_byte_hex: format!("0x{role_gate_byte:02x}"), + } + }) + .collect(); + + SmpSaveWorldSelectionRoleAnalysis { + selected_company_id: probe.selected_company_id, + selected_chairman_profile_id: probe.selected_chairman_profile_id, + campaign_override_flag: probe.campaign_override_flag, + campaign_override_flag_hex: probe.campaign_override_flag_hex.clone(), + chairman_slots, + } +} + fn parse_save_chairman_profile_table_probe( bytes: &[u8], header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, @@ -9986,6 +10124,13 @@ fn read_i64_at(bytes: &[u8], offset: usize) -> Option { ])) } +fn read_u64_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 8)?; + Some(u64::from_le_bytes([ + chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7], + ])) +} + fn read_f32_at(bytes: &[u8], offset: usize) -> Option { let chunk = bytes.get(offset..offset + 4)?; Some(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) @@ -14949,6 +15094,61 @@ mod tests { assert_eq!(table.entries[1].holdings_value_total, 822000); } + #[test] + fn builds_save_world_selection_role_analysis_from_probe() { + let probe = SmpSaveWorldSelectionContextProbe { + profile_family: "rt3-105-save-container-v1".to_string(), + source_kind: "save-direct-world-block".to_string(), + semantic_family: "scenario-selected-company-and-chairman-context".to_string(), + chunk_tag_offset: 0, + payload_offset: 0, + payload_len: 0x4f2c, + payload_len_hex: "0x4f2c".to_string(), + selected_company_id_offset: 0x21, + selected_company_id: 3, + selected_company_id_hex: "0x00000003".to_string(), + selected_chairman_profile_id_offset: 0x25, + selected_chairman_profile_id: 7, + selected_chairman_profile_id_hex: "0x00000007".to_string(), + chairman_slot_selector_offset: 0x87, + chairman_slot_selectors: vec![1, 0, 2, 0], + campaign_override_flag_offset: 0xc5, + campaign_override_flag: 1, + campaign_override_flag_hex: "0x01".to_string(), + chairman_role_gate_offset: 0x0bc3, + chairman_role_gate_bytes: vec![2, 0, 1, 0], + evidence: vec![], + }; + + let analysis = build_save_world_selection_role_analysis(&probe); + + assert_eq!(analysis.selected_company_id, 3); + assert_eq!(analysis.selected_chairman_profile_id, 7); + assert_eq!(analysis.campaign_override_flag_hex, "0x01"); + assert_eq!(analysis.chairman_slots.len(), 4); + assert_eq!(analysis.chairman_slots[0].selector_byte_hex, "0x01"); + assert_eq!(analysis.chairman_slots[2].role_gate_byte_hex, "0x01"); + } + + #[test] + fn builds_save_candidate_views_with_raw_bits() { + let mut bytes = vec![0u8; 0x40]; + bytes[0x08..0x0c].copy_from_slice(&0x3f800000u32.to_le_bytes()); + bytes[0x10..0x18].copy_from_slice(&(-2458.0f64).to_le_bytes()); + + let dword = build_save_dword_candidate(&bytes, 0, "unit_float", 0x08) + .expect("dword candidate should build"); + let qword = + build_save_qword_candidate(&bytes, 0, 0x10).expect("qword candidate should build"); + + assert_eq!(dword.raw_u32_hex, "0x3f800000"); + assert_eq!(dword.value_i32, 1_065_353_216); + assert_eq!(dword.value_f32, 1.0); + assert_eq!(qword.raw_u64, (-2458.0f64).to_bits()); + assert_eq!(qword.value_i64, (-2458.0f64).to_bits() as i64); + assert_eq!(qword.value_f64, -2458.0); + } + #[test] fn classifies_rt3_105_post_span_bridge_variants() { let base_trailer = SmpRuntimeTrailerBlock { diff --git a/docs/README.md b/docs/README.md index 5c9cc89..87f2ac6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -105,7 +105,8 @@ The highest-value next passes are now: finance/governance scalar lanes plus controller-kind reconstruction still remain conservative defaults until their raw offsets are pinned more strongly; the offline analysis command `runtime inspect-save-company-chairman ` now dumps those remaining raw record - candidates directly from the rehosted parser + candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate + context, company dword candidate windows, and richer chairman qword cache views - a checked-in `EventEffects` export now exists at `artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json` diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index ce44067..b202d1e 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -65,8 +65,10 @@ Implemented today: collections now provide save-native roster entries and `observed_entry_count`; raw company debt from the bond table and raw company track-laying capacity from the record tail are grounded too, and `runtime inspect-save-company-chairman ` now exposes the remaining raw - company/chairman scalar candidates directly from the rehosted parser; the remaining raw-save - boundary is company-finance/governance scalar depth plus controller-kind closure, not roster absence + company/chairman scalar candidates directly from the rehosted parser, including fixed-world + chairman slot / role-gate context, company dword candidate windows, and richer chairman qword + cache views; the remaining raw-save boundary is company-finance/governance scalar depth plus + controller-kind closure, not roster absence - a checked-in `EventEffects` export now exists too at `artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`