From 41418b4044f114a89f8f72e7e27a54a856626c7a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 05:59:27 -0700 Subject: [PATCH] Write company governance effects through owner state --- crates/rrt-runtime/src/import.rs | 4 +- crates/rrt-runtime/src/runtime.rs | 53 ++++- crates/rrt-runtime/src/step.rs | 343 ++++++++++++++++++++++++++++-- 3 files changed, 383 insertions(+), 17 deletions(-) diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 86b6bdb..5e8029e 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -6813,7 +6813,9 @@ mod tests { ) .expect("overlay import should project"); - assert_eq!(import.state.companies, base_state.companies); + let mut expected_companies = base_state.companies.clone(); + expected_companies[1].investor_confidence = 38; + assert_eq!(import.state.companies, expected_companies); 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)); diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 3fbc8a5..1451845 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -2280,14 +2280,33 @@ impl RuntimeState { let book_value_per_share = runtime_company_direct_float_field_value_f64(self, company.company_id, 0x32f) .and_then(runtime_round_f64_to_i64); - (company.company_id, current_cash, book_value_per_share) + let prime_rate = runtime_company_prime_rate(self, company.company_id); + let investor_confidence = + runtime_company_investor_confidence(self, company.company_id); + let management_attitude = + runtime_company_management_attitude(self, company.company_id); + ( + company.company_id, + current_cash, + book_value_per_share, + prime_rate, + investor_confidence, + management_attitude, + ) }) .collect::>(); for company in &mut self.companies { - if let Some((_, current_cash, book_value_per_share)) = company_refresh + if let Some(( + _, + current_cash, + book_value_per_share, + prime_rate, + investor_confidence, + management_attitude, + )) = company_refresh .iter() - .find(|(company_id, _, _)| *company_id == company.company_id) + .find(|(company_id, _, _, _, _, _)| *company_id == company.company_id) { if let Some(current_cash) = current_cash { company.current_cash = *current_cash; @@ -2295,6 +2314,15 @@ impl RuntimeState { if let Some(book_value_per_share) = book_value_per_share { company.book_value_per_share = *book_value_per_share; } + if let Some(prime_rate) = prime_rate { + company.prime_rate = Some(*prime_rate); + } + if let Some(investor_confidence) = investor_confidence { + company.investor_confidence = *investor_confidence; + } + if let Some(management_attitude) = management_attitude { + company.management_attitude = *management_attitude; + } } } @@ -6243,7 +6271,10 @@ mod tests { }, world_flags: BTreeMap::new(), save_profile: RuntimeSaveProfileState::default(), - world_restore: RuntimeWorldRestoreState::default(), + world_restore: RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + ..RuntimeWorldRestoreState::default() + }, metadata: BTreeMap::new(), companies: vec![RuntimeCompany { company_id: 1, @@ -6292,9 +6323,20 @@ mod tests { world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: { + let mut terms = vec![0; 0x3b]; + terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100; + terms + }, company_market_state: BTreeMap::from([( 1, RuntimeCompanyMarketState { + cached_share_price_raw_u32: 37.0f32.to_bits(), + issue_opinion_terms_raw_i32: { + let mut terms = vec![0; 0x3b]; + terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 58; + terms + }, direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( 0x32f, 2620.0f32.to_bits(), @@ -6311,6 +6353,9 @@ mod tests { assert_eq!(state.companies[0].current_cash, 275); assert_eq!(state.companies[0].book_value_per_share, 2620); + assert_eq!(state.companies[0].prime_rate, Some(6)); + assert_eq!(state.companies[0].investor_confidence, 37); + assert_eq!(state.companies[0].management_attitude, 58); } #[test] diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index c2c0e28..eac1896 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -318,6 +318,107 @@ fn service_post_company_stat_delta( } } +fn service_set_company_direct_float_field( + state: &mut RuntimeState, + company_id: u32, + field_offset: u32, + value: f64, +) -> bool { + if !value.is_finite() { + return false; + } + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + market_state + .direct_control_transfer_float_fields_raw_u32 + .insert(field_offset, (value as f32).to_bits()); + true +} + +fn service_set_company_cached_share_price( + state: &mut RuntimeState, + company_id: u32, + value: f64, +) -> bool { + if !value.is_finite() { + return false; + } + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + market_state.cached_share_price_raw_u32 = (value as f32).to_bits(); + true +} + +fn service_set_company_issue_opinion_total( + state: &mut RuntimeState, + company_id: u32, + issue_id: u32, + target_total: i64, +) -> bool { + let current_total = crate::runtime::runtime_world_issue_opinion_term_sum_raw( + state, + issue_id, + state + .companies + .iter() + .find(|company| company.company_id == company_id) + .and_then(|company| company.linked_chairman_profile_id), + Some(company_id), + None, + ) + .unwrap_or(0); + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + let issue_index = issue_id as usize; + if market_state.issue_opinion_terms_raw_i32.len() <= issue_index { + market_state + .issue_opinion_terms_raw_i32 + .resize(issue_index + 1, 0); + } + let prior_company_term = i64::from(market_state.issue_opinion_terms_raw_i32[issue_index]); + let next_company_term = prior_company_term.saturating_add(target_total - current_total); + let Ok(next_company_term_i32) = i32::try_from(next_company_term) else { + return false; + }; + market_state.issue_opinion_terms_raw_i32[issue_index] = next_company_term_i32; + true +} + +fn service_set_company_prime_rate_target( + state: &mut RuntimeState, + company_id: u32, + value: i64, +) -> bool { + let Some(baseline) = crate::runtime::runtime_world_prime_rate_baseline(state) else { + return false; + }; + let target_raw_sum = ((value as f64 - baseline) * 100.0).round(); + if !target_raw_sum.is_finite() { + return false; + } + service_set_company_issue_opinion_total( + state, + company_id, + crate::RUNTIME_WORLD_ISSUE_PRIME_RATE, + target_raw_sum as i64, + ) +} + fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool { let Some(bankruptcy_year) = state .world_restore @@ -385,6 +486,25 @@ fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) - company_mutated = true; } + if let Some(current_cash) = crate::runtime::runtime_company_stat_value( + state, + company_id, + crate::RuntimeCompanyStatSelector { + family_id: crate::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ) { + if current_cash != 0 { + company_mutated |= service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -(current_cash as f64), + false, + ); + } + } + company_mutated } @@ -1200,7 +1320,7 @@ fn apply_runtime_effects( state, company_id, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, - value.saturating_sub(prior_cash), + value.saturating_sub(prior_cash) as f64, false, ) { let company = state @@ -1260,6 +1380,43 @@ fn apply_runtime_effects( } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { + let mut applied_through_owner_state = false; + match metric { + RuntimeCompanyMetric::CreditRating => {} + RuntimeCompanyMetric::PrimeRate => { + applied_through_owner_state = + service_set_company_prime_rate_target(state, company_id, *value); + } + RuntimeCompanyMetric::BookValuePerShare => { + applied_through_owner_state = service_set_company_direct_float_field( + state, + company_id, + 0x32f, + *value as f64, + ); + } + RuntimeCompanyMetric::InvestorConfidence => { + applied_through_owner_state = service_set_company_cached_share_price( + state, + company_id, + *value as f64, + ); + } + RuntimeCompanyMetric::ManagementAttitude => { + applied_through_owner_state = service_set_company_issue_opinion_total( + state, + company_id, + crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE, + i64::from(*value), + ); + } + _ => { + return Err(format!( + "unsupported governance metric {:?} in company governance effect", + metric + )); + } + } let company = state .companies .iter_mut() @@ -1274,24 +1431,27 @@ fn apply_runtime_effects( company.credit_rating_score = Some(*value); } RuntimeCompanyMetric::PrimeRate => { - company.prime_rate = Some(*value); + if !applied_through_owner_state { + company.prime_rate = Some(*value); + } } RuntimeCompanyMetric::BookValuePerShare => { - company.book_value_per_share = *value; + if !applied_through_owner_state { + company.book_value_per_share = *value; + } } RuntimeCompanyMetric::InvestorConfidence => { - company.investor_confidence = *value; + if !applied_through_owner_state { + company.investor_confidence = *value; + } } RuntimeCompanyMetric::ManagementAttitude => { - company.management_attitude = *value; + if !applied_through_owner_state { + company.management_attitude = *value; + } } - _ => { - return Err(format!( - "unsupported governance metric {:?} in company governance effect", - metric - )); - } - } + _ => unreachable!(), + }; mutated_company_ids.insert(company_id); } } @@ -4356,6 +4516,165 @@ mod tests { ); } + #[test] + fn set_company_governance_scalar_updates_owner_state_backed_metrics() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Unknown, + current_cash: 100, + 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, + }], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 11, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::PrimeRate, + value: 6, + }, + RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::BookValuePerShare, + value: 2620, + }, + RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::InvestorConfidence, + value: 37, + }, + RuntimeEffect::SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + metric: RuntimeCompanyMetric::ManagementAttitude, + value: 58, + }, + ], + }], + service_state: RuntimeServiceState { + world_issue_opinion_base_terms_raw_i32: { + let mut terms = vec![0; 0x3b]; + terms[crate::RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 0; + terms + }, + company_market_state: BTreeMap::from([( + 1, + crate::RuntimeCompanyMarketState { + issue_opinion_terms_raw_i32: vec![0; 0x3b], + ..crate::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + calendar: crate::CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: crate::RuntimeSaveProfileState::default(), + world_restore: crate::RuntimeWorldRestoreState { + issue_37_value: Some(5.0f32.to_bits()), + ..crate::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + 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, + 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(), + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("governance effect should apply through owner state"); + + assert_eq!(state.companies[0].prime_rate, Some(6)); + assert_eq!(state.companies[0].book_value_per_share, 2620); + assert_eq!(state.companies[0].investor_confidence, 37); + assert_eq!(state.companies[0].management_attitude, 58); + assert_eq!( + crate::runtime::runtime_company_prime_rate(&state, 1), + Some(6) + ); + assert_eq!( + crate::runtime::runtime_company_book_value_per_share(&state, 1), + Some(2620) + ); + assert_eq!( + crate::runtime::runtime_company_investor_confidence(&state, 1), + Some(37) + ); + assert_eq!( + crate::runtime::runtime_company_management_attitude(&state, 1), + Some(58) + ); + assert_eq!( + state.service_state.company_market_state[&1] + .direct_control_transfer_float_fields_raw_u32 + .get(&0x32f) + .copied(), + Some(2620.0f32.to_bits()) + ); + assert_eq!( + state.service_state.company_market_state[&1].cached_share_price_raw_u32, + 37.0f32.to_bits() + ); + assert_eq!( + state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32 + [crate::RUNTIME_WORLD_ISSUE_PRIME_RATE as usize], + 100 + ); + assert_eq!( + state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32 + [crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize], + 58 + ); + } + #[test] fn applies_named_locomotive_availability_effects() { let mut state = RuntimeState {