From b322bed6ad3cdce78541aa5c1f916526b550f4e0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 06:01:43 -0700 Subject: [PATCH] Keep company cash and confiscation owner state in sync --- crates/rrt-runtime/src/step.rs | 259 ++++++++++++++++++++++++++++++--- 1 file changed, 235 insertions(+), 24 deletions(-) diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index eac1896..a02f5b0 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -419,6 +419,44 @@ fn service_set_company_prime_rate_target( ) } +fn service_zero_company_current_cash(state: &mut RuntimeState, company_id: u32) -> bool { + 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, + }, + ) else { + return false; + }; + if current_cash == 0 { + return true; + } + service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -(current_cash as f64), + false, + ) +} + +fn service_clear_company_live_bonds(state: &mut RuntimeState, company_id: u32) -> bool { + let Some(market_state) = state + .service_state + .company_market_state + .get_mut(&company_id) + else { + return false; + }; + market_state.live_bond_slots.clear(); + market_state.bond_count = 0; + market_state.largest_live_bond_principal = None; + market_state.highest_coupon_live_bond_principal = None; + true +} + fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool { let Some(bankruptcy_year) = state .world_restore @@ -486,24 +524,7 @@ 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 |= service_zero_company_current_cash(state, company_id); company_mutated } @@ -1533,6 +1554,8 @@ fn apply_runtime_effects( RuntimeEffect::ConfiscateCompanyAssets { target } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids.iter().copied() { + let _ = service_zero_company_current_cash(state, company_id); + let _ = service_clear_company_live_bonds(state, company_id); let company = state .companies .iter_mut() @@ -1610,17 +1633,35 @@ fn apply_runtime_effects( RuntimeEffect::AdjustCompanyCash { target, delta } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { - let company = state + let prior_cash = state .companies - .iter_mut() + .iter() .find(|company| company.company_id == company_id) + .map(|company| company.current_cash) .ok_or_else(|| { format!("missing company_id {company_id} while applying cash effect") })?; - company.current_cash = - company.current_cash.checked_add(*delta).ok_or_else(|| { - format!("company_id {company_id} cash adjustment overflow") - })?; + let next_cash = prior_cash.checked_add(*delta).ok_or_else(|| { + format!("company_id {company_id} cash adjustment overflow") + })?; + if !service_post_company_stat_delta( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + *delta as f64, + false, + ) { + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| { + format!( + "missing company_id {company_id} while applying cash effect" + ) + })?; + company.current_cash = next_cash; + } mutated_company_ids.insert(company_id); } } @@ -4675,6 +4716,121 @@ mod tests { ); } + #[test] + fn adjust_company_cash_updates_owner_state_backed_current_cash() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) + * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize] = 100.0f64.to_bits(); + + 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: 12, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::Ids { ids: vec![1] }, + delta: 25, + }], + }], + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + crate::RuntimeCompanyMarketState { + year_stat_family_qword_bits, + special_stat_family_232a_qword_bits: vec![0u64; 0x20], + ..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::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("cash adjustment should apply through owner state"); + + assert_eq!(state.companies[0].current_cash, 125); + assert_eq!( + crate::runtime::runtime_company_stat_value( + &state, + 1, + crate::RuntimeCompanyStatSelector { + family_id: crate::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + Some(125) + ); + } + #[test] fn applies_named_locomotive_availability_effects() { let mut state = RuntimeState { @@ -6324,6 +6480,16 @@ mod tests { #[test] fn confiscate_company_assets_zeros_company_and_retires_owned_trains() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) + * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize] = 50.0f64.to_bits(); + let mut state = RuntimeState { companies: vec![ RuntimeCompany { @@ -6393,6 +6559,26 @@ mod tests { target: RuntimeCompanyTarget::SelectedCompany, }], }], + service_state: RuntimeServiceState { + company_market_state: BTreeMap::from([( + 1, + crate::RuntimeCompanyMarketState { + year_stat_family_qword_bits, + special_stat_family_232a_qword_bits: vec![0u64; 0x20], + live_bond_slots: vec![crate::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 20_000, + maturity_year: 1845, + coupon_rate_raw_u32: 0.05f32.to_bits(), + }], + bond_count: 1, + largest_live_bond_principal: Some(20_000), + highest_coupon_live_bond_principal: Some(20_000), + ..crate::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::RuntimeServiceState::default() + }, ..state() }; @@ -6408,6 +6594,31 @@ mod tests { assert_eq!(state.selected_company_id, None); assert!(state.trains[0].retired); assert!(!state.trains[1].retired); + assert_eq!( + crate::runtime::runtime_company_stat_value( + &state, + 1, + crate::RuntimeCompanyStatSelector { + family_id: crate::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + }, + ), + Some(0) + ); + assert!( + state.service_state.company_market_state[&1] + .live_bond_slots + .is_empty() + ); + assert_eq!(state.service_state.company_market_state[&1].bond_count, 0); + assert_eq!( + state.service_state.company_market_state[&1].largest_live_bond_principal, + None + ); + assert_eq!( + state.service_state.company_market_state[&1].highest_coupon_live_bond_principal, + None + ); } #[test]