From 1e0f88bd6281f9380d5711ff146fb641e532ee20 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 20:42:28 -0700 Subject: [PATCH] Carry company stat-band roots into runtime state --- README.md | 5 +-- crates/rrt-fixtures/src/schema.rs | 30 ++++++++++++++++ crates/rrt-runtime/src/import.rs | 9 +++++ crates/rrt-runtime/src/lib.rs | 21 ++++++------ crates/rrt-runtime/src/runtime.rs | 17 +++++++++ crates/rrt-runtime/src/smp.rs | 47 +++++++++++++++++++++++++ crates/rrt-runtime/src/summary.rs | 57 +++++++++++++++++++++++++++++++ docs/README.md | 5 +-- docs/runtime-rehost-plan.md | 4 ++- 9 files changed, 180 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 9a7b4ab..11e59c2 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ math. The next shared company-side slice is now rehosted too: save-native compan flow into a typed company market/cache map on runtime service state, carrying outstanding shares, saved support/share-price/cache words, chairman salary lanes, calendar words, and connection latches for each live company. That map now appears in runtime summaries and save-slice exports, -so later company stat-family / finance readers can build on owned state instead of another round -of single-field save-offset guesses. A checked-in +and it now also carries the first grounded stat-band root windows at `[company+0x0cfb]`, +`[company+0x0d7f]`, and `[company+0x1c47]`, so later company stat-family / finance readers can +build on owned state instead of another round of single-field save-offset guesses. A checked-in The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we should prefer rehosting the owning source state or the real reader/setter family rather than guessing one more derived leaf field from nearby offsets. A checked-in diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index 2ae0daa..3d190ce 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -96,6 +96,12 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub selected_company_mutable_support_scalar_value_f32_text: Option, #[serde(default)] + pub selected_company_stat_band_root_0cfb_count: Option, + #[serde(default)] + pub selected_company_stat_band_root_0d7f_count: Option, + #[serde(default)] + pub selected_company_stat_band_root_1c47_count: Option, + #[serde(default)] pub player_count: Option, #[serde(default)] pub chairman_profile_count: Option, @@ -609,6 +615,30 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(count) = self.selected_company_stat_band_root_0cfb_count { + if actual.selected_company_stat_band_root_0cfb_count != count { + mismatches.push(format!( + "selected_company_stat_band_root_0cfb_count mismatch: expected {count}, got {}", + actual.selected_company_stat_band_root_0cfb_count + )); + } + } + if let Some(count) = self.selected_company_stat_band_root_0d7f_count { + if actual.selected_company_stat_band_root_0d7f_count != count { + mismatches.push(format!( + "selected_company_stat_band_root_0d7f_count mismatch: expected {count}, got {}", + actual.selected_company_stat_band_root_0d7f_count + )); + } + } + if let Some(count) = self.selected_company_stat_band_root_1c47_count { + if actual.selected_company_stat_band_root_1c47_count != count { + mismatches.push(format!( + "selected_company_stat_band_root_1c47_count mismatch: expected {count}, got {}", + actual.selected_company_stat_band_root_1c47_count + )); + } + } if let Some(count) = self.player_count { if actual.player_count != count { mismatches.push(format!( diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 13be5ae..8cf50d9 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -5057,6 +5057,9 @@ mod tests { prior_issue_calendar_word: 4, city_connection_latch: true, linked_transit_latch: false, + stat_band_root_0cfb_candidates: Vec::new(), + stat_band_root_0d7f_candidates: Vec::new(), + stat_band_root_1c47_candidates: Vec::new(), }), }, crate::SmpLoadedCompanyRosterEntry { @@ -5100,6 +5103,9 @@ mod tests { prior_issue_calendar_word: 2, city_connection_latch: false, linked_transit_latch: true, + stat_band_root_0cfb_candidates: Vec::new(), + stat_band_root_0d7f_candidates: Vec::new(), + stat_band_root_1c47_candidates: Vec::new(), }), }, ], @@ -6386,6 +6392,9 @@ mod tests { prior_issue_calendar_word: 3, city_connection_latch: false, linked_transit_latch: true, + stat_band_root_0cfb_candidates: Vec::new(), + stat_band_root_0d7f_candidates: Vec::new(), + stat_band_root_1c47_candidates: Vec::new(), }, )]), ..RuntimeServiceState::default() diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 88a1dea..9d9ecf3 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -48,16 +48,17 @@ pub use runtime::{ RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric, - RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, - RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, - RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, - RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, - RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, - RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, - RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState, - RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric, - RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain, - RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldRestoreState, + RuntimeCompanyStatBandCandidate, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, + RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator, + RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, + RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, + RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, + RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, + RuntimePackedEventTextBandSummary, RuntimePlayer, RuntimePlayerConditionTestScope, + RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, + RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, + RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate, + RuntimeWorldRestoreState, }; pub use smp::{ SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 1649673..60f74d0 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -84,6 +84,12 @@ pub struct RuntimeCompanyMarketState { pub city_connection_latch: bool, #[serde(default)] pub linked_transit_latch: bool, + #[serde(default)] + pub stat_band_root_0cfb_candidates: Vec, + #[serde(default)] + pub stat_band_root_0d7f_candidates: Vec, + #[serde(default)] + pub stat_band_root_1c47_candidates: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] @@ -871,6 +877,17 @@ pub struct RuntimeWorldFinanceNeighborhoodCandidate { pub value_f32_text: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyStatBandCandidate { + 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_text: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct RuntimeWorldRestoreState { #[serde(default)] diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 9996c1d..057241a 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -3754,6 +3754,27 @@ fn parse_save_company_roster_probe( bytes, record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET, )?; + let stat_band_root_0cfb_candidates = + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_CANDIDATE_FIELDS + .iter() + .map(|(label, relative_offset)| { + build_save_dword_candidate(bytes, record_offset, label, *relative_offset) + }) + .collect::>>()?; + let stat_band_root_0d7f_candidates = + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_CANDIDATE_FIELDS + .iter() + .map(|(label, relative_offset)| { + build_save_dword_candidate(bytes, record_offset, label, *relative_offset) + }) + .collect::>>()?; + let stat_band_root_1c47_candidates = + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_CANDIDATE_FIELDS + .iter() + .map(|(label, relative_offset)| { + build_save_dword_candidate(bytes, record_offset, label, *relative_offset) + }) + .collect::>>()?; entries.push(SmpLoadedCompanyRosterEntry { company_id, active, @@ -3788,6 +3809,18 @@ fn parse_save_company_roster_probe( prior_issue_calendar_word, city_connection_latch, linked_transit_latch, + stat_band_root_0cfb_candidates: stat_band_root_0cfb_candidates + .iter() + .map(runtime_company_stat_band_candidate_from_save) + .collect(), + stat_band_root_0d7f_candidates: stat_band_root_0d7f_candidates + .iter() + .map(runtime_company_stat_band_candidate_from_save) + .collect(), + stat_band_root_1c47_candidates: stat_band_root_1c47_candidates + .iter() + .map(runtime_company_stat_band_candidate_from_save) + .collect(), }), }); } @@ -3801,6 +3834,20 @@ fn parse_save_company_roster_probe( }) } +fn runtime_company_stat_band_candidate_from_save( + candidate: &SmpSaveDwordCandidate, +) -> crate::RuntimeCompanyStatBandCandidate { + crate::RuntimeCompanyStatBandCandidate { + label: candidate.label.clone(), + relative_offset: candidate.relative_offset, + relative_offset_hex: candidate.relative_offset_hex.clone(), + raw_u32: candidate.raw_u32, + raw_u32_hex: candidate.raw_u32_hex.clone(), + value_i32: candidate.value_i32, + value_f32_text: format!("{:.6}", candidate.value_f32), + } +} + fn detect_save_company_record_start_offset( bytes: &[u8], header_probe: &SmpSaveTaggedCollectionHeaderProbe, diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 6b7daec..fddc18a 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -49,6 +49,9 @@ pub struct RuntimeSummary { pub selected_company_outstanding_shares: Option, pub selected_company_cached_share_price_value_f32_text: Option, pub selected_company_mutable_support_scalar_value_f32_text: Option, + pub selected_company_stat_band_root_0cfb_count: usize, + pub selected_company_stat_band_root_0d7f_count: usize, + pub selected_company_stat_band_root_1c47_count: usize, pub selected_company_last_dividend_year: Option, pub selected_company_chairman_bonus_year: Option, pub selected_company_chairman_bonus_amount: Option, @@ -249,6 +252,15 @@ impl RuntimeSummary { .map(|market_state| { raw_u32_to_f32_text(market_state.mutable_support_scalar_raw_u32) }), + selected_company_stat_band_root_0cfb_count: selected_company_market_state + .map(|market_state| market_state.stat_band_root_0cfb_candidates.len()) + .unwrap_or(0), + selected_company_stat_band_root_0d7f_count: selected_company_market_state + .map(|market_state| market_state.stat_band_root_0d7f_candidates.len()) + .unwrap_or(0), + selected_company_stat_band_root_1c47_count: selected_company_market_state + .map(|market_state| market_state.stat_band_root_1c47_candidates.len()) + .unwrap_or(0), selected_company_last_dividend_year: selected_company_market_state .map(|market_state| market_state.last_dividend_year) .filter(|year| *year != 0), @@ -1890,6 +1902,48 @@ mod tests { prior_issue_calendar_word: 4, city_connection_latch: true, linked_transit_latch: false, + stat_band_root_0cfb_candidates: vec![ + crate::RuntimeCompanyStatBandCandidate { + label: "stat_band_0cfb_word_1".to_string(), + relative_offset: 0x0cfb, + relative_offset_hex: "0xcfb".to_string(), + raw_u32: 1, + raw_u32_hex: "0x00000001".to_string(), + value_i32: 1, + value_f32_text: "0.000000".to_string(), + }, + crate::RuntimeCompanyStatBandCandidate { + label: "stat_band_0cfb_word_2".to_string(), + relative_offset: 0x0cff, + relative_offset_hex: "0xcff".to_string(), + raw_u32: 2, + raw_u32_hex: "0x00000002".to_string(), + value_i32: 2, + value_f32_text: "0.000000".to_string(), + }, + ], + stat_band_root_0d7f_candidates: vec![ + crate::RuntimeCompanyStatBandCandidate { + label: "stat_band_0d7f_word_1".to_string(), + relative_offset: 0x0d7f, + relative_offset_hex: "0xd7f".to_string(), + raw_u32: 3, + raw_u32_hex: "0x00000003".to_string(), + value_i32: 3, + value_f32_text: "0.000000".to_string(), + }, + ], + stat_band_root_1c47_candidates: vec![ + crate::RuntimeCompanyStatBandCandidate { + label: "stat_band_1c47_word_1".to_string(), + relative_offset: 0x1c47, + relative_offset_hex: "0x1c47".trim_start_matches("0x").to_string(), + raw_u32: 4, + raw_u32_hex: "0x00000004".to_string(), + value_i32: 4, + value_f32_text: "0.000000".to_string(), + }, + ], }, )]), ..RuntimeServiceState::default() @@ -1907,6 +1961,9 @@ mod tests { summary.selected_company_mutable_support_scalar_value_f32_text, Some("1.000000".to_string()) ); + assert_eq!(summary.selected_company_stat_band_root_0cfb_count, 2); + assert_eq!(summary.selected_company_stat_band_root_0d7f_count, 1); + assert_eq!(summary.selected_company_stat_band_root_1c47_count, 1); assert_eq!(summary.selected_company_last_dividend_year, Some(1841)); assert_eq!(summary.selected_company_chairman_bonus_year, Some(1842)); assert_eq!(summary.selected_company_chairman_bonus_amount, Some(750)); diff --git a/docs/README.md b/docs/README.md index 1ca4c6d..a57bb26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -112,8 +112,9 @@ The highest-value next passes are now: tuning band, derived holdings-at-share-price and cached purchasing-power totals, context, company dword candidate windows, and richer chairman qword cache views; the current rehosted company-side owner state now also includes a typed market/cache map carrying saved - outstanding-shares, support/share-price/cache words, salary lanes, calendar words, and - connection latches for each live company, so later finance/stat-family readers can attach to + outstanding-shares, support/share-price/cache words, salary lanes, calendar words, connection + latches, and the first grounded stat-band root windows at `[company+0x0cfb]`, `[company+0x0d7f]`, + and `[company+0x1c47]` for each live company, so later finance/stat-family readers can attach to owned runtime data instead of one more guessed save offset - 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 diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 323f286..798888d 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -197,7 +197,9 @@ later tranche once stronger evidence exists, but the current project rule is exp rehosting shared owner state and reader/setter families first, and only guess at one more leaf field when that richer owning-state path is blocked. Richer runtime ownership should still be added where later descriptor, stat-family, or simulation work needs more than the current event-owned -roster. +roster. The current owned company-side roster now includes not just the market/cache lanes but also +the first grounded stat-band root windows at `[company+0x0cfb]`, `[company+0x0d7f]`, and +`[company+0x1c47]`, so later finance readers can target saved owner state directly. ## Why This Boundary