From 26a7a34ad0c055fad1647985816bffe1e81d3647 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 18:51:42 -0700 Subject: [PATCH] Refresh chairman wealth from company market state --- crates/rrt-runtime/src/import.rs | 4 + crates/rrt-runtime/src/runtime.rs | 260 ++++++++++++++++++++++++++++++ crates/rrt-runtime/src/step.rs | 91 +++++++++-- 3 files changed, 343 insertions(+), 12 deletions(-) diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index a1f2e96..4df3065 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -315,6 +315,8 @@ pub fn project_save_slice_to_runtime_state_import( ..RuntimeServiceState::default() }, }; + let mut state = state; + state.refresh_derived_market_state(); state.validate()?; Ok(RuntimeStateImport { @@ -424,6 +426,8 @@ pub fn project_save_slice_overlay_to_runtime_state_import( ..base_state.service_state.clone() }, }; + let mut state = state; + state.refresh_derived_market_state(); state.validate()?; Ok(RuntimeStateImport { diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 5d08ae0..283e770 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -1716,6 +1716,61 @@ impl RuntimeState { Ok(()) } + + pub fn refresh_derived_market_state(&mut self) { + let company_share_prices = self + .service_state + .company_market_state + .iter() + .filter_map(|(company_id, market_state)| { + rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32) + .map(|share_price| (*company_id, share_price)) + }) + .collect::>(); + + for profile in &mut self.chairman_profiles { + let preserved_threshold_adjusted_holdings_component = profile + .purchasing_power_total + .saturating_sub(profile.current_cash) + .max(0); + if let Some(holdings_value_total) = derive_runtime_chairman_holdings_share_price_total( + &profile.company_holdings, + &company_share_prices, + ) { + profile.holdings_value_total = holdings_value_total; + } + profile.net_worth_total = profile + .current_cash + .saturating_add(profile.holdings_value_total); + profile.purchasing_power_total = profile + .current_cash + .saturating_add(preserved_threshold_adjusted_holdings_component) + .max(profile.net_worth_total); + } + } +} + +fn rounded_cached_share_price_i64(raw_u32: u32) -> Option { + let value = f32::from_bits(raw_u32); + if !value.is_finite() { + return None; + } + if value < i64::MIN as f32 || value > i64::MAX as f32 { + return None; + } + Some(value.round() as i64) +} + +fn derive_runtime_chairman_holdings_share_price_total( + holdings_by_company: &BTreeMap, + company_share_prices: &BTreeMap, +) -> Option { + let mut total = 0i64; + for (company_id, units) in holdings_by_company { + let share_price = *company_share_prices.get(company_id)?; + total = total.checked_add((*units as i64).checked_mul(share_price)?)?; + } + Some(total) } fn validate_runtime_effect( @@ -3372,4 +3427,209 @@ mod tests { assert!(state.validate().is_err()); } + + #[test] + fn refreshes_chairman_totals_from_company_market_state() { + let mut 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, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }, + RuntimeCompany { + company_id: 2, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }, + ], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 100, + linked_company_id: Some(1), + company_holdings: BTreeMap::from([(1, 2), (2, 3)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 400, + }], + 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, + RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41200000, + ..RuntimeCompanyMarketState::default() + }, + ), + ( + 2, + RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41a00000, + ..RuntimeCompanyMarketState::default() + }, + ), + ]), + ..RuntimeServiceState::default() + }, + }; + + state.refresh_derived_market_state(); + + assert_eq!(state.chairman_profiles[0].holdings_value_total, 80); + assert_eq!(state.chairman_profiles[0].net_worth_total, 180); + assert_eq!(state.chairman_profiles[0].purchasing_power_total, 400); + } + + #[test] + fn refreshes_chairman_purchasing_power_when_cash_changes() { + let mut 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, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 50, + linked_company_id: Some(1), + company_holdings: BTreeMap::from([(1, 2)]), + holdings_value_total: 20, + net_worth_total: 70, + purchasing_power_total: 130, + }], + 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, + RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41200000, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + state.chairman_profiles[0].current_cash = 80; + + state.refresh_derived_market_state(); + + assert_eq!(state.chairman_profiles[0].holdings_value_total, 20); + assert_eq!(state.chairman_profiles[0].net_worth_total, 100); + assert_eq!(state.chairman_profiles[0].purchasing_power_total, 130); + } } diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 42f9f86..1b85dcb 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -115,6 +115,7 @@ pub fn execute_step_command( 0 } }; + state.refresh_derived_market_state(); let final_summary = RuntimeSummary::from_state(state); Ok(StepResult { @@ -363,7 +364,14 @@ fn apply_runtime_effects( "missing chairman profile_id {profile_id} while applying cash effect" ) })?; + let preserved_threshold_adjusted_holdings_component = chairman + .purchasing_power_total + .saturating_sub(chairman.current_cash) + .max(0); chairman.current_cash = *value; + chairman.purchasing_power_total = chairman + .current_cash + .saturating_add(preserved_threshold_adjusted_holdings_component); } } RuntimeEffect::SetCompanyGovernanceScalar { @@ -3790,28 +3798,64 @@ mod tests { #[test] fn set_chairman_cash_supports_all_active_target() { let mut state = RuntimeState { + 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: RuntimeCompanyControllerKind::Human, + linked_chairman_profile_id: Some(1), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + RuntimeCompany { + company_id: 2, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + linked_chairman_profile_id: Some(2), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + ], chairman_profiles: vec![ RuntimeChairmanProfile { profile_id: 1, name: "Chairman One".to_string(), active: true, current_cash: 10, - linked_company_id: None, - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, + linked_company_id: Some(1), + company_holdings: BTreeMap::from([(1, 2)]), + holdings_value_total: 20, + net_worth_total: 30, + purchasing_power_total: 70, }, RuntimeChairmanProfile { profile_id: 2, name: "Chairman Two".to_string(), active: true, current_cash: 20, - linked_company_id: None, - company_holdings: BTreeMap::new(), - holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, + linked_company_id: Some(2), + company_holdings: BTreeMap::from([(2, 3)]), + holdings_value_total: 60, + net_worth_total: 80, + purchasing_power_total: 110, }, RuntimeChairmanProfile { profile_id: 3, @@ -3821,8 +3865,8 @@ mod tests { linked_company_id: None, company_holdings: BTreeMap::new(), holdings_value_total: 0, - net_worth_total: 0, - purchasing_power_total: 0, + net_worth_total: 30, + purchasing_power_total: 30, }, ], event_runtime_records: vec![RuntimeEventRecord { @@ -3839,6 +3883,25 @@ mod tests { value: 77, }], }], + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([ + ( + 1, + crate::RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41200000, + ..crate::RuntimeCompanyMarketState::default() + }, + ), + ( + 2, + crate::RuntimeCompanyMarketState { + cached_share_price_raw_u32: 0x41a00000, + ..crate::RuntimeCompanyMarketState::default() + }, + ), + ]), + ..RuntimeServiceState::default() + }, ..state() }; @@ -3851,6 +3914,10 @@ mod tests { assert_eq!(state.chairman_profiles[0].current_cash, 77); assert_eq!(state.chairman_profiles[1].current_cash, 77); assert_eq!(state.chairman_profiles[2].current_cash, 30); + assert_eq!(state.chairman_profiles[0].net_worth_total, 97); + assert_eq!(state.chairman_profiles[0].purchasing_power_total, 137); + assert_eq!(state.chairman_profiles[1].net_worth_total, 137); + assert_eq!(state.chairman_profiles[1].purchasing_power_total, 167); } #[test]