From 9ff4cac6fbcc3148bc36550cc1034555d4663621 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 20:46:23 -0700 Subject: [PATCH] Rehost first company stat-family reader seam --- README.md | 5 +- crates/rrt-runtime/src/lib.rs | 24 +++--- crates/rrt-runtime/src/runtime.rs | 136 ++++++++++++++++++++++++++++++ docs/README.md | 3 +- docs/runtime-rehost-plan.md | 4 +- 5 files changed, 158 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 11e59c2..463479a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,10 @@ saved support/share-price/cache words, chairman salary lanes, calendar words, an latches for each live company. That map now appears in runtime summaries and save-slice exports, 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 +build on owned state instead of another round of single-field save-offset guesses. The first +runtime-side `0x2329` stat-family reader seam is now rehosted too for the currently grounded slots +`0x0d` (`current_cash`) and `0x1d` (`book_value_per_share`), so later annual-finance logic can +extend one shared reader family instead of hard-coding more direct field accesses. 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-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 9d9ecf3..00fdbac 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -48,17 +48,19 @@ pub use runtime::{ RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric, - 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, + RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, 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, + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, runtime_company_stat_value, }; 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 60f74d0..0ca4387 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -330,6 +330,16 @@ pub enum RuntimeChairmanMetric { PurchasingPowerTotal, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyStatSelector { + pub family_id: u32, + pub slot_id: u32, +} + +pub const RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER: u32 = 0x2329; +pub const RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH: u32 = 0x0d; +pub const RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE: u32 = 0x1d; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RuntimeTerritoryMetric { @@ -1786,6 +1796,24 @@ impl RuntimeState { } } +pub fn runtime_company_stat_value( + state: &RuntimeState, + company_id: u32, + selector: RuntimeCompanyStatSelector, +) -> Option { + let company = state.companies.iter().find(|company| company.company_id == company_id)?; + match (selector.family_id, selector.slot_id) { + (RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH) => { + Some(company.current_cash) + } + ( + RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, + ) => Some(company.book_value_per_share), + _ => None, + } +} + fn rounded_cached_share_price_i64(raw_u32: u32) -> Option { let value = f32::from_bits(raw_u32); if !value.is_finite() { @@ -3669,4 +3697,112 @@ mod tests { assert_eq!(state.chairman_profiles[0].net_worth_total, 100); assert_eq!(state.chairman_profiles[0].purchasing_power_total, 130); } + + #[test] + fn reads_grounded_company_stat_family_slots_from_runtime_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 125_000, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 2_620, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + Some(125_000) + ); + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, + }, + ), + Some(2_620) + ); + assert_eq!( + runtime_company_stat_value( + &state, + 7, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: 0x2b, + }, + ), + None + ); + assert_eq!( + runtime_company_stat_value( + &state, + 99, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + None + ); + } } diff --git a/docs/README.md b/docs/README.md index a57bb26..2eb5675 100644 --- a/docs/README.md +++ b/docs/README.md @@ -115,7 +115,8 @@ The highest-value next passes are now: 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 + owned runtime data instead of one more guessed save offset; the first runtime-side `0x2329` + stat-family reader seam is now also rehosted for slots `0x0d` and `0x1d` - 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 diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 798888d..19e578e 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -199,7 +199,9 @@ field when that richer owning-state path is blocked. Richer runtime ownership sh where later descriptor, stat-family, or simulation work needs more than the current event-owned 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. +`[company+0x1c47]`, and the first runtime-side `0x2329` stat-family reader seam is now rehosted +for slots `0x0d` and `0x1d`, so later finance readers can target saved owner state and one shared +reader family directly. ## Why This Boundary