From 5198f80cd9bf75e2772e7165bce0a511635c9095 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 18:28:53 -0700 Subject: [PATCH] Rehost save-native company market cache state --- README.md | 7 +- crates/rrt-fixtures/src/schema.rs | 48 ++++++ crates/rrt-runtime/src/import.rs | 236 ++++++++++++++++++++++-------- crates/rrt-runtime/src/lib.rs | 8 +- crates/rrt-runtime/src/runtime.rs | 42 ++++++ crates/rrt-runtime/src/smp.rs | 234 +++++++++++++++++++++++++++-- crates/rrt-runtime/src/summary.rs | 116 +++++++++++++++ docs/README.md | 6 +- docs/runtime-rehost-plan.md | 12 +- 9 files changed, 630 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index dad9193..1ba38c7 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,12 @@ holdings-at-share-price / cached purchasing-power comparisons. The same fixed `0 block is now probed for both the grounded issue-`0x37` pair at `[world+0x29/+0x2d]` and the separate six-float economic tuning band, but current atlas evidence still keeps that editor-facing tuning family distinct from the governance issue lanes behind investor confidence and prime-rate -math. A checked-in +math. The next shared company-side slice is now rehosted too: save-native company direct records +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 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 a529311..ec870ec 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -84,6 +84,14 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub active_company_count: Option, #[serde(default)] + pub company_market_state_owner_count: Option, + #[serde(default)] + pub selected_company_outstanding_shares: Option, + #[serde(default)] + pub selected_company_cached_share_price_value_f32_text: Option, + #[serde(default)] + pub selected_company_mutable_support_scalar_value_f32_text: Option, + #[serde(default)] pub player_count: Option, #[serde(default)] pub chairman_profile_count: Option, @@ -541,6 +549,46 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(count) = self.company_market_state_owner_count { + if actual.company_market_state_owner_count != count { + mismatches.push(format!( + "company_market_state_owner_count mismatch: expected {count}, got {}", + actual.company_market_state_owner_count + )); + } + } + if let Some(value) = self.selected_company_outstanding_shares { + if actual.selected_company_outstanding_shares != Some(value) { + mismatches.push(format!( + "selected_company_outstanding_shares mismatch: expected {value}, got {:?}", + actual.selected_company_outstanding_shares + )); + } + } + if let Some(value) = &self.selected_company_cached_share_price_value_f32_text { + if actual + .selected_company_cached_share_price_value_f32_text + .as_ref() + != Some(value) + { + mismatches.push(format!( + "selected_company_cached_share_price_value_f32_text mismatch: expected {value:?}, got {:?}", + actual.selected_company_cached_share_price_value_f32_text + )); + } + } + if let Some(value) = &self.selected_company_mutable_support_scalar_value_f32_text { + if actual + .selected_company_mutable_support_scalar_value_f32_text + .as_ref() + != Some(value) + { + mismatches.push(format!( + "selected_company_mutable_support_scalar_value_f32_text mismatch: expected {value:?}, got {:?}", + actual.selected_company_mutable_support_scalar_value_f32_text + )); + } + } 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 7a19bc9..a1f2e96 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -7,9 +7,9 @@ use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapsh use crate::{ CalendarPoint, RuntimeCargoCatalogEntry, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, - RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyTarget, - RuntimeCondition, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, - RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, + RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketState, + RuntimeCompanyTarget, RuntimeCondition, RuntimeEffect, RuntimeEventRecord, + RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, @@ -99,6 +99,8 @@ struct SaveSliceProjection { has_company_projection: bool, has_company_selection_override: bool, selected_company_id: Option, + company_market_state: BTreeMap, + has_company_market_projection: bool, chairman_profiles: Vec, has_chairman_projection: bool, has_chairman_selection_override: bool, @@ -308,7 +310,10 @@ pub fn project_save_slice_to_runtime_state_import( territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: projection.world_scalar_overrides, special_conditions: projection.special_conditions, - service_state: RuntimeServiceState::default(), + service_state: RuntimeServiceState { + company_market_state: projection.company_market_state, + ..RuntimeServiceState::default() + }, }; state.validate()?; @@ -410,7 +415,14 @@ pub fn project_save_slice_overlay_to_runtime_state_import( territory_runtime_variables: base_state.territory_runtime_variables.clone(), world_scalar_overrides: base_state.world_scalar_overrides.clone(), special_conditions: projection.special_conditions, - service_state: base_state.service_state.clone(), + service_state: RuntimeServiceState { + company_market_state: if projection.has_company_market_projection { + projection.company_market_state + } else { + base_state.service_state.company_market_state.clone() + }, + ..base_state.service_state.clone() + }, }; state.validate()?; @@ -915,64 +927,88 @@ fn project_save_slice_components( None }; - let (companies, has_company_projection, has_company_selection_override, selected_company_id) = - if let Some(roster) = &save_slice.company_roster { + let ( + companies, + has_company_projection, + has_company_selection_override, + selected_company_id, + company_market_state, + has_company_market_projection, + ) = if let Some(roster) = &save_slice.company_roster { + metadata.insert( + "save_slice.company_roster_source_kind".to_string(), + roster.source_kind.clone(), + ); + metadata.insert( + "save_slice.company_roster_semantic_family".to_string(), + roster.semantic_family.clone(), + ); + metadata.insert( + "save_slice.company_roster_entry_count".to_string(), + roster.observed_entry_count.to_string(), + ); + let market_state = roster + .entries + .iter() + .filter_map(|entry| { + entry + .market_state + .as_ref() + .map(|state| (entry.company_id, state.clone())) + }) + .collect::>(); + metadata.insert( + "save_slice.company_market_state_owner_count".to_string(), + market_state.len().to_string(), + ); + if let Some(selected_company_id) = roster.selected_company_id { metadata.insert( - "save_slice.company_roster_source_kind".to_string(), - roster.source_kind.clone(), + "save_slice.selected_company_id".to_string(), + selected_company_id.to_string(), ); - metadata.insert( - "save_slice.company_roster_semantic_family".to_string(), - roster.semantic_family.clone(), - ); - metadata.insert( - "save_slice.company_roster_entry_count".to_string(), - roster.observed_entry_count.to_string(), - ); - if let Some(selected_company_id) = roster.selected_company_id { - metadata.insert( - "save_slice.selected_company_id".to_string(), - selected_company_id.to_string(), - ); - } - if roster.entries.is_empty() { - ( - Vec::new(), - false, - roster.selected_company_id.is_some(), - roster.selected_company_id, - ) - } else { - ( - roster - .entries - .iter() - .map(|entry| RuntimeCompany { - company_id: entry.company_id, - current_cash: entry.current_cash, - debt: entry.debt, - credit_rating_score: entry.credit_rating_score, - prime_rate: entry.prime_rate, - active: entry.active, - available_track_laying_capacity: entry.available_track_laying_capacity, - controller_kind: entry.controller_kind, - linked_chairman_profile_id: entry.linked_chairman_profile_id, - book_value_per_share: entry.book_value_per_share, - investor_confidence: entry.investor_confidence, - management_attitude: entry.management_attitude, - takeover_cooldown_year: entry.takeover_cooldown_year, - merger_cooldown_year: entry.merger_cooldown_year, - track_piece_counts: entry.track_piece_counts, - }) - .collect::>(), - true, - roster.selected_company_id.is_some(), - roster.selected_company_id, - ) - } + } + if roster.entries.is_empty() { + ( + Vec::new(), + false, + roster.selected_company_id.is_some(), + roster.selected_company_id, + BTreeMap::new(), + false, + ) } else { - (Vec::new(), false, false, None) - }; + ( + roster + .entries + .iter() + .map(|entry| RuntimeCompany { + company_id: entry.company_id, + current_cash: entry.current_cash, + debt: entry.debt, + credit_rating_score: entry.credit_rating_score, + prime_rate: entry.prime_rate, + active: entry.active, + available_track_laying_capacity: entry.available_track_laying_capacity, + controller_kind: entry.controller_kind, + linked_chairman_profile_id: entry.linked_chairman_profile_id, + book_value_per_share: entry.book_value_per_share, + investor_confidence: entry.investor_confidence, + management_attitude: entry.management_attitude, + takeover_cooldown_year: entry.takeover_cooldown_year, + merger_cooldown_year: entry.merger_cooldown_year, + track_piece_counts: entry.track_piece_counts, + }) + .collect::>(), + true, + roster.selected_company_id.is_some(), + roster.selected_company_id, + market_state, + true, + ) + } + } else { + (Vec::new(), false, false, None, BTreeMap::new(), false) + }; let ( chairman_profiles, @@ -1112,6 +1148,8 @@ fn project_save_slice_components( has_company_projection, has_company_selection_override, selected_company_id, + company_market_state, + has_company_market_projection, chairman_profiles, has_chairman_projection, has_chairman_selection_override, @@ -4952,6 +4990,22 @@ mod tests { management_attitude: 58, takeover_cooldown_year: Some(1839), merger_cooldown_year: Some(1838), + market_state: Some(crate::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + mutable_support_scalar_raw_u32: 0x3f99999a, + young_company_support_scalar_raw_u32: 0x42700000, + support_progress_word: 12, + recent_per_share_subscore_raw_u32: 0x420c0000, + cached_share_price_raw_u32: 0x42200000, + chairman_salary_baseline: 24, + chairman_salary_current: 30, + founding_year: 1831, + last_bankruptcy_year: 0, + current_issue_calendar_word: 5, + prior_issue_calendar_word: 4, + city_connection_latch: true, + linked_transit_latch: false, + }), }, crate::SmpLoadedCompanyRosterEntry { company_id: 2, @@ -4976,6 +5030,22 @@ mod tests { management_attitude: 31, takeover_cooldown_year: None, merger_cooldown_year: None, + market_state: Some(crate::RuntimeCompanyMarketState { + outstanding_shares: 18_000, + mutable_support_scalar_raw_u32: 0x3f4ccccd, + young_company_support_scalar_raw_u32: 0x42580000, + support_progress_word: 9, + recent_per_share_subscore_raw_u32: 0x41f00000, + cached_share_price_raw_u32: 0x41f80000, + chairman_salary_baseline: 20, + chairman_salary_current: 22, + founding_year: 1833, + last_bankruptcy_year: 0, + current_issue_calendar_word: 3, + prior_issue_calendar_word: 2, + city_connection_latch: false, + linked_transit_latch: true, + }), }, ], } @@ -6073,6 +6143,16 @@ mod tests { assert_eq!(import.state.selected_chairman_profile_id, Some(1)); assert_eq!(import.state.companies[0].book_value_per_share, 2620); assert_eq!(import.state.chairman_profiles[0].current_cash, 500); + assert_eq!(import.state.service_state.company_market_state.len(), 2); + assert_eq!( + import + .state + .service_state + .company_market_state + .get(&1) + .map(|state| state.cached_share_price_raw_u32), + Some(0x42200000) + ); } #[test] @@ -6149,6 +6229,15 @@ mod tests { assert_eq!(import.state.chairman_profiles.len(), 2); assert_eq!(import.state.selected_chairman_profile_id, Some(1)); assert_eq!(import.state.territories, base_state.territories); + assert_eq!( + import + .state + .service_state + .company_market_state + .get(&2) + .map(|state| state.linked_transit_latch), + Some(true) + ); } #[test] @@ -6216,6 +6305,28 @@ mod tests { }, ], selected_chairman_profile_id: Some(9), + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 42, + crate::RuntimeCompanyMarketState { + outstanding_shares: 30_000, + mutable_support_scalar_raw_u32: 0x3f19999a, + young_company_support_scalar_raw_u32: 0x42580000, + support_progress_word: 8, + recent_per_share_subscore_raw_u32: 0x42000000, + cached_share_price_raw_u32: 0x42180000, + chairman_salary_baseline: 21, + chairman_salary_current: 24, + founding_year: 1834, + last_bankruptcy_year: 0, + current_issue_calendar_word: 4, + prior_issue_calendar_word: 3, + city_connection_latch: false, + linked_transit_latch: true, + }, + )]), + ..RuntimeServiceState::default() + }, ..state() }; let save_slice = SmpLoadedSaveSlice { @@ -6263,6 +6374,10 @@ mod tests { assert_eq!(import.state.selected_company_id, Some(1)); assert_eq!(import.state.chairman_profiles, base_state.chairman_profiles); assert_eq!(import.state.selected_chairman_profile_id, Some(1)); + assert_eq!( + import.state.service_state.company_market_state, + base_state.service_state.company_market_state + ); } #[test] @@ -13371,6 +13486,7 @@ mod tests { trigger_dispatch_counts: BTreeMap::new(), total_event_record_services: 4, dirty_rerun_count: 2, + company_market_state: BTreeMap::new(), }, }; let save_slice = SmpLoadedSaveSlice { diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 9b15313..d84a727 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -47,10 +47,10 @@ pub use runtime::{ RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope, - RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, - RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, - RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, - RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, + RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric, + RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, + RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, + RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 2768110..5d08ae0 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -48,6 +48,38 @@ pub struct RuntimeCompany { pub track_piece_counts: RuntimeTrackPieceCounts, } +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct RuntimeCompanyMarketState { + #[serde(default)] + pub outstanding_shares: u32, + #[serde(default)] + pub mutable_support_scalar_raw_u32: u32, + #[serde(default)] + pub young_company_support_scalar_raw_u32: u32, + #[serde(default)] + pub support_progress_word: u32, + #[serde(default)] + pub recent_per_share_subscore_raw_u32: u32, + #[serde(default)] + pub cached_share_price_raw_u32: u32, + #[serde(default)] + pub chairman_salary_baseline: u32, + #[serde(default)] + pub chairman_salary_current: u32, + #[serde(default)] + pub founding_year: u32, + #[serde(default)] + pub last_bankruptcy_year: u32, + #[serde(default)] + pub current_issue_calendar_word: u32, + #[serde(default)] + pub prior_issue_calendar_word: u32, + #[serde(default)] + pub city_connection_latch: bool, + #[serde(default)] + pub linked_transit_latch: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeTrackPieceCounts { #[serde(default)] @@ -798,6 +830,8 @@ pub struct RuntimeServiceState { pub total_event_record_services: u64, #[serde(default)] pub dirty_rerun_count: u64, + #[serde(default)] + pub company_market_state: BTreeMap, } #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] @@ -1629,6 +1663,14 @@ impl RuntimeState { } } } + for company_id in self.service_state.company_market_state.keys() { + if !seen_company_ids.contains(company_id) { + return Err(format!( + "service_state.company_market_state references unknown company_id {}", + company_id + )); + } + } for (player_id, vars) in &self.player_runtime_variables { if !seen_player_ids.contains(player_id) { return Err(format!( diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 79f7faa..821d00a 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -9,10 +9,10 @@ use sha2::{Digest, Sha256}; use crate::{ RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope, - RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, - RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, - RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeTerritoryMetric, - RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, + RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric, + RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, + RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimePlayerTarget, + RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, }; pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec; @@ -2196,6 +2196,8 @@ pub struct SmpLoadedCompanyRosterEntry { pub takeover_cooldown_year: Option, #[serde(default)] pub merger_cooldown_year: Option, + #[serde(default)] + pub market_state: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -3329,20 +3331,36 @@ const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET: usize = 0x14f; const SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET: usize = 0x15f; const SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET: usize = 0x157; const SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163; +const SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET: usize = 0x16b; +const SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET: usize = 0x173; const SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET: usize = 0x289; const SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18; +const SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET: usize = 0x0d07; const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET: usize = 0x0d59; const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56; +const SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET: usize = 0x0d19; 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), + ( + "support_progress_word", + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET, + ), + ( + "recent_per_share_subscore", + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET, + ), ("cached_share_price", 0x0d7b), - ("current_issue_calendar_word", 0x16b), - ("prior_issue_calendar_word", 0x173), + ( + "current_issue_calendar_word", + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET, + ), + ( + "prior_issue_calendar_word", + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET, + ), ]; const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [(&str, usize); 6] = [ ("post_capacity_word_1", 0x7684), @@ -3398,9 +3416,65 @@ fn parse_save_company_roster_probe( bytes, record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET, )?; + let outstanding_shares = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET, + )?; let debt = parse_save_company_total_debt(bytes, record_offset)?; let available_track_laying_capacity = parse_save_company_available_track_laying_capacity(bytes, record_offset)?; + let mutable_support_scalar_raw_u32 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET, + )?; + let young_company_support_scalar_raw_u32 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET, + )?; + let support_progress_word = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET, + )?; + let recent_per_share_subscore_raw_u32 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET, + )?; + let cached_share_price_raw_u32 = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET, + )?; + let chairman_salary_baseline = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET, + )?; + let chairman_salary_current = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET, + )?; + let founding_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET, + )?; + let last_bankruptcy_year = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET, + )?; + let current_issue_calendar_word = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET, + )?; + let prior_issue_calendar_word = read_u32_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET, + )?; + let city_connection_latch = read_u8_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET, + )? != 0; + let linked_transit_latch = read_u8_at( + bytes, + record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET, + )? != 0; let merger_cooldown_year = parse_nonzero_u32( bytes, record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET, @@ -3425,6 +3499,22 @@ fn parse_save_company_roster_probe( management_attitude: 0, takeover_cooldown_year, merger_cooldown_year, + market_state: Some(RuntimeCompanyMarketState { + outstanding_shares, + mutable_support_scalar_raw_u32, + young_company_support_scalar_raw_u32, + support_progress_word, + recent_per_share_subscore_raw_u32, + cached_share_price_raw_u32, + chairman_salary_baseline, + chairman_salary_current, + founding_year, + last_bankruptcy_year, + current_issue_calendar_word, + prior_issue_calendar_word, + city_connection_latch, + linked_transit_latch, + }), }); } @@ -15386,7 +15476,31 @@ mod tests { .copy_from_slice(&0x000061aau32.to_le_bytes()); bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000061abu32.to_le_bytes()); - for (index, (name, linked, merger, takeover, bond_count, _debt, track_capacity)) in [ + for ( + index, + ( + name, + linked, + merger, + takeover, + bond_count, + _debt, + track_capacity, + mutable_support_scalar_raw_u32, + young_company_support_scalar_raw_u32, + support_progress_word, + recent_per_share_subscore_raw_u32, + cached_share_price_raw_u32, + chairman_salary_baseline, + chairman_salary_current, + founding_year, + last_bankruptcy_year, + current_issue_calendar_word, + prior_issue_calendar_word, + city_connection_latch, + linked_transit_latch, + ), + ) in [ ( "Company One", 1u32, @@ -15395,8 +15509,42 @@ mod tests { 2u8, 1_000_000u32, Some(603i32), + 0x3f800000u32, + 0x42340000u32, + 17u32, + 0x41f00000u32, + 0x426c0000u32, + 24u32, + 31u32, + 1842u32, + 1851u32, + 7u32, + 6u32, + true, + false, + ), + ( + "Company Two", + 2u32, + 0u32, + 1871u32, + 1u8, + 500_000u32, + None, + 0x40000000u32, + 0x42700000u32, + 33u32, + 0x42000000u32, + 0x42780000u32, + 28u32, + 36u32, + 1845u32, + 0u32, + 3u32, + 2u32, + false, + true, ), - ("Company Two", 2u32, 0u32, 1871u32, 1u8, 500_000u32, None), ] .into_iter() .enumerate() @@ -15412,6 +15560,12 @@ mod tests { bytes[record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET] = 1; bytes[record_offset + 0x47..record_offset + 0x4b] .copy_from_slice(&20000u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET + 4] + .copy_from_slice(&mutable_support_scalar_raw_u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET + 4] + .copy_from_slice(&young_company_support_scalar_raw_u32.to_le_bytes()); bytes[record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET] = bond_count; for slot_index in 0..bond_count as usize { let slot_offset = record_offset @@ -15432,6 +15586,37 @@ mod tests { bytes[record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET ..record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET + 4] .copy_from_slice(&raw_capacity.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET + 4] + .copy_from_slice(&support_progress_word.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET + 4] + .copy_from_slice(&recent_per_share_subscore_raw_u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET + 4] + .copy_from_slice(&cached_share_price_raw_u32.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET + 4] + .copy_from_slice(&chairman_salary_baseline.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET + 4] + .copy_from_slice(&chairman_salary_current.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET + 4] + .copy_from_slice(&founding_year.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET + 4] + .copy_from_slice(&last_bankruptcy_year.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET + 4] + .copy_from_slice(¤t_issue_calendar_word.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET + ..record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET + 4] + .copy_from_slice(&prior_issue_calendar_word.to_le_bytes()); + bytes[record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET] = + u8::from(city_connection_latch); + bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET] = + u8::from(linked_transit_latch); } let header_probe = parse_save_company_collection_header_probe( @@ -15481,11 +15666,40 @@ mod tests { assert_eq!(roster.entries[0].debt, 1_000_000); assert_eq!(roster.entries[0].available_track_laying_capacity, Some(603)); assert_eq!(roster.entries[0].merger_cooldown_year, Some(1862)); + let market_state = roster.entries[0] + .market_state + .as_ref() + .expect("company market state should load"); + assert_eq!(market_state.outstanding_shares, 20_000); + assert_eq!(market_state.mutable_support_scalar_raw_u32, 0x3f800000); + assert_eq!( + market_state.young_company_support_scalar_raw_u32, + 0x42340000 + ); + assert_eq!(market_state.support_progress_word, 17); + assert_eq!(market_state.recent_per_share_subscore_raw_u32, 0x41f00000); + assert_eq!(market_state.cached_share_price_raw_u32, 0x426c0000); + assert_eq!(market_state.chairman_salary_baseline, 24); + assert_eq!(market_state.chairman_salary_current, 31); + assert_eq!(market_state.founding_year, 1842); + assert_eq!(market_state.last_bankruptcy_year, 1851); + assert_eq!(market_state.current_issue_calendar_word, 7); + assert_eq!(market_state.prior_issue_calendar_word, 6); + assert!(market_state.city_connection_latch); + assert!(!market_state.linked_transit_latch); assert_eq!(roster.entries[1].company_id, 2); assert_eq!(roster.entries[1].linked_chairman_profile_id, Some(2)); assert_eq!(roster.entries[1].debt, 500_000); assert_eq!(roster.entries[1].available_track_laying_capacity, None); assert_eq!(roster.entries[1].takeover_cooldown_year, Some(1871)); + let second_market_state = roster.entries[1] + .market_state + .as_ref() + .expect("second company market state should load"); + assert_eq!(second_market_state.current_issue_calendar_word, 3); + assert_eq!(second_market_state.prior_issue_calendar_word, 2); + assert!(!second_market_state.city_connection_latch); + assert!(second_market_state.linked_transit_latch); } #[test] diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 41b092b..88f4187 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -2,6 +2,10 @@ use serde::{Deserialize, Serialize}; use crate::{CalendarPoint, RuntimeState}; +fn raw_u32_to_f32_text(raw: u32) -> String { + format!("{:.6}", f32::from_bits(raw)) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeSummary { pub calendar: CalendarPoint, @@ -39,6 +43,10 @@ pub struct RuntimeSummary { pub metadata_count: usize, pub company_count: usize, pub active_company_count: usize, + pub company_market_state_owner_count: usize, + 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 player_count: usize, pub chairman_profile_count: usize, pub active_chairman_profile_count: usize, @@ -122,6 +130,9 @@ pub struct RuntimeSummary { impl RuntimeSummary { pub fn from_state(state: &RuntimeState) -> Self { + let selected_company_market_state = state + .selected_company_id + .and_then(|company_id| state.service_state.company_market_state.get(&company_id)); Self { calendar: state.calendar, calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(), @@ -214,6 +225,15 @@ impl RuntimeSummary { .iter() .filter(|company| company.active) .count(), + company_market_state_owner_count: state.service_state.company_market_state.len(), + selected_company_outstanding_shares: selected_company_market_state + .map(|market_state| market_state.outstanding_shares), + selected_company_cached_share_price_value_f32_text: selected_company_market_state + .map(|market_state| raw_u32_to_f32_text(market_state.cached_share_price_raw_u32)), + selected_company_mutable_support_scalar_value_f32_text: selected_company_market_state + .map(|market_state| { + raw_u32_to_f32_text(market_state.mutable_support_scalar_raw_u32) + }), player_count: state.players.len(), chairman_profile_count: state.chairman_profiles.len(), active_chairman_profile_count: state @@ -1765,4 +1785,100 @@ mod tests { let summary = RuntimeSummary::from_state(&state); assert_eq!(summary.packed_event_blocked_shell_owned_descriptor_count, 1); } + + #[test] + fn summarizes_selected_company_market_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: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: Some(1), + 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 { + company_market_state: BTreeMap::from([( + 1, + crate::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + mutable_support_scalar_raw_u32: 0x3f800000, + young_company_support_scalar_raw_u32: 0x42340000, + support_progress_word: 12, + recent_per_share_subscore_raw_u32: 0x420c0000, + cached_share_price_raw_u32: 0x42200000, + chairman_salary_baseline: 24, + chairman_salary_current: 30, + founding_year: 1831, + last_bankruptcy_year: 0, + current_issue_calendar_word: 5, + prior_issue_calendar_word: 4, + city_connection_latch: true, + linked_transit_latch: false, + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!(summary.company_market_state_owner_count, 1); + assert_eq!(summary.selected_company_outstanding_shares, Some(20_000)); + assert_eq!( + summary.selected_company_cached_share_price_value_f32_text, + Some("40.000000".to_string()) + ); + assert_eq!( + summary.selected_company_mutable_support_scalar_value_f32_text, + Some("1.000000".to_string()) + ); + } } diff --git a/docs/README.md b/docs/README.md index de61aad..85d42f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -109,7 +109,11 @@ The highest-value next passes are now: candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate context, the grounded fixed-world issue-`0x37` pair, the separate six-float economic tuning band, derived holdings-at-share-price and cached purchasing-power totals, - context, company dword candidate windows, and richer chairman qword cache views + 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 + 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 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 691b169..ef186f8 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -69,7 +69,10 @@ Implemented today: the same fixed world payload now exposes the grounded issue-`0x37` pair at `[world+0x29/+0x2d]` and the separate six-float economic tuning band `[world+0x0be2..+0x0bf6]` through save inspection too, but current atlas evidence still keeps that editor-tuning family separate from - the company-governance issue lanes; + the company-governance issue lanes; the next shared company-side owning state is rehosted now + too, because save-native company direct records now project into a typed runtime + `company_market_state` cache map carrying outstanding shares, support/share-price/cache words, + chairman salary lanes, calendar words, and connection latches for each live company; and `runtime inspect-save-company-chairman ` now exposes the remaining raw company/chairman scalar candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate context, company dword candidate windows, richer chairman qword @@ -189,8 +192,11 @@ frontier is no longer anonymous id recovery; it is the remaining recovered-but-n families from the checked-in semantic catalog, especially cargo-price, add-building, and other descriptor clusters that now have explicit shell-owned or evidence-blocked status but not yet a bounded executable landing surface. Raw save reconstruction for company/chairman context is still a -later tranche once stronger evidence exists. Richer runtime ownership should still be added only -where a later descriptor or condition family needs more than the current event-owned roster. +later tranche once stronger evidence exists, but the current project rule is explicit: prefer +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. ## Why This Boundary