use std::collections::BTreeSet; use serde::{Deserialize, Serialize}; use crate::runtime::{ runtime_company_bond_interest_rate_quote_f64, runtime_company_support_adjusted_share_price_scalar_with_pressure_f64, runtime_round_f64_to_i64, }; use crate::{ RUNTIME_COMPANY_STAT_SLOT_COUNT, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN, RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget, RuntimeState, RuntimeSummary, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, calendar::BoundaryEventKind, runtime_company_annual_dividend_policy_state, runtime_company_annual_finance_policy_state, runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state, runtime_company_book_value_per_share, runtime_company_credit_rating, runtime_company_investor_confidence, runtime_company_management_attitude, runtime_company_prime_rate, }; const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4]; const COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT: u32 = 0x33f; const COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE: f64 = -0.02; const COMPANY_REPURCHASE_PRESSURE_SCALE: f64 = 0.7; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum StepCommand { AdvanceTo { calendar: crate::CalendarPoint }, StepCount { steps: u32 }, ServiceTriggerKind { trigger_kind: u8 }, ServicePeriodicBoundary, } impl StepCommand { pub fn validate(&self) -> Result<(), String> { match self { Self::AdvanceTo { calendar } => calendar.validate(), Self::StepCount { steps } => { if *steps == 0 { return Err("step_count command requires steps > 0".to_string()); } Ok(()) } Self::ServiceTriggerKind { .. } | Self::ServicePeriodicBoundary => Ok(()), } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BoundaryEvent { pub kind: String, pub calendar: crate::CalendarPoint, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServiceEvent { pub kind: String, pub trigger_kind: Option, pub serviced_record_ids: Vec, pub applied_effect_count: u32, pub mutated_company_ids: Vec, pub mutated_player_ids: Vec, pub appended_record_ids: Vec, pub activated_record_ids: Vec, pub deactivated_record_ids: Vec, pub removed_record_ids: Vec, pub dirty_rerun: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StepResult { pub initial_summary: RuntimeSummary, pub final_summary: RuntimeSummary, pub steps_executed: u64, pub boundary_events: Vec, pub service_events: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] enum EventGraphMutation { Append(RuntimeEventRecordTemplate), Activate { record_id: u32 }, Deactivate { record_id: u32 }, Remove { record_id: u32 }, } #[derive(Debug, Default)] struct AppliedEffectsSummary { applied_effect_count: u32, appended_record_ids: Vec, activated_record_ids: Vec, deactivated_record_ids: Vec, removed_record_ids: Vec, } #[derive(Debug, Default)] struct ResolvedConditionContext { matching_company_ids: BTreeSet, matching_player_ids: BTreeSet, matching_chairman_profile_ids: BTreeSet, } pub fn execute_step_command( state: &mut RuntimeState, command: &StepCommand, ) -> Result { state.validate()?; command.validate()?; let initial_summary = RuntimeSummary::from_state(state); let mut boundary_events = Vec::new(); let mut service_events = Vec::new(); let steps_executed = match command { StepCommand::AdvanceTo { calendar } => { advance_to_target_calendar_point(state, *calendar, &mut boundary_events)? } StepCommand::StepCount { steps } => step_count(state, *steps, &mut boundary_events), StepCommand::ServiceTriggerKind { trigger_kind } => { service_trigger_kind(state, *trigger_kind, &mut service_events)?; 0 } StepCommand::ServicePeriodicBoundary => { service_periodic_boundary(state, &mut service_events)?; 0 } }; state.refresh_derived_market_state(); let final_summary = RuntimeSummary::from_state(state); Ok(StepResult { initial_summary, final_summary, steps_executed, boundary_events, service_events, }) } fn advance_to_target_calendar_point( state: &mut RuntimeState, target: crate::CalendarPoint, boundary_events: &mut Vec, ) -> Result { target.validate()?; if target < state.calendar { return Err(format!( "advance_to target {:?} is earlier than current calendar {:?}", target, state.calendar )); } let mut steps = 0_u64; while state.calendar < target { step_once(state, boundary_events); steps += 1; } Ok(steps) } fn step_count( state: &mut RuntimeState, steps: u32, boundary_events: &mut Vec, ) -> u64 { for _ in 0..steps { step_once(state, boundary_events); } steps.into() } fn step_once(state: &mut RuntimeState, boundary_events: &mut Vec) { let boundary = state.calendar.step_forward(); if boundary != BoundaryEventKind::Tick { boundary_events.push(BoundaryEvent { kind: boundary_kind_label(boundary).to_string(), calendar: state.calendar, }); } } fn boundary_kind_label(boundary: BoundaryEventKind) -> &'static str { match boundary { BoundaryEventKind::Tick => "tick", BoundaryEventKind::PhaseRollover => "phase_rollover", BoundaryEventKind::MonthRollover => "month_rollover", BoundaryEventKind::YearRollover => "year_rollover", } } fn service_periodic_boundary( state: &mut RuntimeState, service_events: &mut Vec, ) -> Result<(), String> { state.service_state.periodic_boundary_calls += 1; for trigger_kind in PERIODIC_TRIGGER_KIND_ORDER { service_trigger_kind(state, trigger_kind, service_events)?; } service_company_annual_finance_policy(state, service_events)?; Ok(()) } fn service_decode_saved_f64_bits(raw_bits: u64) -> Option { let value = f64::from_bits(raw_bits); value.is_finite().then_some(value) } fn service_ensure_company_stat_post_capacity( market_state: &mut crate::RuntimeCompanyMarketState, slot_id: u32, ) -> Option { let index = slot_id .checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? .try_into() .ok()?; let required_year_len = ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; if market_state.year_stat_family_qword_bits.len() < required_year_len { market_state .year_stat_family_qword_bits .resize(required_year_len, 0.0f64.to_bits()); } let required_special_len = RUNTIME_COMPANY_STAT_SLOT_COUNT as usize; if market_state.special_stat_family_232a_qword_bits.len() < required_special_len { market_state .special_stat_family_232a_qword_bits .resize(required_special_len, 0.0f64.to_bits()); } Some(index) } fn service_post_company_stat_delta( state: &mut RuntimeState, company_id: u32, slot_id: u32, delta: f64, mirror_cash_totals: bool, ) -> bool { if !delta.is_finite() { return false; } let Some(refreshed_current_cash) = ({ let Some(market_state) = state .service_state .company_market_state .get_mut(&company_id) else { return false; }; let Some(index) = service_ensure_company_stat_post_capacity(market_state, slot_id) else { return false; }; let prior_year_value = market_state .year_stat_family_qword_bits .get(index) .copied() .and_then(service_decode_saved_f64_bits) .unwrap_or(0.0); market_state.year_stat_family_qword_bits[index] = (prior_year_value + delta).to_bits(); let special_index = slot_id as usize; let prior_special_value = market_state .special_stat_family_232a_qword_bits .get(special_index) .copied() .and_then(service_decode_saved_f64_bits) .unwrap_or(0.0); market_state.special_stat_family_232a_qword_bits[special_index] = (prior_special_value + delta).to_bits(); if mirror_cash_totals { let cash_index = RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize; let prior_cash_shadow_value = market_state .special_stat_family_232a_qword_bits .get(cash_index) .copied() .and_then(service_decode_saved_f64_bits) .unwrap_or(0.0); market_state.special_stat_family_232a_qword_bits[cash_index] = (prior_cash_shadow_value + delta).to_bits(); } market_state .year_stat_family_qword_bits .get( (RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize, ) .copied() .and_then(service_decode_saved_f64_bits) .and_then(runtime_round_f64_to_i64) }) else { return false; }; if let Some(company) = state .companies .iter_mut() .find(|company| company.company_id == company_id) { company.current_cash = refreshed_current_cash; true } else { false } } fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool { let Some(bankruptcy_year) = state .world_restore .packed_year_word_raw_u16 .map(u32::from) .or_else(|| Some(state.calendar.year)) else { return false; }; let mut company_mutated = false; if let Some(market_state) = state .service_state .company_market_state .get_mut(&company_id) { market_state.last_bankruptcy_year = bankruptcy_year; for slot in &mut market_state.live_bond_slots { slot.principal /= 2; } market_state .live_bond_slots .retain(|slot| slot.principal > 0); market_state.bond_count = market_state.live_bond_slots.len().min(u8::MAX as usize) as u8; market_state.largest_live_bond_principal = market_state .live_bond_slots .iter() .map(|slot| slot.principal) .max(); market_state.highest_coupon_live_bond_principal = market_state .live_bond_slots .iter() .filter_map(|slot| { let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; coupon.is_finite().then_some((coupon, slot.principal)) }) .max_by(|left, right| { left.0 .partial_cmp(&right.0) .unwrap_or(std::cmp::Ordering::Equal) }) .map(|(_, principal)| principal); company_mutated = true; } let remaining_debt = state .service_state .company_market_state .get(&company_id) .map(|market_state| { market_state .live_bond_slots .iter() .map(|slot| u64::from(slot.principal)) .sum::() }); if let Some(company) = state .companies .iter_mut() .find(|company| company.company_id == company_id) { if let Some(remaining_debt) = remaining_debt { company.debt = remaining_debt.min(u64::from(u32::MAX)) as u64; } company_mutated = true; } company_mutated } fn service_repay_matured_company_live_bonds_and_compact( state: &mut RuntimeState, company_id: u32, ) -> bool { let Some(current_year_word) = state .world_restore .packed_year_word_raw_u16 .map(u32::from) .or_else(|| Some(state.calendar.year)) else { return false; }; let retired_principal_total = state .service_state .company_market_state .get(&company_id) .map(|market_state| { market_state .live_bond_slots .iter() .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) .map(|slot| u64::from(slot.principal)) .sum::() }) .unwrap_or(0); if retired_principal_total == 0 { return false; } let retired_principal_total_f64 = retired_principal_total as f64; let mut company_mutated = false; company_mutated |= service_post_company_stat_delta( state, company_id, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, -retired_principal_total_f64, false, ); company_mutated |= service_post_company_stat_delta( state, company_id, 0x12, retired_principal_total_f64, false, ); if let Some(market_state) = state .service_state .company_market_state .get_mut(&company_id) { market_state .live_bond_slots .retain(|slot| slot.maturity_year == 0 || slot.maturity_year > current_year_word); for (slot_index, slot) in market_state.live_bond_slots.iter_mut().enumerate() { slot.slot_index = slot_index as u32; } market_state.bond_count = market_state.live_bond_slots.len().min(u8::MAX as usize) as u8; market_state.largest_live_bond_principal = market_state .live_bond_slots .iter() .map(|slot| slot.principal) .max(); market_state.highest_coupon_live_bond_principal = market_state .live_bond_slots .iter() .filter_map(|slot| { let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; coupon.is_finite().then_some((coupon, slot.principal)) }) .max_by(|left, right| { left.0 .partial_cmp(&right.0) .unwrap_or(std::cmp::Ordering::Equal) }) .map(|(_, principal)| principal); company_mutated = true; } if let Some(company) = state .companies .iter_mut() .find(|company| company.company_id == company_id) { company.debt = state .service_state .company_market_state .get(&company_id) .map(|market_state| { market_state .live_bond_slots .iter() .map(|slot| u64::from(slot.principal)) .sum::() }) .unwrap_or(0); company_mutated = true; } company_mutated } fn service_company_annual_finance_policy( state: &mut RuntimeState, service_events: &mut Vec, ) -> Result<(), String> { let active_company_ids = state .companies .iter() .filter(|company| company.active) .map(|company| company.company_id) .collect::>(); let active_company_id_set = active_company_ids.iter().copied().collect::>(); state .service_state .annual_finance_last_actions .retain(|company_id, _| active_company_id_set.contains(company_id)); state.service_state.annual_finance_service_calls += 1; let mut mutated_company_ids = BTreeSet::new(); let mut applied_effect_count = 0u32; for company_id in active_company_ids { let Some(policy_state) = runtime_company_annual_finance_policy_state(state, company_id) else { continue; }; state .service_state .annual_finance_last_actions .insert(company_id, policy_state.action); *state .service_state .annual_finance_action_counts .entry(policy_state.action) .or_insert(0) += 1; match policy_state.action { crate::RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment => { let Some(dividend_state) = runtime_company_annual_dividend_policy_state(state, company_id) else { continue; }; let Some(proposed_tenths) = dividend_state.proposed_dividend_per_share_tenths else { continue; }; let Some(market_state) = state .service_state .company_market_state .get_mut(&company_id) else { continue; }; let raw_bits = ((proposed_tenths as f32) / 10.0).to_bits(); let prior_bits = market_state .direct_control_transfer_float_fields_raw_u32 .insert(COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT, raw_bits); if prior_bits != Some(raw_bits) { applied_effect_count += 1; state.service_state.annual_dividend_adjustment_commit_count += 1; mutated_company_ids.insert(company_id); } } crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy | crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => { if service_apply_company_bankruptcy(state, company_id) { applied_effect_count += 1; mutated_company_ids.insert(company_id); } } crate::RuntimeCompanyAnnualFinancePolicyAction::StockIssue => { let Some(issue_state) = runtime_company_annual_stock_issue_state(state, company_id) else { continue; }; let Some(batch_size) = issue_state.trimmed_issue_batch_size else { continue; }; let Some(proceeds_per_tranche) = issue_state.pressured_proceeds else { continue; }; let Some(current_tuple_word_0) = state.world_restore.current_calendar_tuple_word_raw_u32 else { continue; }; let Some(current_tuple_word_1) = state.world_restore.current_calendar_tuple_word_2_raw_u32 else { continue; }; let Some(total_share_delta) = batch_size.checked_mul(2) else { continue; }; let Some(next_outstanding_shares) = state .service_state .company_market_state .get(&company_id) .map(|market_state| market_state.outstanding_shares) .and_then(|value| value.checked_add(total_share_delta)) else { continue; }; let mut mutated = false; for _ in 0..2 { mutated |= service_post_company_stat_delta( state, company_id, 0x0c, (batch_size as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, true, ); mutated |= service_post_company_stat_delta( state, company_id, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, proceeds_per_tranche as f64, false, ); } let Some(market_state) = state .service_state .company_market_state .get_mut(&company_id) else { continue; }; market_state.outstanding_shares = next_outstanding_shares; market_state.prior_issue_calendar_word = market_state.current_issue_calendar_word; market_state.prior_issue_calendar_word_2 = market_state.current_issue_calendar_word_2; market_state.current_issue_calendar_word = current_tuple_word_0; market_state.current_issue_calendar_word_2 = current_tuple_word_1; if mutated { applied_effect_count += 1; mutated_company_ids.insert(company_id); } } crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue => { let mut mutated = false; let Some(bond_state) = crate::runtime::runtime_company_annual_bond_policy_state(state, company_id) else { continue; }; if !bond_state.eligible_for_bond_issue_branch { continue; } mutated |= service_repay_matured_company_live_bonds_and_compact(state, company_id); let issue_bond_count = bond_state.proposed_issue_bond_count.unwrap_or(0); let Some(principal) = bond_state.issue_principal_step else { if mutated { applied_effect_count += 1; mutated_company_ids.insert(company_id); } continue; }; let Some(years_to_maturity) = bond_state.proposed_issue_years_to_maturity else { if mutated { applied_effect_count += 1; mutated_company_ids.insert(company_id); } continue; }; let Some(maturity_year) = state .world_restore .packed_year_word_raw_u16 .map(u32::from) .and_then(|year| year.checked_add(years_to_maturity)) else { if mutated { applied_effect_count += 1; mutated_company_ids.insert(company_id); } continue; }; for _ in 0..issue_bond_count { let Some(quote_rate) = runtime_company_bond_interest_rate_quote_f64( state, company_id, principal, years_to_maturity, ) else { break; }; mutated |= service_post_company_stat_delta( state, company_id, 0x0c, (principal as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, true, ); mutated |= service_post_company_stat_delta( state, company_id, 0x12, -(principal as f64), false, ); mutated |= service_post_company_stat_delta( state, company_id, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, principal as f64, false, ); let Some(market_state) = state .service_state .company_market_state .get_mut(&company_id) else { break; }; let slot_index = market_state.bond_count as u32; if market_state.bond_count == u8::MAX { break; } market_state .live_bond_slots .push(crate::RuntimeCompanyBondSlot { slot_index, principal, maturity_year, coupon_rate_raw_u32: (quote_rate as f32).to_bits(), }); market_state.bond_count = market_state.bond_count.saturating_add(1); market_state.largest_live_bond_principal = Some( market_state .largest_live_bond_principal .unwrap_or(0) .max(principal), ); let highest_coupon_live_principal = market_state .live_bond_slots .iter() .filter_map(|slot| { let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; coupon.is_finite().then_some((coupon, slot.principal)) }) .max_by(|left, right| { left.0 .partial_cmp(&right.0) .unwrap_or(std::cmp::Ordering::Equal) }) .map(|(_, principal)| principal); market_state.highest_coupon_live_bond_principal = highest_coupon_live_principal; } if let Some(company) = state .companies .iter_mut() .find(|company| company.company_id == company_id) { company.debt = state .service_state .company_market_state .get(&company_id) .map(|market_state| { market_state .live_bond_slots .iter() .map(|slot| u64::from(slot.principal)) .sum::() }) .unwrap_or(company.debt); } if mutated { applied_effect_count += 1; mutated_company_ids.insert(company_id); } } crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => { let mut mutated = false; for _ in 0..128 { let Some(repurchase_state) = runtime_company_annual_stock_repurchase_state(state, company_id) else { break; }; if !repurchase_state.eligible_for_single_batch_repurchase { break; } let Some(batch_size) = repurchase_state.repurchase_batch_size else { break; }; let Some(pressure_shares) = runtime_round_f64_to_i64( batch_size as f64 * COMPANY_REPURCHASE_PRESSURE_SCALE, ) else { break; }; let Some(share_price_scalar) = runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( state, company_id, pressure_shares, ) else { break; }; let Some(repurchase_total) = runtime_round_f64_to_i64(share_price_scalar * batch_size as f64) else { break; }; if repurchase_total <= 0 { break; } let Some(next_outstanding_shares) = state .service_state .company_market_state .get(&company_id) .map(|market_state| market_state.outstanding_shares) .and_then(|value| value.checked_sub(batch_size)) else { break; }; mutated |= service_post_company_stat_delta( state, company_id, 0x0c, (repurchase_total as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, true, ); mutated |= service_post_company_stat_delta( state, company_id, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, -(repurchase_total as f64), false, ); let Some(market_state) = state .service_state .company_market_state .get_mut(&company_id) else { break; }; market_state.outstanding_shares = next_outstanding_shares; } if mutated { applied_effect_count += 1; mutated_company_ids.insert(company_id); } } _ => {} } } service_events.push(ServiceEvent { kind: "annual_finance_policy".to_string(), trigger_kind: None, serviced_record_ids: Vec::new(), applied_effect_count, mutated_company_ids: mutated_company_ids.into_iter().collect(), mutated_player_ids: Vec::new(), appended_record_ids: Vec::new(), activated_record_ids: Vec::new(), deactivated_record_ids: Vec::new(), removed_record_ids: Vec::new(), dirty_rerun: false, }); Ok(()) } fn service_trigger_kind( state: &mut RuntimeState, trigger_kind: u8, service_events: &mut Vec, ) -> Result<(), String> { let eligible_indices = state .event_runtime_records .iter() .enumerate() .filter(|(_, record)| { record.active && record.trigger_kind == trigger_kind && !(record.one_shot && record.has_fired) }) .map(|(index, _)| index) .collect::>(); let mut serviced_record_ids = Vec::new(); let mut applied_effect_count = 0_u32; let mut mutated_company_ids = BTreeSet::new(); let mut mutated_player_ids = BTreeSet::new(); let mut appended_record_ids = Vec::new(); let mut activated_record_ids = Vec::new(); let mut deactivated_record_ids = Vec::new(); let mut removed_record_ids = Vec::new(); let mut staged_event_graph_mutations = Vec::new(); let mut dirty_rerun = false; *state .service_state .trigger_dispatch_counts .entry(trigger_kind) .or_insert(0) += 1; for index in eligible_indices { let ( record_id, record_conditions, record_effects, record_marks_collection_dirty, record_one_shot, ) = { let record = &state.event_runtime_records[index]; ( record.record_id, record.conditions.clone(), record.effects.clone(), record.marks_collection_dirty, record.one_shot, ) }; let Some(condition_context) = evaluate_record_conditions(state, &record_conditions)? else { continue; }; let effect_summary = apply_runtime_effects( state, &record_effects, &condition_context, &mut mutated_company_ids, &mut mutated_player_ids, &mut staged_event_graph_mutations, )?; applied_effect_count += effect_summary.applied_effect_count; appended_record_ids.extend(effect_summary.appended_record_ids); activated_record_ids.extend(effect_summary.activated_record_ids); deactivated_record_ids.extend(effect_summary.deactivated_record_ids); removed_record_ids.extend(effect_summary.removed_record_ids); { let record = &mut state.event_runtime_records[index]; record.service_count += 1; if record_one_shot { record.has_fired = true; } } serviced_record_ids.push(record_id); state.service_state.total_event_record_services += 1; if trigger_kind != 0x0a && record_marks_collection_dirty { dirty_rerun = true; } } commit_staged_event_graph_mutations(state, &staged_event_graph_mutations)?; service_events.push(ServiceEvent { kind: "trigger_dispatch".to_string(), trigger_kind: Some(trigger_kind), serviced_record_ids, applied_effect_count, mutated_company_ids: mutated_company_ids.into_iter().collect(), mutated_player_ids: mutated_player_ids.into_iter().collect(), appended_record_ids, activated_record_ids, deactivated_record_ids, removed_record_ids, dirty_rerun, }); if dirty_rerun { state.service_state.dirty_rerun_count += 1; service_trigger_kind(state, 0x0a, service_events)?; } Ok(()) } fn apply_runtime_effects( state: &mut RuntimeState, effects: &[RuntimeEffect], condition_context: &ResolvedConditionContext, mutated_company_ids: &mut BTreeSet, mutated_player_ids: &mut BTreeSet, staged_event_graph_mutations: &mut Vec, ) -> Result { let mut summary = AppliedEffectsSummary::default(); for effect in effects { match effect { RuntimeEffect::SetWorldFlag { key, value } => { state.world_flags.insert(key.clone(), *value); } RuntimeEffect::SetWorldScalarOverride { key, value } => { state.world_scalar_overrides.insert(key.clone(), *value); } RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { state.world_restore.limited_track_building_amount = Some(*value); } RuntimeEffect::SetEconomicStatusCode { value } => { state.world_restore.economic_status_code = Some(*value); } RuntimeEffect::SetCompanyCash { target, value } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { 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 = *value; mutated_company_ids.insert(company_id); } } RuntimeEffect::SetPlayerCash { target, value } => { let player_ids = resolve_player_target_ids(state, target, condition_context)?; for player_id in player_ids { let player = state .players .iter_mut() .find(|player| player.player_id == player_id) .ok_or_else(|| { format!("missing player_id {player_id} while applying cash effect") })?; player.current_cash = *value; mutated_player_ids.insert(player_id); } } RuntimeEffect::SetChairmanCash { target, value } => { let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?; for profile_id in profile_ids { let chairman = state .chairman_profiles .iter_mut() .find(|profile| profile.profile_id == profile_id) .ok_or_else(|| { format!( "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 { target, metric, value, } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { let company = state .companies .iter_mut() .find(|company| company.company_id == company_id) .ok_or_else(|| { format!( "missing company_id {company_id} while applying governance effect" ) })?; match metric { RuntimeCompanyMetric::CreditRating => { company.credit_rating_score = Some(*value); } RuntimeCompanyMetric::PrimeRate => { company.prime_rate = Some(*value); } RuntimeCompanyMetric::BookValuePerShare => { company.book_value_per_share = *value; } RuntimeCompanyMetric::InvestorConfidence => { company.investor_confidence = *value; } RuntimeCompanyMetric::ManagementAttitude => { company.management_attitude = *value; } _ => { return Err(format!( "unsupported governance metric {:?} in company governance effect", metric )); } } mutated_company_ids.insert(company_id); } } RuntimeEffect::DeactivatePlayer { target } => { let player_ids = resolve_player_target_ids(state, target, condition_context)?; for player_id in player_ids { let player = state .players .iter_mut() .find(|player| player.player_id == player_id) .ok_or_else(|| { format!( "missing player_id {player_id} while applying deactivate effect" ) })?; player.active = false; mutated_player_ids.insert(player_id); if state.selected_player_id == Some(player_id) { state.selected_player_id = None; } } } RuntimeEffect::DeactivateChairman { target } => { let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?; for profile_id in profile_ids.iter().copied() { let linked_company_id = state .chairman_profiles .iter() .find(|profile| profile.profile_id == profile_id) .and_then(|profile| profile.linked_company_id); let chairman = state .chairman_profiles .iter_mut() .find(|profile| profile.profile_id == profile_id) .ok_or_else(|| { format!( "missing chairman profile_id {profile_id} while applying deactivate effect" ) })?; chairman.active = false; chairman.linked_company_id = None; if state.selected_chairman_profile_id == Some(profile_id) { state.selected_chairman_profile_id = None; } if let Some(linked_company_id) = linked_company_id { if let Some(company) = state .companies .iter_mut() .find(|company| company.company_id == linked_company_id) { company.linked_chairman_profile_id = None; mutated_company_ids.insert(linked_company_id); } for other in &mut state.chairman_profiles { if other.profile_id != profile_id && other.linked_company_id == Some(linked_company_id) { other.linked_company_id = None; } } } } } RuntimeEffect::SetCompanyTerritoryAccess { target, territory, value, } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; let territory_ids = resolve_territory_target_ids(state, territory)?; set_company_territory_access_pairs( &mut state.company_territory_access, &company_ids, &territory_ids, *value, ); mutated_company_ids.extend(company_ids); } RuntimeEffect::ConfiscateCompanyAssets { target } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids.iter().copied() { let company = state .companies .iter_mut() .find(|company| company.company_id == company_id) .ok_or_else(|| { format!( "missing company_id {company_id} while applying confiscate effect" ) })?; company.current_cash = 0; company.debt = 0; company.active = false; mutated_company_ids.insert(company_id); if state.selected_company_id == Some(company_id) { state.selected_company_id = None; } } retire_matching_trains(&mut state.trains, Some(&company_ids), None, None); } RuntimeEffect::DeactivateCompany { target } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { let company = state .companies .iter_mut() .find(|company| company.company_id == company_id) .ok_or_else(|| { format!( "missing company_id {company_id} while applying deactivate effect" ) })?; company.active = false; mutated_company_ids.insert(company_id); if state.selected_company_id == Some(company_id) { state.selected_company_id = None; } } } RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { let company = state .companies .iter_mut() .find(|company| company.company_id == company_id) .ok_or_else(|| { format!( "missing company_id {company_id} while applying track capacity effect" ) })?; company.available_track_laying_capacity = *value; mutated_company_ids.insert(company_id); } } RuntimeEffect::RetireTrains { company_target, territory_target, locomotive_name, } => { let company_ids = company_target .as_ref() .map(|target| resolve_company_target_ids(state, target, condition_context)) .transpose()?; let territory_ids = territory_target .as_ref() .map(|target| resolve_territory_target_ids(state, target)) .transpose()?; retire_matching_trains( &mut state.trains, company_ids.as_ref(), territory_ids.as_ref(), locomotive_name.as_deref(), ); } RuntimeEffect::AdjustCompanyCash { target, delta } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { 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 = company.current_cash.checked_add(*delta).ok_or_else(|| { format!("company_id {company_id} cash adjustment overflow") })?; mutated_company_ids.insert(company_id); } } RuntimeEffect::AdjustCompanyDebt { target, delta } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { let company = state .companies .iter_mut() .find(|company| company.company_id == company_id) .ok_or_else(|| { format!("missing company_id {company_id} while applying debt effect") })?; company.debt = apply_u64_delta(company.debt, *delta, company_id)?; mutated_company_ids.insert(company_id); } } RuntimeEffect::SetCandidateAvailability { name, value } => { state.candidate_availability.insert(name.clone(), *value); } RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => { state .named_locomotive_availability .insert(name.clone(), u32::from(*value)); } RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => { state .named_locomotive_availability .insert(name.clone(), *value); } RuntimeEffect::SetNamedLocomotiveCost { name, value } => { state.named_locomotive_cost.insert(name.clone(), *value); } RuntimeEffect::SetCargoPriceOverride { target, value } => match target { RuntimeCargoPriceTarget::All => { state.all_cargo_price_override = Some(*value); } RuntimeCargoPriceTarget::Named { name } => { state .named_cargo_price_overrides .insert(name.clone(), *value); } }, RuntimeEffect::SetCargoProductionOverride { target, value } => match target { RuntimeCargoProductionTarget::All => { state.all_cargo_production_override = Some(*value); } RuntimeCargoProductionTarget::Factory => { state.factory_cargo_production_override = Some(*value); } RuntimeCargoProductionTarget::FarmMine => { state.farm_mine_cargo_production_override = Some(*value); } RuntimeCargoProductionTarget::Named { name } => { state .named_cargo_production_overrides .insert(name.clone(), *value); } }, RuntimeEffect::SetCargoProductionSlot { slot, value } => { state.cargo_production_overrides.insert(*slot, *value); } RuntimeEffect::SetWorldVariable { index, value } => { state.world_runtime_variables.insert(*index, *value); } RuntimeEffect::SetCompanyVariable { target, index, value, } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids { state .company_runtime_variables .entry(company_id) .or_default() .insert(*index, *value); mutated_company_ids.insert(company_id); } } RuntimeEffect::SetPlayerVariable { target, index, value, } => { let player_ids = resolve_player_target_ids(state, target, condition_context)?; for player_id in player_ids { state .player_runtime_variables .entry(player_id) .or_default() .insert(*index, *value); } } RuntimeEffect::SetTerritoryVariable { target, index, value, } => { let territory_ids = resolve_territory_target_ids(state, target)?; for territory_id in territory_ids { state .territory_runtime_variables .entry(territory_id) .or_default() .insert(*index, *value); } } RuntimeEffect::SetTerritoryAccessCost { value } => { state.world_restore.territory_access_cost = Some(*value); } RuntimeEffect::SetSpecialCondition { label, value } => { state.special_conditions.insert(label.clone(), *value); } RuntimeEffect::AppendEventRecord { record } => { staged_event_graph_mutations.push(EventGraphMutation::Append((**record).clone())); summary.appended_record_ids.push(record.record_id); } RuntimeEffect::ActivateEventRecord { record_id } => { staged_event_graph_mutations.push(EventGraphMutation::Activate { record_id: *record_id, }); summary.activated_record_ids.push(*record_id); } RuntimeEffect::DeactivateEventRecord { record_id } => { staged_event_graph_mutations.push(EventGraphMutation::Deactivate { record_id: *record_id, }); summary.deactivated_record_ids.push(*record_id); } RuntimeEffect::RemoveEventRecord { record_id } => { staged_event_graph_mutations.push(EventGraphMutation::Remove { record_id: *record_id, }); summary.removed_record_ids.push(*record_id); } } summary.applied_effect_count += 1; } Ok(summary) } fn commit_staged_event_graph_mutations( state: &mut RuntimeState, staged_event_graph_mutations: &[EventGraphMutation], ) -> Result<(), String> { for mutation in staged_event_graph_mutations { match mutation { EventGraphMutation::Append(record) => { if state .event_runtime_records .iter() .any(|existing| existing.record_id == record.record_id) { return Err(format!( "cannot append duplicate event record_id {}", record.record_id )); } state .event_runtime_records .push(record.clone().into_runtime_record()); } EventGraphMutation::Activate { record_id } => { let record = state .event_runtime_records .iter_mut() .find(|record| record.record_id == *record_id) .ok_or_else(|| { format!("cannot activate missing event record_id {record_id}") })?; record.active = true; } EventGraphMutation::Deactivate { record_id } => { let record = state .event_runtime_records .iter_mut() .find(|record| record.record_id == *record_id) .ok_or_else(|| { format!("cannot deactivate missing event record_id {record_id}") })?; record.active = false; } EventGraphMutation::Remove { record_id } => { let index = state .event_runtime_records .iter() .position(|record| record.record_id == *record_id) .ok_or_else(|| format!("cannot remove missing event record_id {record_id}"))?; state.event_runtime_records.remove(index); } } } state.validate() } fn evaluate_record_conditions( state: &RuntimeState, conditions: &[RuntimeCondition], ) -> Result, String> { if conditions.is_empty() { return Ok(Some(ResolvedConditionContext::default())); } let mut company_matches: Option> = None; let mut player_matches: Option> = None; let mut chairman_matches: Option> = None; for condition in conditions { match condition { RuntimeCondition::WorldVariableThreshold { index, comparator, value, } => { let actual = state .world_runtime_variables .get(index) .copied() .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::CompanyNumericThreshold { target, metric, comparator, value, } => { let resolved = resolve_company_target_ids( state, target, &ResolvedConditionContext::default(), )?; let matching = resolved .into_iter() .filter(|company_id| { state .companies .iter() .find(|company| company.company_id == *company_id) .is_some_and(|company| { compare_condition_value( company_metric_value(state, company, *metric), *comparator, *value, ) }) }) .collect::>(); if matching.is_empty() { return Ok(None); } intersect_company_matches(&mut company_matches, matching); if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { return Ok(None); } } RuntimeCondition::CompanyVariableThreshold { target, index, comparator, value, } => { let resolved = resolve_company_target_ids( state, target, &ResolvedConditionContext::default(), )?; let matching = resolved .into_iter() .filter(|company_id| { let actual = state .company_runtime_variables .get(company_id) .and_then(|vars| vars.get(index)) .copied() .unwrap_or(0); compare_condition_value(actual, *comparator, *value) }) .collect::>(); if matching.is_empty() { return Ok(None); } intersect_company_matches(&mut company_matches, matching); if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { return Ok(None); } } RuntimeCondition::TerritoryNumericThreshold { target, metric, comparator, value, } => { let territory_ids = resolve_territory_target_ids(state, target)?; let actual = territory_metric_value(state, &territory_ids, *metric); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::TerritoryVariableThreshold { target, index, comparator, value, } => { let territory_ids = resolve_territory_target_ids(state, target)?; let actual = territory_ids .iter() .map(|territory_id| { state .territory_runtime_variables .get(territory_id) .and_then(|vars| vars.get(index)) .copied() .unwrap_or(0) }) .sum::(); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::PlayerVariableThreshold { target, index, comparator, value, } => { let resolved = resolve_player_target_ids(state, target, &ResolvedConditionContext::default())?; let matching = resolved .into_iter() .filter(|player_id| { let actual = state .player_runtime_variables .get(player_id) .and_then(|vars| vars.get(index)) .copied() .unwrap_or(0); compare_condition_value(actual, *comparator, *value) }) .collect::>(); if matching.is_empty() { return Ok(None); } intersect_player_matches(&mut player_matches, matching); if player_matches.as_ref().is_some_and(BTreeSet::is_empty) { return Ok(None); } } RuntimeCondition::ChairmanNumericThreshold { target, metric, comparator, value, } => { let resolved = resolve_chairman_target_ids( state, target, &ResolvedConditionContext::default(), )?; let matching = resolved .into_iter() .filter(|profile_id| { state .chairman_profiles .iter() .find(|profile| profile.profile_id == *profile_id) .is_some_and(|profile| { compare_condition_value( chairman_metric_value(profile, *metric), *comparator, *value, ) }) }) .collect::>(); if matching.is_empty() { return Ok(None); } intersect_chairman_matches(&mut chairman_matches, matching); if chairman_matches.as_ref().is_some_and(BTreeSet::is_empty) { return Ok(None); } } RuntimeCondition::CompanyTerritoryNumericThreshold { target, territory, metric, comparator, value, } => { let territory_ids = resolve_territory_target_ids(state, territory)?; let resolved = resolve_company_target_ids( state, target, &ResolvedConditionContext::default(), )?; let matching = resolved .into_iter() .filter(|company_id| { compare_condition_value( company_territory_metric_value( state, *company_id, &territory_ids, *metric, ), *comparator, *value, ) }) .collect::>(); if matching.is_empty() { return Ok(None); } intersect_company_matches(&mut company_matches, matching); if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { return Ok(None); } } RuntimeCondition::SpecialConditionThreshold { label, comparator, value, } => { let actual = state .special_conditions .get(label) .copied() .map(i64::from) .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::CandidateAvailabilityThreshold { name, comparator, value, } => { let actual = state .candidate_availability .get(name) .copied() .map(i64::from) .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::NamedLocomotiveAvailabilityThreshold { name, comparator, value, } => { let actual = state .named_locomotive_availability .get(name) .copied() .map(i64::from) .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::NamedLocomotiveCostThreshold { name, comparator, value, } => { let actual = state .named_locomotive_cost .get(name) .copied() .map(i64::from) .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::CargoProductionSlotThreshold { slot, comparator, value, .. } => { let actual = state .cargo_production_overrides .get(slot) .copied() .map(i64::from) .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => { let actual = state .cargo_production_overrides .values() .copied() .map(i64::from) .sum::(); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::FactoryProductionTotalThreshold { comparator, value } => { let actual = cargo_production_total_for_class(state, RuntimeCargoClass::Factory); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value } => { let actual = cargo_production_total_for_class(state, RuntimeCargoClass::FarmMine); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value } => { let actual = cargo_production_total_for_class(state, RuntimeCargoClass::Other); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => { let actual = state .world_restore .limited_track_building_amount .map(i64::from) .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => { let actual = state .world_restore .territory_access_cost .map(i64::from) .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => { let actual = state .world_restore .economic_status_code .map(i64::from) .unwrap_or(0); if !compare_condition_value(actual, *comparator, *value) { return Ok(None); } } RuntimeCondition::WorldFlagEquals { key, value } => { let actual = state.world_flags.get(key).copied().unwrap_or(false); if actual != *value { return Ok(None); } } } } Ok(Some(ResolvedConditionContext { matching_company_ids: company_matches.unwrap_or_default(), matching_player_ids: player_matches.unwrap_or_default(), matching_chairman_profile_ids: chairman_matches.unwrap_or_default(), })) } fn intersect_company_matches(company_matches: &mut Option>, next: BTreeSet) { match company_matches { Some(existing) => { existing.retain(|company_id| next.contains(company_id)); } None => { *company_matches = Some(next); } } } fn intersect_player_matches(player_matches: &mut Option>, next: BTreeSet) { match player_matches { Some(existing) => { existing.retain(|player_id| next.contains(player_id)); } None => { *player_matches = Some(next); } } } fn intersect_chairman_matches(chairman_matches: &mut Option>, next: BTreeSet) { match chairman_matches { Some(existing) => { existing.retain(|profile_id| next.contains(profile_id)); } None => { *chairman_matches = Some(next); } } } fn resolve_company_target_ids( state: &RuntimeState, target: &RuntimeCompanyTarget, condition_context: &ResolvedConditionContext, ) -> Result, String> { match target { RuntimeCompanyTarget::AllActive => Ok(state .companies .iter() .filter(|company| company.active) .map(|company| company.company_id) .collect()), RuntimeCompanyTarget::Ids { ids } => { let known_ids = state .companies .iter() .map(|company| company.company_id) .collect::>(); for company_id in ids { if !known_ids.contains(company_id) { return Err(format!("target references unknown company_id {company_id}")); } } Ok(ids.clone()) } RuntimeCompanyTarget::HumanCompanies => { if state .companies .iter() .any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown) { return Err( "target requires company role context but at least one company has unknown controller_kind" .to_string(), ); } Ok(state .companies .iter() .filter(|company| { company.active && company.controller_kind == RuntimeCompanyControllerKind::Human }) .map(|company| company.company_id) .collect()) } RuntimeCompanyTarget::AiCompanies => { if state .companies .iter() .any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown) { return Err( "target requires company role context but at least one company has unknown controller_kind" .to_string(), ); } Ok(state .companies .iter() .filter(|company| { company.active && company.controller_kind == RuntimeCompanyControllerKind::Ai }) .map(|company| company.company_id) .collect()) } RuntimeCompanyTarget::SelectedCompany => { let selected_company_id = state .selected_company_id .ok_or_else(|| "target requires selected_company_id context".to_string())?; if state .companies .iter() .any(|company| company.company_id == selected_company_id && company.active) { Ok(vec![selected_company_id]) } else { Err( "target requires selected_company_id to reference an active company" .to_string(), ) } } RuntimeCompanyTarget::ConditionTrueCompany => { if condition_context.matching_company_ids.is_empty() { Err("target requires condition-evaluation context".to_string()) } else { Ok(condition_context .matching_company_ids .iter() .copied() .collect()) } } } } fn resolve_player_target_ids( state: &RuntimeState, target: &RuntimePlayerTarget, condition_context: &ResolvedConditionContext, ) -> Result, String> { match target { RuntimePlayerTarget::AllActive => Ok(state .players .iter() .filter(|player| player.active) .map(|player| player.player_id) .collect()), RuntimePlayerTarget::Ids { ids } => { let known_ids = state .players .iter() .map(|player| player.player_id) .collect::>(); for player_id in ids { if !known_ids.contains(player_id) { return Err(format!("target references unknown player_id {player_id}")); } } Ok(ids.clone()) } RuntimePlayerTarget::HumanPlayers => { if state .players .iter() .any(|player| player.controller_kind == RuntimeCompanyControllerKind::Unknown) { return Err( "target requires player role context but at least one player has unknown controller_kind" .to_string(), ); } Ok(state .players .iter() .filter(|player| { player.active && player.controller_kind == RuntimeCompanyControllerKind::Human }) .map(|player| player.player_id) .collect()) } RuntimePlayerTarget::AiPlayers => { if state .players .iter() .any(|player| player.controller_kind == RuntimeCompanyControllerKind::Unknown) { return Err( "target requires player role context but at least one player has unknown controller_kind" .to_string(), ); } Ok(state .players .iter() .filter(|player| { player.active && player.controller_kind == RuntimeCompanyControllerKind::Ai }) .map(|player| player.player_id) .collect()) } RuntimePlayerTarget::SelectedPlayer => { let selected_player_id = state .selected_player_id .ok_or_else(|| "target requires selected_player_id context".to_string())?; if state .players .iter() .any(|player| player.player_id == selected_player_id && player.active) { Ok(vec![selected_player_id]) } else { Err("target requires selected_player_id to reference an active player".to_string()) } } RuntimePlayerTarget::ConditionTruePlayer => { if condition_context.matching_player_ids.is_empty() { Err("target requires player condition-evaluation context".to_string()) } else { Ok(condition_context .matching_player_ids .iter() .copied() .collect()) } } } } fn resolve_chairman_target_ids( state: &RuntimeState, target: &RuntimeChairmanTarget, condition_context: &ResolvedConditionContext, ) -> Result, String> { match target { RuntimeChairmanTarget::AllActive => Ok(state .chairman_profiles .iter() .filter(|profile| profile.active) .map(|profile| profile.profile_id) .collect()), RuntimeChairmanTarget::HumanChairmen => Ok(state .chairman_profiles .iter() .filter(|profile| { chairman_profile_matches_company_controller_kind( state, profile, RuntimeCompanyControllerKind::Human, ) }) .map(|profile| profile.profile_id) .collect()), RuntimeChairmanTarget::AiChairmen => Ok(state .chairman_profiles .iter() .filter(|profile| { chairman_profile_matches_company_controller_kind( state, profile, RuntimeCompanyControllerKind::Ai, ) }) .map(|profile| profile.profile_id) .collect()), RuntimeChairmanTarget::Ids { ids } => { let known_ids = state .chairman_profiles .iter() .map(|profile| profile.profile_id) .collect::>(); for profile_id in ids { if !known_ids.contains(profile_id) { return Err(format!( "target references unknown chairman profile_id {profile_id}" )); } } Ok(ids.clone()) } RuntimeChairmanTarget::SelectedChairman => { let selected_profile_id = state.selected_chairman_profile_id.ok_or_else(|| { "target requires selected_chairman_profile_id context".to_string() })?; if state .chairman_profiles .iter() .any(|profile| profile.profile_id == selected_profile_id && profile.active) { Ok(vec![selected_profile_id]) } else { Err( "target requires selected_chairman_profile_id to reference an active chairman profile" .to_string(), ) } } RuntimeChairmanTarget::ConditionTrueChairman => { if condition_context.matching_chairman_profile_ids.is_empty() { Err("target requires chairman condition-evaluation context".to_string()) } else { Ok(condition_context .matching_chairman_profile_ids .iter() .copied() .collect()) } } } } fn chairman_profile_matches_company_controller_kind( state: &RuntimeState, profile: &crate::RuntimeChairmanProfile, controller_kind: RuntimeCompanyControllerKind, ) -> bool { profile.active && profile .linked_company_id .and_then(|company_id| { state .companies .iter() .find(|company| company.company_id == company_id) }) .is_some_and(|company| company.controller_kind == controller_kind) } fn resolve_territory_target_ids( state: &RuntimeState, target: &RuntimeTerritoryTarget, ) -> Result, String> { match target { RuntimeTerritoryTarget::AllTerritories => Ok(state .territories .iter() .map(|territory| territory.territory_id) .collect()), RuntimeTerritoryTarget::Ids { ids } => { let known_ids = state .territories .iter() .map(|territory| territory.territory_id) .collect::>(); for territory_id in ids { if !known_ids.contains(territory_id) { return Err(format!( "territory target references unknown territory_id {territory_id}" )); } } Ok(ids.clone()) } } } fn company_metric_value( state: &RuntimeState, company: &crate::RuntimeCompany, metric: RuntimeCompanyMetric, ) -> i64 { match metric { RuntimeCompanyMetric::CurrentCash => company.current_cash, RuntimeCompanyMetric::TotalDebt => company.debt as i64, RuntimeCompanyMetric::CreditRating => { runtime_company_credit_rating(state, company.company_id).unwrap_or(0) } RuntimeCompanyMetric::PrimeRate => { runtime_company_prime_rate(state, company.company_id).unwrap_or(0) } RuntimeCompanyMetric::BookValuePerShare => { runtime_company_book_value_per_share(state, company.company_id).unwrap_or(0) } RuntimeCompanyMetric::InvestorConfidence => { runtime_company_investor_confidence(state, company.company_id).unwrap_or(0) } RuntimeCompanyMetric::ManagementAttitude => { runtime_company_management_attitude(state, company.company_id).unwrap_or(0) } RuntimeCompanyMetric::TrackPiecesTotal => i64::from(company.track_piece_counts.total), RuntimeCompanyMetric::TrackPiecesSingle => i64::from(company.track_piece_counts.single), RuntimeCompanyMetric::TrackPiecesDouble => i64::from(company.track_piece_counts.double), RuntimeCompanyMetric::TrackPiecesTransition => { i64::from(company.track_piece_counts.transition) } RuntimeCompanyMetric::TrackPiecesElectric => i64::from(company.track_piece_counts.electric), RuntimeCompanyMetric::TrackPiecesNonElectric => { i64::from(company.track_piece_counts.non_electric) } } } fn chairman_metric_value( profile: &crate::RuntimeChairmanProfile, metric: RuntimeChairmanMetric, ) -> i64 { match metric { RuntimeChairmanMetric::CurrentCash => profile.current_cash, RuntimeChairmanMetric::HoldingsValueTotal => profile.holdings_value_total, RuntimeChairmanMetric::NetWorthTotal => profile.net_worth_total, RuntimeChairmanMetric::PurchasingPowerTotal => profile.purchasing_power_total, } } fn territory_metric_value( state: &RuntimeState, territory_ids: &[u32], metric: RuntimeTerritoryMetric, ) -> i64 { state .territories .iter() .filter(|territory| territory_ids.contains(&territory.territory_id)) .map(|territory| { track_piece_metric_value( territory.track_piece_counts, territory_metric_to_track_metric(metric), ) }) .sum() } fn company_territory_metric_value( state: &RuntimeState, company_id: u32, territory_ids: &[u32], metric: RuntimeTrackMetric, ) -> i64 { state .company_territory_track_piece_counts .iter() .filter(|entry| { entry.company_id == company_id && territory_ids.contains(&entry.territory_id) }) .map(|entry| track_piece_metric_value(entry.track_piece_counts, metric)) .sum() } fn track_piece_metric_value(counts: RuntimeTrackPieceCounts, metric: RuntimeTrackMetric) -> i64 { match metric { RuntimeTrackMetric::Total => i64::from(counts.total), RuntimeTrackMetric::Single => i64::from(counts.single), RuntimeTrackMetric::Double => i64::from(counts.double), RuntimeTrackMetric::Transition => i64::from(counts.transition), RuntimeTrackMetric::Electric => i64::from(counts.electric), RuntimeTrackMetric::NonElectric => i64::from(counts.non_electric), } } fn territory_metric_to_track_metric(metric: RuntimeTerritoryMetric) -> RuntimeTrackMetric { match metric { RuntimeTerritoryMetric::TrackPiecesTotal => RuntimeTrackMetric::Total, RuntimeTerritoryMetric::TrackPiecesSingle => RuntimeTrackMetric::Single, RuntimeTerritoryMetric::TrackPiecesDouble => RuntimeTrackMetric::Double, RuntimeTerritoryMetric::TrackPiecesTransition => RuntimeTrackMetric::Transition, RuntimeTerritoryMetric::TrackPiecesElectric => RuntimeTrackMetric::Electric, RuntimeTerritoryMetric::TrackPiecesNonElectric => RuntimeTrackMetric::NonElectric, } } fn compare_condition_value( actual: i64, comparator: RuntimeConditionComparator, expected: i64, ) -> bool { match comparator { RuntimeConditionComparator::Ge => actual >= expected, RuntimeConditionComparator::Le => actual <= expected, RuntimeConditionComparator::Gt => actual > expected, RuntimeConditionComparator::Lt => actual < expected, RuntimeConditionComparator::Eq => actual == expected, RuntimeConditionComparator::Ne => actual != expected, } } fn cargo_production_total_for_class(state: &RuntimeState, cargo_class: RuntimeCargoClass) -> i64 { state .cargo_catalog .iter() .filter(|entry| entry.cargo_class == cargo_class) .filter_map(|entry| state.cargo_production_overrides.get(&entry.slot_id)) .copied() .map(i64::from) .sum() } fn apply_u64_delta(current: u64, delta: i64, company_id: u32) -> Result { if delta >= 0 { current .checked_add(delta as u64) .ok_or_else(|| format!("company_id {company_id} debt adjustment overflow")) } else { current .checked_sub(delta.unsigned_abs()) .ok_or_else(|| format!("company_id {company_id} debt adjustment underflow")) } } fn retire_matching_trains( trains: &mut [crate::RuntimeTrain], company_ids: Option<&Vec>, territory_ids: Option<&Vec>, locomotive_name: Option<&str>, ) { for train in trains.iter_mut() { if !train.active || train.retired { continue; } if company_ids.is_some_and(|company_ids| !company_ids.contains(&train.owner_company_id)) { continue; } if territory_ids.is_some_and(|territory_ids| { !train .territory_id .is_some_and(|territory_id| territory_ids.contains(&territory_id)) }) { continue; } if locomotive_name.is_some_and(|name| train.locomotive_name.as_deref() != Some(name)) { continue; } train.active = false; train.retired = true; } } fn set_company_territory_access_pairs( access_entries: &mut Vec, company_ids: &[u32], territory_ids: &[u32], value: bool, ) { if value { for company_id in company_ids { for territory_id in territory_ids { if !access_entries.iter().any(|entry| { entry.company_id == *company_id && entry.territory_id == *territory_id }) { access_entries.push(crate::RuntimeCompanyTerritoryAccess { company_id: *company_id, territory_id: *territory_id, }); } } } } else { access_entries.retain(|entry| { !(company_ids.contains(&entry.company_id) && territory_ids.contains(&entry.territory_id)) }); } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use super::*; use crate::{ CalendarPoint, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePlayer, RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState, }; fn state() -> RuntimeState { 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, controller_kind: RuntimeCompanyControllerKind::Unknown, current_cash: 10, 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, }], 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(), } } #[test] fn advances_to_target() { let mut state = state(); let result = execute_step_command( &mut state, &StepCommand::AdvanceTo { calendar: CalendarPoint { year: 1830, month_slot: 0, phase_slot: 0, tick_slot: 5, }, }, ) .expect("advance_to should succeed"); assert_eq!(result.steps_executed, 5); assert_eq!(state.calendar.tick_slot, 5); } #[test] fn rejects_backward_target() { let mut state = state(); state.calendar.tick_slot = 3; let result = execute_step_command( &mut state, &StepCommand::AdvanceTo { calendar: CalendarPoint { year: 1830, month_slot: 0, phase_slot: 0, tick_slot: 2, }, }, ); assert!(result.is_err()); } #[test] fn services_periodic_trigger_order_and_dirty_rerun() { let mut state = RuntimeState { event_runtime_records: vec![ RuntimeEventRecord { record_id: 1, trigger_kind: 1, active: true, service_count: 0, marks_collection_dirty: true, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetWorldFlag { key: "runtime.effect_fired".to_string(), value: true, }], }, RuntimeEventRecord { record_id: 2, trigger_kind: 4, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::AdjustCompanyCash { target: RuntimeCompanyTarget::AllActive, delta: 5, }], }, RuntimeEventRecord { record_id: 3, trigger_kind: 0x0a, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetSpecialCondition { label: "Dirty rerun fired".to_string(), value: 1, }], }, ], ..state() }; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary service should succeed"); assert_eq!(result.steps_executed, 0); assert_eq!(state.service_state.periodic_boundary_calls, 1); assert_eq!(state.service_state.total_event_record_services, 3); assert_eq!(state.service_state.dirty_rerun_count, 1); assert_eq!(state.event_runtime_records[0].service_count, 1); assert_eq!(state.event_runtime_records[1].service_count, 1); assert_eq!(state.event_runtime_records[2].service_count, 1); assert_eq!(state.world_flags.get("runtime.effect_fired"), Some(&true)); assert_eq!(state.companies[0].current_cash, 15); assert_eq!(state.special_conditions.get("Dirty rerun fired"), Some(&1)); assert_eq!( state.service_state.trigger_dispatch_counts.get(&1), Some(&1) ); assert_eq!( state.service_state.trigger_dispatch_counts.get(&4), Some(&1) ); assert_eq!( state.service_state.trigger_dispatch_counts.get(&0x0a), Some(&1) ); assert_eq!(result.service_events.len(), 8); assert_eq!(result.service_events[0].applied_effect_count, 1); assert_eq!( result .service_events .iter() .find(|event| event.trigger_kind == Some(4)) .expect("trigger kind 4 event should be present") .applied_effect_count, 1 ); assert_eq!( result .service_events .iter() .find(|event| event.trigger_kind == Some(0x0a)) .expect("trigger kind 0x0a event should be present") .applied_effect_count, 1 ); assert_eq!( result .service_events .iter() .find(|event| event.trigger_kind == Some(4)) .expect("trigger kind 4 event should be present") .mutated_company_ids, vec![1] ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy") ); } #[test] fn periodic_boundary_applies_dividend_adjustment_from_annual_finance_policy() { 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 ]; let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; bits[index] = value.to_bits(); }; let write_prior_year_value = |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; bits[index] = value.to_bits(); }; write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0); write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0); write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0); let mut state = RuntimeState { calendar: crate::CalendarPoint { year: 1845, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_flags: BTreeMap::new(), save_profile: crate::RuntimeSaveProfileState::default(), world_restore: crate::RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1845), partial_year_progress_raw_u8: Some(0x0c), dividend_policy_raw_u8: Some(0), dividend_adjustment_allowed: Some(true), stock_issue_and_buyback_policy_raw_u8: Some(0), stock_issue_and_buyback_allowed: Some(true), bond_issue_and_repayment_policy_raw_u8: Some(0), bond_issue_and_repayment_allowed: Some(true), bankruptcy_policy_raw_u8: Some(0), bankruptcy_allowed: Some(true), building_density_growth_setting_raw_u32: Some(1), ..crate::RuntimeWorldRestoreState::default() }, metadata: BTreeMap::new(), companies: vec![crate::RuntimeCompany { company_id: 21, controller_kind: crate::RuntimeCompanyControllerKind::Unknown, current_cash: 0, debt: 0, credit_rating_score: None, prime_rate: None, track_piece_counts: crate::RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, linked_chairman_profile_id: Some(7), book_value_per_share: 0, investor_confidence: 0, management_attitude: 0, takeover_cooldown_year: None, merger_cooldown_year: None, }], selected_company_id: Some(21), players: Vec::new(), selected_player_id: None, chairman_profiles: vec![crate::RuntimeChairmanProfile { profile_id: 7, name: "Chairman Seven".to_string(), active: true, current_cash: 0, linked_company_id: Some(21), company_holdings: BTreeMap::from([(21, 9_500)]), holdings_value_total: 0, net_worth_total: 0, purchasing_power_total: 0, }], 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: crate::RuntimeServiceState { company_market_state: BTreeMap::from([( 21, crate::RuntimeCompanyMarketState { outstanding_shares: 10_000, founding_year: 1840, last_dividend_year: 1844, year_stat_family_qword_bits, direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( 0x33f, 0.4f32.to_bits(), )]), ..crate::RuntimeCompanyMarketState::default() }, )]), ..crate::RuntimeServiceState::default() }, }; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary should apply annual finance policy"); assert_eq!(state.service_state.annual_finance_service_calls, 1); assert_eq!( state.service_state.annual_dividend_adjustment_commit_count, 1 ); assert_eq!( state.service_state.annual_finance_last_actions.get(&21), Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment) ); assert_eq!( state.service_state.company_market_state[&21] .direct_control_transfer_float_fields_raw_u32 .get(&0x33f), Some(&1.8f32.to_bits()) ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy" && event.applied_effect_count == 1 && event.mutated_company_ids == vec![21]) ); } #[test] fn periodic_boundary_applies_stock_issue_from_annual_finance_policy() { let mut year_stat_family_qword_bits = vec![ 0u64; (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * 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] = 250_000.0f64.to_bits(); let current_issue_calendar_word = 0x0101_0725; let current_issue_calendar_word_2 = 0x0001_0001; let prior_issue_calendar_word = 0x0101_0701; let prior_issue_calendar_word_2 = 0x0001_0001; let mut state = RuntimeState { calendar: crate::CalendarPoint { year: 1845, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_flags: BTreeMap::new(), save_profile: crate::RuntimeSaveProfileState::default(), world_restore: crate::RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1845), partial_year_progress_raw_u8: Some(0x0c), current_calendar_tuple_word_raw_u32: Some(0x0101_0726), current_calendar_tuple_word_2_raw_u32: Some(0x0001_0001), absolute_counter_raw_u32: Some(885_911_040), stock_issue_and_buyback_policy_raw_u8: Some(0), stock_issue_and_buyback_allowed: Some(true), bond_issue_and_repayment_policy_raw_u8: Some(0), bond_issue_and_repayment_allowed: Some(true), bankruptcy_policy_raw_u8: Some(0), bankruptcy_allowed: Some(true), issue_37_value: Some(5.0f32.to_bits()), issue_38_value: Some(2), ..crate::RuntimeWorldRestoreState::default() }, metadata: BTreeMap::new(), companies: vec![crate::RuntimeCompany { company_id: 22, controller_kind: crate::RuntimeCompanyControllerKind::Unknown, current_cash: 0, debt: 0, credit_rating_score: None, prime_rate: None, track_piece_counts: crate::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, }], selected_company_id: Some(22), 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: crate::RuntimeServiceState { world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], company_market_state: BTreeMap::from([( 22, crate::RuntimeCompanyMarketState { outstanding_shares: 20_000, bond_count: 2, highest_coupon_live_bond_principal: Some(300_000), founding_year: 1840, cached_share_price_raw_u32: 35.0f32.to_bits(), recent_per_share_cache_absolute_counter: 885_911_040, recent_per_share_cached_value_bits: 34.0f64.to_bits(), current_issue_calendar_word, current_issue_calendar_word_2, prior_issue_calendar_word, prior_issue_calendar_word_2, live_bond_slots: vec![ crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 300_000, maturity_year: 0, coupon_rate_raw_u32: 0.11f32.to_bits(), }, crate::RuntimeCompanyBondSlot { slot_index: 1, principal: 200_000, maturity_year: 0, coupon_rate_raw_u32: 0.07f32.to_bits(), }, ], direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( 0x32f, 30.0f32.to_bits(), )]), year_stat_family_qword_bits, ..crate::RuntimeCompanyMarketState::default() }, )]), ..crate::RuntimeServiceState::default() }, }; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary should apply annual stock issue policy"); assert_eq!( state.service_state.annual_finance_last_actions.get(&22), Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::StockIssue) ); assert_eq!(state.companies[0].current_cash, 390_000); assert_eq!( state.service_state.company_market_state[&22].outstanding_shares, 24_000 ); assert_eq!( state.service_state.company_market_state[&22].prior_issue_calendar_word, current_issue_calendar_word ); assert_eq!( state.service_state.company_market_state[&22].prior_issue_calendar_word_2, current_issue_calendar_word_2 ); assert_eq!( state.service_state.company_market_state[&22].current_issue_calendar_word, 0x0101_0726 ); assert_eq!( state.service_state.company_market_state[&22].current_issue_calendar_word_2, 0x0001_0001 ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy" && event.applied_effect_count == 1 && event.mutated_company_ids == vec![22]) ); } #[test] fn periodic_boundary_applies_stock_repurchase_from_annual_finance_policy() { let mut year_stat_family_qword_bits = vec![ 0u64; (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * 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] = 1_600_000.0f64.to_bits(); let base_state = RuntimeState { calendar: crate::CalendarPoint { year: 1845, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_flags: BTreeMap::new(), save_profile: crate::RuntimeSaveProfileState::default(), world_restore: crate::RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1845), partial_year_progress_raw_u8: Some(0x0c), absolute_counter_raw_u32: Some(1_000), stock_issue_and_buyback_policy_raw_u8: Some(0), stock_issue_and_buyback_allowed: Some(true), bond_issue_and_repayment_policy_raw_u8: Some(0), bond_issue_and_repayment_allowed: Some(true), bankruptcy_policy_raw_u8: Some(0), bankruptcy_allowed: Some(true), building_density_growth_setting_raw_u32: Some(0), ..crate::RuntimeWorldRestoreState::default() }, metadata: BTreeMap::new(), companies: vec![crate::RuntimeCompany { company_id: 23, controller_kind: crate::RuntimeCompanyControllerKind::Unknown, current_cash: 0, debt: 0, credit_rating_score: None, prime_rate: None, track_piece_counts: crate::RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, linked_chairman_profile_id: Some(8), book_value_per_share: 0, investor_confidence: 0, management_attitude: 0, takeover_cooldown_year: None, merger_cooldown_year: None, }], selected_company_id: Some(23), players: Vec::new(), selected_player_id: None, chairman_profiles: vec![crate::RuntimeChairmanProfile { profile_id: 8, name: "Chairman".to_string(), active: true, current_cash: 0, linked_company_id: Some(23), company_holdings: BTreeMap::from([(23, 9_000)]), holdings_value_total: 0, net_worth_total: 0, purchasing_power_total: 0, }], selected_chairman_profile_id: Some(8), 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: crate::RuntimeServiceState { world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], company_market_state: BTreeMap::from([( 23, crate::RuntimeCompanyMarketState { outstanding_shares: 10_000, founding_year: 1840, city_connection_latch: true, recent_per_share_cache_absolute_counter: 1_000, recent_per_share_cached_value_bits: 50.0f64.to_bits(), mutable_support_scalar_raw_u32: 0.0f32.to_bits(), young_company_support_scalar_raw_u32: 0.0f32.to_bits(), year_stat_family_qword_bits, ..crate::RuntimeCompanyMarketState::default() }, )]), ..crate::RuntimeServiceState::default() }, }; let pressured_share_price = crate::runtime::runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( &base_state, 23, 700, ) .expect("repurchase share price"); let expected_repurchase_total = crate::runtime::runtime_round_f64_to_i64(pressured_share_price * 1_000.0) .expect("repurchase total should round"); let mut state = base_state; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary should apply annual stock repurchase policy"); assert_eq!( state.service_state.annual_finance_last_actions.get(&23), Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase) ); assert_eq!( state.companies[0].current_cash, 1_600_000 - expected_repurchase_total ); assert_eq!( state.service_state.company_market_state[&23].outstanding_shares, 9_000 ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy" && event.applied_effect_count == 1 && event.mutated_company_ids == vec![23]) ); } #[test] fn periodic_boundary_applies_bond_issue_from_annual_finance_policy() { let mut year_stat_family_qword_bits = vec![ 0u64; (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * 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] = (-400_000.0f64).to_bits(); let mut state = RuntimeState { calendar: crate::CalendarPoint { year: 1845, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_flags: BTreeMap::new(), save_profile: crate::RuntimeSaveProfileState::default(), world_restore: crate::RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1845), partial_year_progress_raw_u8: Some(0x0c), bond_issue_and_repayment_policy_raw_u8: Some(0), bond_issue_and_repayment_allowed: Some(true), stock_issue_and_buyback_policy_raw_u8: Some(0), stock_issue_and_buyback_allowed: Some(true), bankruptcy_policy_raw_u8: Some(0), bankruptcy_allowed: Some(true), ..crate::RuntimeWorldRestoreState::default() }, metadata: BTreeMap::new(), companies: vec![crate::RuntimeCompany { company_id: 24, controller_kind: crate::RuntimeCompanyControllerKind::Unknown, current_cash: 0, debt: 0, credit_rating_score: Some(6), prime_rate: Some(5), track_piece_counts: crate::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, }], selected_company_id: Some(24), 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: crate::RuntimeServiceState { company_market_state: BTreeMap::from([( 24, crate::RuntimeCompanyMarketState { outstanding_shares: 10_000, founding_year: 1840, year_stat_family_qword_bits, ..crate::RuntimeCompanyMarketState::default() }, )]), ..crate::RuntimeServiceState::default() }, }; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary should apply annual bond issue policy"); assert_eq!( state.service_state.annual_finance_last_actions.get(&24), Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) ); assert_eq!(state.companies[0].current_cash, 100_000); assert_eq!(state.service_state.company_market_state[&24].bond_count, 1); assert_eq!( state.service_state.company_market_state[&24].largest_live_bond_principal, Some(500_000) ); assert_eq!( state.service_state.company_market_state[&24].highest_coupon_live_bond_principal, Some(500_000) ); assert_eq!( state.service_state.company_market_state[&24].live_bond_slots, vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 500_000, maturity_year: 1875, coupon_rate_raw_u32: 0.09f32.to_bits(), }] ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy" && event.applied_effect_count == 1 && event.mutated_company_ids == vec![24]) ); } #[test] fn periodic_boundary_retires_live_bonds_when_annual_bond_lane_needs_no_reissue() { 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 ]; let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; bits[index] = value.to_bits(); }; write_current_value( &mut year_stat_family_qword_bits, crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, 900_000.0, ); write_current_value(&mut year_stat_family_qword_bits, 0x12, -350_000.0); let mut state = crate::RuntimeState { calendar: crate::CalendarPoint { year: 1845, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_restore: crate::RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1845), partial_year_progress_raw_u8: Some(0x0c), bond_issue_and_repayment_policy_raw_u8: Some(0), bond_issue_and_repayment_allowed: Some(true), stock_issue_and_buyback_policy_raw_u8: Some(0), stock_issue_and_buyback_allowed: Some(true), bankruptcy_policy_raw_u8: Some(0), bankruptcy_allowed: Some(true), ..crate::RuntimeWorldRestoreState::default() }, world_flags: BTreeMap::new(), save_profile: crate::RuntimeSaveProfileState::default(), metadata: BTreeMap::new(), companies: vec![crate::RuntimeCompany { company_id: 25, current_cash: 900_000, debt: 350_000, active: true, credit_rating_score: Some(7), prime_rate: Some(6), controller_kind: crate::RuntimeCompanyControllerKind::Unknown, linked_chairman_profile_id: None, available_track_laying_capacity: None, book_value_per_share: 0, investor_confidence: 0, management_attitude: 0, takeover_cooldown_year: None, merger_cooldown_year: None, track_piece_counts: crate::RuntimeTrackPieceCounts::default(), }], selected_company_id: Some(25), 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: crate::RuntimeServiceState { company_market_state: BTreeMap::from([( 25, crate::RuntimeCompanyMarketState { bond_count: 2, live_bond_slots: vec![ crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 200_000, maturity_year: 1845, coupon_rate_raw_u32: 0.09f32.to_bits(), }, crate::RuntimeCompanyBondSlot { slot_index: 1, principal: 150_000, maturity_year: 1845, coupon_rate_raw_u32: 0.08f32.to_bits(), }, ], year_stat_family_qword_bits, ..crate::RuntimeCompanyMarketState::default() }, )]), ..crate::RuntimeServiceState::default() }, }; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary should apply annual bond repayment lane"); assert_eq!( state.service_state.annual_finance_last_actions.get(&25), Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) ); assert_eq!(state.companies[0].current_cash, 550_000); assert_eq!(state.companies[0].debt, 0); assert_eq!(state.service_state.company_market_state[&25].bond_count, 0); assert!( state.service_state.company_market_state[&25] .live_bond_slots .is_empty() ); assert_eq!( state.service_state.company_market_state[&25].largest_live_bond_principal, None ); assert_eq!( state.service_state.company_market_state[&25].highest_coupon_live_bond_principal, None ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy" && event.applied_effect_count == 1 && event.mutated_company_ids == vec![25]) ); } #[test] fn periodic_boundary_retires_then_reissues_exact_annual_bond_count() { 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 ]; let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; bits[index] = value.to_bits(); }; write_current_value( &mut year_stat_family_qword_bits, crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, -400_000.0, ); write_current_value(&mut year_stat_family_qword_bits, 0x12, -350_000.0); let mut state = crate::RuntimeState { calendar: crate::CalendarPoint { year: 1845, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_restore: crate::RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1845), partial_year_progress_raw_u8: Some(0x0c), bond_issue_and_repayment_policy_raw_u8: Some(0), bond_issue_and_repayment_allowed: Some(true), stock_issue_and_buyback_policy_raw_u8: Some(0), stock_issue_and_buyback_allowed: Some(true), bankruptcy_policy_raw_u8: Some(0), bankruptcy_allowed: Some(true), ..crate::RuntimeWorldRestoreState::default() }, world_flags: BTreeMap::new(), save_profile: crate::RuntimeSaveProfileState::default(), metadata: BTreeMap::new(), companies: vec![crate::RuntimeCompany { company_id: 26, current_cash: -400_000, debt: 350_000, active: true, credit_rating_score: Some(7), prime_rate: Some(6), controller_kind: crate::RuntimeCompanyControllerKind::Unknown, linked_chairman_profile_id: None, available_track_laying_capacity: None, book_value_per_share: 0, investor_confidence: 0, management_attitude: 0, takeover_cooldown_year: None, merger_cooldown_year: None, track_piece_counts: crate::RuntimeTrackPieceCounts::default(), }], selected_company_id: Some(26), 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: crate::RuntimeServiceState { company_market_state: BTreeMap::from([( 26, crate::RuntimeCompanyMarketState { bond_count: 2, linked_transit_latch: true, live_bond_slots: vec![ crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 200_000, maturity_year: 1845, coupon_rate_raw_u32: 0.09f32.to_bits(), }, crate::RuntimeCompanyBondSlot { slot_index: 1, principal: 150_000, maturity_year: 1845, coupon_rate_raw_u32: 0.08f32.to_bits(), }, ], year_stat_family_qword_bits, ..crate::RuntimeCompanyMarketState::default() }, )]), ..crate::RuntimeServiceState::default() }, }; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary should apply annual bond restructure lane"); assert_eq!( state.service_state.annual_finance_last_actions.get(&26), Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue) ); assert_eq!(state.companies[0].current_cash, 250_000); assert_eq!(state.companies[0].debt, 1_000_000); assert_eq!(state.service_state.company_market_state[&26].bond_count, 2); assert_eq!( state.service_state.company_market_state[&26].largest_live_bond_principal, Some(500_000) ); assert_eq!( state.service_state.company_market_state[&26].highest_coupon_live_bond_principal, Some(500_000) ); assert_eq!( state.service_state.company_market_state[&26].live_bond_slots, vec![ crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 500_000, maturity_year: 1875, coupon_rate_raw_u32: 0.09f32.to_bits(), }, crate::RuntimeCompanyBondSlot { slot_index: 1, principal: 500_000, maturity_year: 1875, coupon_rate_raw_u32: 0.09f32.to_bits(), }, ] ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy" && event.applied_effect_count == 1 && event.mutated_company_ids == vec![26]) ); } #[test] fn periodic_boundary_applies_creditor_pressure_bankruptcy_from_annual_finance_policy() { 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 ]; let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; bits[index] = value.to_bits(); }; let write_prior_year_value = |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; bits[index] = value.to_bits(); }; write_current_value( &mut year_stat_family_qword_bits, crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, -200_000.0, ); write_current_value(&mut year_stat_family_qword_bits, 0x12, -500_000.0); write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 95_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -125_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -115_000.0); let mut state = RuntimeState { calendar: crate::CalendarPoint { year: 1845, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_flags: BTreeMap::new(), save_profile: crate::RuntimeSaveProfileState::default(), world_restore: crate::RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1845), partial_year_progress_raw_u8: Some(0x0c), bankruptcy_policy_raw_u8: Some(0), bankruptcy_allowed: Some(true), ..crate::RuntimeWorldRestoreState::default() }, metadata: BTreeMap::new(), companies: vec![crate::RuntimeCompany { company_id: 31, controller_kind: crate::RuntimeCompanyControllerKind::Unknown, current_cash: 0, debt: 200_000, credit_rating_score: None, prime_rate: None, track_piece_counts: crate::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, }], selected_company_id: Some(31), players: Vec::new(), selected_player_id: None, chairman_profiles: Vec::new(), selected_chairman_profile_id: None, trains: vec![crate::RuntimeTrain { train_id: 88, owner_company_id: 31, territory_id: None, locomotive_name: Some("Mikado".to_string()), active: true, retired: false, }], 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: crate::RuntimeServiceState { company_market_state: BTreeMap::from([( 31, crate::RuntimeCompanyMarketState { outstanding_shares: 10_000, bond_count: 1, largest_live_bond_principal: Some(500_000), highest_coupon_live_bond_principal: Some(500_000), cached_share_price_raw_u32: 25.0f32.to_bits(), founding_year: 1841, last_bankruptcy_year: 1832, year_stat_family_qword_bits, live_bond_slots: vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 500_000, maturity_year: 0, coupon_rate_raw_u32: 0.08f32.to_bits(), }], ..crate::RuntimeCompanyMarketState::default() }, )]), ..crate::RuntimeServiceState::default() }, }; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary should apply creditor-pressure bankruptcy"); assert_eq!( state.service_state.annual_finance_last_actions.get(&31), Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy) ); assert_eq!(state.companies[0].current_cash, 0); assert_eq!(state.companies[0].debt, 250_000); assert!(state.companies[0].active); assert_eq!(state.selected_company_id, Some(31)); assert!(!state.trains[0].retired); assert_eq!( state.service_state.company_market_state[&31].last_bankruptcy_year, 1845 ); assert_eq!(state.service_state.company_market_state[&31].bond_count, 1); assert_eq!( state.service_state.company_market_state[&31].largest_live_bond_principal, Some(250_000) ); assert_eq!( state.service_state.company_market_state[&31].highest_coupon_live_bond_principal, Some(250_000) ); assert!( state.service_state.company_market_state[&31].live_bond_slots == vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 250_000, maturity_year: 0, coupon_rate_raw_u32: 0.08f32.to_bits(), }] ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy" && event.applied_effect_count == 1 && event.mutated_company_ids == vec![31]) ); } #[test] fn periodic_boundary_applies_deep_distress_bankruptcy_fallback_from_annual_finance_policy() { 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 ]; let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; bits[index] = value.to_bits(); }; let write_prior_year_value = |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; bits[index] = value.to_bits(); }; write_current_value( &mut year_stat_family_qword_bits, crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, -350_000.0, ); write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0); write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0); let mut state = RuntimeState { calendar: crate::CalendarPoint { year: 1845, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_flags: BTreeMap::new(), save_profile: crate::RuntimeSaveProfileState::default(), world_restore: crate::RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1845), partial_year_progress_raw_u8: Some(0x0c), bankruptcy_policy_raw_u8: Some(0), bankruptcy_allowed: Some(true), ..crate::RuntimeWorldRestoreState::default() }, metadata: BTreeMap::new(), companies: vec![crate::RuntimeCompany { company_id: 32, controller_kind: crate::RuntimeCompanyControllerKind::Unknown, current_cash: 0, debt: 50_000, credit_rating_score: None, prime_rate: None, track_piece_counts: crate::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, }], selected_company_id: Some(32), players: Vec::new(), selected_player_id: None, chairman_profiles: Vec::new(), selected_chairman_profile_id: None, trains: vec![crate::RuntimeTrain { train_id: 89, owner_company_id: 32, territory_id: None, locomotive_name: Some("Orca".to_string()), active: true, retired: false, }], 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: crate::RuntimeServiceState { company_market_state: BTreeMap::from([( 32, crate::RuntimeCompanyMarketState { outstanding_shares: 8_000, bond_count: 1, largest_live_bond_principal: Some(250_000), highest_coupon_live_bond_principal: Some(250_000), founding_year: 1841, last_bankruptcy_year: 1840, year_stat_family_qword_bits, live_bond_slots: vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 250_000, maturity_year: 0, coupon_rate_raw_u32: 0.07f32.to_bits(), }], ..crate::RuntimeCompanyMarketState::default() }, )]), ..crate::RuntimeServiceState::default() }, }; let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) .expect("periodic boundary should apply deep-distress bankruptcy fallback"); assert_eq!( state.service_state.annual_finance_last_actions.get(&32), Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback) ); assert_eq!(state.companies[0].current_cash, 0); assert_eq!(state.companies[0].debt, 125_000); assert!(state.companies[0].active); assert_eq!(state.selected_company_id, Some(32)); assert!(!state.trains[0].retired); assert_eq!( state.service_state.company_market_state[&32].last_bankruptcy_year, 1845 ); assert_eq!(state.service_state.company_market_state[&32].bond_count, 1); assert_eq!( state.service_state.company_market_state[&32].largest_live_bond_principal, Some(125_000) ); assert_eq!( state.service_state.company_market_state[&32].highest_coupon_live_bond_principal, Some(125_000) ); assert!( state.service_state.company_market_state[&32].live_bond_slots == vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 125_000, maturity_year: 0, coupon_rate_raw_u32: 0.07f32.to_bits(), }] ); assert!( result .service_events .iter() .any(|event| event.kind == "annual_finance_policy" && event.applied_effect_count == 1 && event.mutated_company_ids == vec![32]) ); } #[test] fn applies_company_effects_for_specific_targets() { let mut state = RuntimeState { companies: vec![ RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Unknown, current_cash: 10, debt: 5, 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, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Unknown, current_cash: 20, debt: 8, 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: 10, 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![2] }, delta: 4, }, RuntimeEffect::AdjustCompanyDebt { target: RuntimeCompanyTarget::Ids { ids: vec![2] }, delta: -3, }, ], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("targeted company effects should succeed"); assert_eq!(state.companies[0].current_cash, 10); assert_eq!(state.companies[1].current_cash, 24); assert_eq!(state.companies[1].debt, 5); assert_eq!(result.service_events[0].applied_effect_count, 2); assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); } #[test] fn applies_named_locomotive_availability_effects() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 10, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![ RuntimeEffect::SetNamedLocomotiveAvailability { name: "Big Boy".to_string(), value: false, }, RuntimeEffect::SetNamedLocomotiveAvailability { name: "GP7".to_string(), value: true, }, ], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("named locomotive availability effects should succeed"); assert_eq!(state.named_locomotive_availability.get("Big Boy"), Some(&0)); assert_eq!(state.named_locomotive_availability.get("GP7"), Some(&1)); assert_eq!(result.service_events[0].applied_effect_count, 2); } #[test] fn applies_named_locomotive_cost_effects() { let mut state = RuntimeState { 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::SetNamedLocomotiveCost { name: "Big Boy".to_string(), value: 250000, }, RuntimeEffect::SetNamedLocomotiveCost { name: "GP7".to_string(), value: 175000, }, ], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("named locomotive cost effects should succeed"); assert_eq!(state.named_locomotive_cost.get("Big Boy"), Some(&250000)); assert_eq!(state.named_locomotive_cost.get("GP7"), Some(&175000)); assert_eq!(result.service_events[0].applied_effect_count, 2); } #[test] fn applies_scalar_named_locomotive_availability_effects() { let mut state = RuntimeState { 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::SetNamedLocomotiveAvailabilityValue { name: "Big Boy".to_string(), value: 42, }, RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name: "GP7".to_string(), value: 7, }, ], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("scalar named locomotive availability effects should succeed"); assert_eq!( state.named_locomotive_availability.get("Big Boy"), Some(&42) ); assert_eq!(state.named_locomotive_availability.get("GP7"), Some(&7)); assert_eq!(result.service_events[0].applied_effect_count, 2); } #[test] fn applies_world_scalar_override_effects() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 13, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![ RuntimeEffect::SetWorldScalarOverride { key: "world.build_stations_cost".to_string(), value: 350000, }, RuntimeEffect::SetCargoPriceOverride { target: RuntimeCargoPriceTarget::All, value: 180, }, RuntimeEffect::SetCargoPriceOverride { target: RuntimeCargoPriceTarget::Named { name: "Coal".to_string(), }, value: 95, }, RuntimeEffect::SetCargoProductionOverride { target: RuntimeCargoProductionTarget::Factory, value: 225, }, RuntimeEffect::SetCargoProductionOverride { target: RuntimeCargoProductionTarget::Named { name: "Corn".to_string(), }, value: 140, }, RuntimeEffect::SetCargoProductionSlot { slot: 1, value: 125, }, RuntimeEffect::SetTerritoryAccessCost { value: 750000 }, ], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("world scalar override effects should succeed"); assert_eq!( state .world_scalar_overrides .get("world.build_stations_cost"), Some(&350000) ); assert_eq!(state.all_cargo_price_override, Some(180)); assert_eq!(state.named_cargo_price_overrides.get("Coal"), Some(&95)); assert_eq!(state.factory_cargo_production_override, Some(225)); assert_eq!( state.named_cargo_production_overrides.get("Corn"), Some(&140) ); assert_eq!(state.cargo_production_overrides.get(&1), Some(&125)); assert_eq!(state.world_restore.territory_access_cost, Some(750000)); assert_eq!(result.service_events[0].applied_effect_count, 7); } #[test] fn applies_runtime_variable_effects() { let mut state = RuntimeState { companies: vec![ RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 10, 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, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 20, 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, }, ], players: vec![RuntimePlayer { player_id: 9, current_cash: 0, active: true, controller_kind: RuntimeCompanyControllerKind::Human, }], territories: vec![RuntimeTerritory { territory_id: 7, name: Some("North".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }], event_runtime_records: vec![RuntimeEventRecord { record_id: 14, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![ RuntimeEffect::SetWorldVariable { index: 1, value: -5, }, RuntimeEffect::SetCompanyVariable { target: RuntimeCompanyTarget::AllActive, index: 2, value: 17, }, RuntimeEffect::SetPlayerVariable { target: RuntimePlayerTarget::Ids { ids: vec![9] }, index: 3, value: 99, }, RuntimeEffect::SetTerritoryVariable { target: RuntimeTerritoryTarget::Ids { ids: vec![7] }, index: 4, value: 1234, }, ], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("runtime variable effects should succeed"); assert_eq!(state.world_runtime_variables.get(&1), Some(&-5)); assert_eq!( state .company_runtime_variables .get(&1) .and_then(|vars| vars.get(&2)), Some(&17) ); assert_eq!( state .company_runtime_variables .get(&2) .and_then(|vars| vars.get(&2)), Some(&17) ); assert_eq!( state .player_runtime_variables .get(&9) .and_then(|vars| vars.get(&3)), Some(&99) ); assert_eq!( state .territory_runtime_variables .get(&7) .and_then(|vars| vars.get(&4)), Some(&1234) ); assert_eq!(result.service_events[0].applied_effect_count, 4); } #[test] fn resolves_symbolic_company_targets() { let mut state = RuntimeState { companies: vec![ RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 10, 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, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 20, debt: 2, 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, }, ], selected_company_id: Some(1), 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::AdjustCompanyCash { target: RuntimeCompanyTarget::HumanCompanies, delta: 5, }], }, 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::AdjustCompanyDebt { target: RuntimeCompanyTarget::AiCompanies, delta: 3, }], }, RuntimeEventRecord { record_id: 13, 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::SelectedCompany, delta: 7, }], }, ], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("symbolic target effects should succeed"); assert_eq!(state.companies[0].current_cash, 22); assert_eq!(state.companies[0].debt, 0); assert_eq!(state.companies[1].current_cash, 20); assert_eq!(state.companies[1].debt, 5); assert_eq!(result.service_events[0].mutated_company_ids, vec![1, 2]); } #[test] fn rejects_selected_company_target_without_selection_context() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 14, 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::SelectedCompany, delta: 1, }], }], ..state() }; let error = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect_err("selected company target should require selection context"); assert!(error.contains("selected_company_id")); } #[test] fn rejects_human_or_ai_targets_without_role_context() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 15, 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::HumanCompanies, delta: 1, }], }], ..state() }; let error = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect_err("human target should require controller metadata"); assert!(error.contains("controller_kind")); } #[test] fn all_active_and_role_targets_exclude_inactive_companies() { let mut state = RuntimeState { companies: vec![ RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 10, debt: 1, 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, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 20, debt: 2, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: false, 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, }, RuntimeCompany { company_id: 3, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 30, debt: 3, 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: 16, 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::AllActive, delta: 5, }], }, RuntimeEventRecord { record_id: 17, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::AdjustCompanyDebt { target: RuntimeCompanyTarget::HumanCompanies, delta: 4, }], }, RuntimeEventRecord { record_id: 18, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::AdjustCompanyDebt { target: RuntimeCompanyTarget::AiCompanies, delta: 6, }], }, ], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("active-company filtering should succeed"); assert_eq!(state.companies[0].current_cash, 15); assert_eq!(state.companies[1].current_cash, 20); assert_eq!(state.companies[2].current_cash, 35); assert_eq!(state.companies[0].debt, 5); assert_eq!(state.companies[1].debt, 2); assert_eq!(state.companies[2].debt, 9); } #[test] fn deactivating_selected_company_clears_selection() { let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 10, debt: 0, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: Some(8), linked_chairman_profile_id: None, book_value_per_share: 0, investor_confidence: 0, management_attitude: 0, takeover_cooldown_year: None, merger_cooldown_year: None, }], selected_company_id: Some(1), event_runtime_records: vec![RuntimeEventRecord { record_id: 19, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::DeactivateCompany { target: RuntimeCompanyTarget::SelectedCompany, }], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("deactivate company effect should succeed"); assert!(!state.companies[0].active); assert_eq!(state.selected_company_id, None); assert_eq!(result.service_events[0].mutated_company_ids, vec![1]); } #[test] fn deactivating_selected_player_clears_selection() { let mut state = RuntimeState { players: vec![RuntimePlayer { player_id: 1, current_cash: 500, active: true, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_player_id: Some(1), event_runtime_records: vec![RuntimeEventRecord { record_id: 19, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::DeactivatePlayer { target: crate::RuntimePlayerTarget::SelectedPlayer, }], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("deactivate player effect should succeed"); assert!(!state.players[0].active); assert_eq!(state.selected_player_id, None); assert_eq!(result.service_events[0].mutated_player_ids, vec![1]); } #[test] fn sets_track_laying_capacity_for_resolved_targets() { let mut state = RuntimeState { companies: vec![ RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 10, 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, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 20, 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: 20, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { target: RuntimeCompanyTarget::Ids { ids: vec![2] }, value: Some(14), }], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("track capacity effect should succeed"); assert_eq!(state.companies[0].available_track_laying_capacity, None); assert_eq!(state.companies[1].available_track_laying_capacity, Some(14)); assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); } #[test] fn sets_and_clears_company_territory_access_for_resolved_targets() { let mut state = RuntimeState { companies: vec![ RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 10, 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, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 20, 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, }, ], territories: vec![ RuntimeTerritory { territory_id: 7, name: Some("Appalachia".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }, RuntimeTerritory { territory_id: 8, name: Some("Great Plains".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }, ], event_runtime_records: vec![ RuntimeEventRecord { record_id: 21, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: true, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetCompanyTerritoryAccess { target: RuntimeCompanyTarget::SelectedCompany, territory: RuntimeTerritoryTarget::Ids { ids: vec![7, 8] }, value: true, }], }, RuntimeEventRecord { record_id: 22, trigger_kind: 8, active: true, service_count: 0, marks_collection_dirty: false, one_shot: true, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetCompanyTerritoryAccess { target: RuntimeCompanyTarget::SelectedCompany, territory: RuntimeTerritoryTarget::Ids { ids: vec![8] }, value: false, }], }, ], selected_company_id: Some(1), ..state() }; let first = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("territory access grant should succeed"); assert_eq!( state.company_territory_access, vec![ crate::RuntimeCompanyTerritoryAccess { company_id: 1, territory_id: 7, }, crate::RuntimeCompanyTerritoryAccess { company_id: 1, territory_id: 8, }, ] ); assert_eq!(first.service_events[0].mutated_company_ids, vec![1]); execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 8 }, ) .expect("territory access clear should succeed"); assert_eq!( state.company_territory_access, vec![crate::RuntimeCompanyTerritoryAccess { company_id: 1, territory_id: 7, }] ); } #[test] fn rejects_condition_true_company_target_without_condition_context() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 16, 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::ConditionTrueCompany, delta: 1, }], }], ..state() }; let error = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect_err("condition-relative target should remain blocked"); assert!(error.contains("condition-evaluation context")); } #[test] fn evaluates_world_state_conditions_before_effects_run() { let mut state = RuntimeState { world_restore: RuntimeWorldRestoreState { economic_status_code: Some(3), ..RuntimeWorldRestoreState::default() }, world_flags: BTreeMap::from([( String::from("world.disable_stock_buying_and_selling"), true, )]), candidate_availability: BTreeMap::from([(String::from("Mogul"), 2)]), special_conditions: BTreeMap::from([(String::from("Use Wartime Cargos"), 1)]), event_runtime_records: vec![ RuntimeEventRecord { record_id: 23, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![ RuntimeCondition::SpecialConditionThreshold { label: "Use Wartime Cargos".to_string(), comparator: RuntimeConditionComparator::Ge, value: 1, }, RuntimeCondition::CandidateAvailabilityThreshold { name: "Mogul".to_string(), comparator: RuntimeConditionComparator::Eq, value: 2, }, RuntimeCondition::EconomicStatusCodeThreshold { comparator: RuntimeConditionComparator::Eq, value: 3, }, RuntimeCondition::WorldFlagEquals { key: "world.disable_stock_buying_and_selling".to_string(), value: true, }, ], effects: vec![RuntimeEffect::SetWorldFlag { key: "world_condition_passed".to_string(), value: true, }], }, RuntimeEventRecord { record_id: 24, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::SpecialConditionThreshold { label: "Disable Cargo Economy".to_string(), comparator: RuntimeConditionComparator::Gt, value: 0, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world_condition_failed".to_string(), value: true, }], }, ], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("world-state conditions should evaluate successfully"); assert_eq!(result.service_events[0].serviced_record_ids, vec![23]); assert_eq!(state.world_flags.get("world_condition_passed"), Some(&true)); assert_eq!(state.world_flags.get("world_condition_failed"), None); } #[test] fn evaluates_world_scalar_conditions_before_effects_run() { let mut state = RuntimeState { world_restore: RuntimeWorldRestoreState { limited_track_building_amount: Some(18), territory_access_cost: Some(750000), ..RuntimeWorldRestoreState::default() }, cargo_catalog: vec![ crate::RuntimeCargoCatalogEntry { slot_id: 1, label: "Cargo Production Slot 1".to_string(), cargo_class: RuntimeCargoClass::Factory, supplied_token_stem: None, demanded_token_stem: None, }, crate::RuntimeCargoCatalogEntry { slot_id: 5, label: "Cargo Production Slot 5".to_string(), cargo_class: RuntimeCargoClass::FarmMine, supplied_token_stem: None, demanded_token_stem: None, }, crate::RuntimeCargoCatalogEntry { slot_id: 9, label: "Cargo Production Slot 9".to_string(), cargo_class: RuntimeCargoClass::Other, supplied_token_stem: None, demanded_token_stem: None, }, ], named_locomotive_availability: BTreeMap::from([(String::from("Big Boy"), 42)]), named_locomotive_cost: BTreeMap::from([(String::from("GP7"), 175000)]), cargo_production_overrides: BTreeMap::from([(1, 125), (5, 75), (9, 30)]), event_runtime_records: vec![ RuntimeEventRecord { record_id: 25, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![ RuntimeCondition::NamedLocomotiveAvailabilityThreshold { name: "Big Boy".to_string(), comparator: RuntimeConditionComparator::Eq, value: 42, }, RuntimeCondition::NamedLocomotiveCostThreshold { name: "GP7".to_string(), comparator: RuntimeConditionComparator::Eq, value: 175000, }, RuntimeCondition::CargoProductionSlotThreshold { slot: 1, label: "Cargo Production Slot 1".to_string(), comparator: RuntimeConditionComparator::Eq, value: 125, }, RuntimeCondition::CargoProductionTotalThreshold { comparator: RuntimeConditionComparator::Eq, value: 230, }, RuntimeCondition::FactoryProductionTotalThreshold { comparator: RuntimeConditionComparator::Eq, value: 125, }, RuntimeCondition::FarmMineProductionTotalThreshold { comparator: RuntimeConditionComparator::Eq, value: 75, }, RuntimeCondition::OtherCargoProductionTotalThreshold { comparator: RuntimeConditionComparator::Eq, value: 30, }, RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator: RuntimeConditionComparator::Eq, value: 18, }, RuntimeCondition::TerritoryAccessCostThreshold { comparator: RuntimeConditionComparator::Eq, value: 750000, }, ], effects: vec![RuntimeEffect::SetWorldFlag { key: "world_scalar_condition_passed".to_string(), value: true, }], }, RuntimeEventRecord { record_id: 26, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold { name: "Missing Loco".to_string(), comparator: RuntimeConditionComparator::Gt, value: 0, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world_scalar_condition_failed".to_string(), value: true, }], }, ], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("world-scalar conditions should evaluate successfully"); assert_eq!(result.service_events[0].serviced_record_ids, vec![25]); assert_eq!( state.world_flags.get("world_scalar_condition_passed"), Some(&true) ); assert_eq!(state.world_flags.get("world_scalar_condition_failed"), None); } #[test] fn evaluates_runtime_variable_conditions_before_effects_run() { let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 10, 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, }], selected_company_id: Some(1), players: vec![RuntimePlayer { player_id: 1, current_cash: 50, active: true, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_player_id: Some(1), territories: vec![RuntimeTerritory { territory_id: 7, name: Some("North".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }], world_runtime_variables: BTreeMap::from([(1, 111)]), company_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(2, 222)]))]), player_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(3, 333)]))]), territory_runtime_variables: BTreeMap::from([(7, BTreeMap::from([(4, 444)]))]), event_runtime_records: vec![ RuntimeEventRecord { record_id: 27, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![ RuntimeCondition::WorldVariableThreshold { index: 1, comparator: RuntimeConditionComparator::Eq, value: 111, }, RuntimeCondition::CompanyVariableThreshold { target: RuntimeCompanyTarget::SelectedCompany, index: 2, comparator: RuntimeConditionComparator::Eq, value: 222, }, RuntimeCondition::PlayerVariableThreshold { target: RuntimePlayerTarget::SelectedPlayer, index: 3, comparator: RuntimeConditionComparator::Eq, value: 333, }, RuntimeCondition::TerritoryVariableThreshold { target: RuntimeTerritoryTarget::Ids { ids: vec![7] }, index: 4, comparator: RuntimeConditionComparator::Eq, value: 444, }, ], effects: vec![RuntimeEffect::SetWorldFlag { key: "runtime_variable_condition_passed".to_string(), value: true, }], }, RuntimeEventRecord { record_id: 28, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::PlayerVariableThreshold { target: RuntimePlayerTarget::SelectedPlayer, index: 4, comparator: RuntimeConditionComparator::Gt, value: 0, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "runtime_variable_condition_failed".to_string(), value: true, }], }, ], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("runtime-variable conditions should evaluate successfully"); assert_eq!(result.service_events[0].serviced_record_ids, vec![27]); assert_eq!( state.world_flags.get("runtime_variable_condition_passed"), Some(&true) ); assert_eq!( state.world_flags.get("runtime_variable_condition_failed"), None ); } #[test] fn one_shot_record_only_fires_once() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 20, trigger_kind: 2, active: true, service_count: 0, marks_collection_dirty: false, one_shot: true, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetWorldFlag { key: "one_shot".to_string(), value: true, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 2 }, ) .expect("first one-shot service should succeed"); let second = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 2 }, ) .expect("second one-shot service should succeed"); assert_eq!(state.event_runtime_records[0].service_count, 1); assert!(state.event_runtime_records[0].has_fired); assert_eq!( second.service_events[0].serviced_record_ids, Vec::::new() ); assert_eq!(second.service_events[0].applied_effect_count, 0); } #[test] fn rejects_debt_underflow() { let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Unknown, current_cash: 10, debt: 2, 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: 30, trigger_kind: 3, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::AdjustCompanyDebt { target: RuntimeCompanyTarget::AllActive, delta: -3, }], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 3 }, ); assert!(result.is_err()); } #[test] fn appended_record_waits_until_later_pass_without_dirty_rerun() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 40, trigger_kind: 5, active: true, service_count: 0, marks_collection_dirty: false, one_shot: true, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::AppendEventRecord { record: Box::new(RuntimeEventRecordTemplate { record_id: 41, trigger_kind: 5, active: true, marks_collection_dirty: false, one_shot: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetWorldFlag { key: "follow_on_later_pass".to_string(), value: true, }], }), }], }], ..state() }; let first = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 5 }, ) .expect("first pass should succeed"); assert_eq!(first.service_events.len(), 1); assert_eq!(first.service_events[0].serviced_record_ids, vec![40]); assert_eq!(first.service_events[0].appended_record_ids, vec![41]); assert_eq!(state.world_flags.get("follow_on_later_pass"), None); assert_eq!(state.event_runtime_records.len(), 2); assert_eq!(state.event_runtime_records[1].service_count, 0); let second = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 5 }, ) .expect("second pass should succeed"); assert_eq!(second.service_events[0].serviced_record_ids, vec![41]); assert_eq!(state.world_flags.get("follow_on_later_pass"), Some(&true)); assert!(state.event_runtime_records[0].has_fired); assert_eq!(state.event_runtime_records[1].service_count, 1); } #[test] fn appended_record_runs_in_dirty_rerun_after_commit() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 50, trigger_kind: 1, active: true, service_count: 0, marks_collection_dirty: true, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::AppendEventRecord { record: Box::new(RuntimeEventRecordTemplate { record_id: 51, trigger_kind: 0x0a, active: true, marks_collection_dirty: false, one_shot: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetWorldFlag { key: "dirty_rerun_follow_on".to_string(), value: true, }], }), }], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 1 }, ) .expect("dirty rerun with follow-on should succeed"); assert_eq!(result.service_events.len(), 2); assert_eq!(result.service_events[0].serviced_record_ids, vec![50]); assert_eq!(result.service_events[0].appended_record_ids, vec![51]); assert_eq!(result.service_events[1].trigger_kind, Some(0x0a)); assert_eq!(result.service_events[1].serviced_record_ids, vec![51]); assert_eq!(state.service_state.dirty_rerun_count, 1); assert_eq!(state.event_runtime_records.len(), 2); assert_eq!(state.event_runtime_records[1].service_count, 1); assert_eq!(state.world_flags.get("dirty_rerun_follow_on"), Some(&true)); } #[test] fn lifecycle_mutations_commit_between_passes() { let mut state = RuntimeState { event_runtime_records: vec![ RuntimeEventRecord { record_id: 60, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: true, has_fired: false, conditions: Vec::new(), effects: vec![ RuntimeEffect::AppendEventRecord { record: Box::new(RuntimeEventRecordTemplate { record_id: 64, trigger_kind: 7, active: true, marks_collection_dirty: false, one_shot: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetCandidateAvailability { name: "Appended Industry".to_string(), value: 1, }], }), }, RuntimeEffect::DeactivateEventRecord { record_id: 61 }, RuntimeEffect::ActivateEventRecord { record_id: 62 }, RuntimeEffect::RemoveEventRecord { record_id: 63 }, ], }, RuntimeEventRecord { record_id: 61, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetWorldFlag { key: "deactivated_after_first_pass".to_string(), value: true, }], }, RuntimeEventRecord { record_id: 62, trigger_kind: 7, active: false, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetSpecialCondition { label: "Activated On Second Pass".to_string(), value: 1, }], }, RuntimeEventRecord { record_id: 63, trigger_kind: 7, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetWorldFlag { key: "removed_after_first_pass".to_string(), value: true, }], }, ], ..state() }; let first = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("first lifecycle pass should succeed"); assert_eq!( first.service_events[0].serviced_record_ids, vec![60, 61, 63] ); assert_eq!(first.service_events[0].appended_record_ids, vec![64]); assert_eq!(first.service_events[0].activated_record_ids, vec![62]); assert_eq!(first.service_events[0].deactivated_record_ids, vec![61]); assert_eq!(first.service_events[0].removed_record_ids, vec![63]); assert_eq!( state .event_runtime_records .iter() .map(|record| (record.record_id, record.active)) .collect::>(), vec![(60, true), (61, false), (62, true), (64, true)] ); let second = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("second lifecycle pass should succeed"); assert_eq!(second.service_events[0].serviced_record_ids, vec![62, 64]); assert_eq!( state.special_conditions.get("Activated On Second Pass"), Some(&1) ); assert_eq!( state.candidate_availability.get("Appended Industry"), Some(&1) ); assert_eq!( state.world_flags.get("deactivated_after_first_pass"), Some(&true) ); assert_eq!( state.world_flags.get("removed_after_first_pass"), Some(&true) ); } #[test] fn rejects_duplicate_appended_record_id() { let mut state = RuntimeState { event_runtime_records: vec![ RuntimeEventRecord { record_id: 70, trigger_kind: 4, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::AppendEventRecord { record: Box::new(RuntimeEventRecordTemplate { record_id: 71, trigger_kind: 4, active: true, marks_collection_dirty: false, one_shot: false, conditions: Vec::new(), effects: Vec::new(), }), }], }, RuntimeEventRecord { record_id: 71, trigger_kind: 4, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: Vec::new(), }, ], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 4 }, ); assert!(result.is_err()); } #[test] fn rejects_missing_lifecycle_mutation_target() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 80, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::ActivateEventRecord { record_id: 999 }], }], ..state() }; let result = execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ); assert!(result.is_err()); } #[test] fn applies_economic_status_code_effect() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 90, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetEconomicStatusCode { value: 3 }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("economic-status effect should succeed"); assert_eq!(state.world_restore.economic_status_code, Some(3)); } #[test] fn applies_limited_track_building_amount_effect() { let mut state = RuntimeState { event_runtime_records: vec![RuntimeEventRecord { record_id: 91, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("limited-track-building-amount effect should succeed"); assert_eq!(state.world_restore.limited_track_building_amount, Some(18)); } #[test] fn confiscate_company_assets_zeros_company_and_retires_owned_trains() { let mut state = RuntimeState { companies: vec![ RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 50, debt: 7, 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, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 80, debt: 9, 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, }, ], selected_company_id: Some(1), trains: vec![ RuntimeTrain { train_id: 10, owner_company_id: 1, territory_id: None, locomotive_name: Some("Mikado".to_string()), active: true, retired: false, }, RuntimeTrain { train_id: 11, owner_company_id: 2, territory_id: None, locomotive_name: Some("Orca".to_string()), active: true, retired: false, }, ], event_runtime_records: vec![RuntimeEventRecord { record_id: 91, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::ConfiscateCompanyAssets { target: RuntimeCompanyTarget::SelectedCompany, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("confiscation effect should succeed"); assert_eq!(state.companies[0].current_cash, 0); assert_eq!(state.companies[0].debt, 0); assert!(!state.companies[0].active); assert_eq!(state.selected_company_id, None); assert!(state.trains[0].retired); assert!(!state.trains[1].retired); } #[test] fn retire_trains_respects_company_territory_and_locomotive_filters() { let mut state = RuntimeState { territories: vec![ RuntimeTerritory { territory_id: 7, name: Some("Appalachia".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }, RuntimeTerritory { territory_id: 8, name: Some("Great Plains".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }, ], trains: vec![ RuntimeTrain { train_id: 10, owner_company_id: 1, territory_id: Some(7), locomotive_name: Some("Mikado".to_string()), active: true, retired: false, }, RuntimeTrain { train_id: 11, owner_company_id: 1, territory_id: Some(7), locomotive_name: Some("Orca".to_string()), active: true, retired: false, }, RuntimeTrain { train_id: 12, owner_company_id: 1, territory_id: Some(8), locomotive_name: Some("Mikado".to_string()), active: true, retired: false, }, ], event_runtime_records: vec![RuntimeEventRecord { record_id: 92, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::RetireTrains { company_target: Some(RuntimeCompanyTarget::SelectedCompany), territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }), locomotive_name: Some("Mikado".to_string()), }], }], selected_company_id: Some(1), ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("retire-trains effect should succeed"); assert!(state.trains[0].retired); assert!(!state.trains[1].retired); assert!(!state.trains[2].retired); } #[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: 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: Some(2), company_holdings: BTreeMap::from([(2, 3)]), holdings_value_total: 60, net_worth_total: 80, purchasing_power_total: 110, }, RuntimeChairmanProfile { profile_id: 3, name: "Chairman Three".to_string(), active: false, current_cash: 30, linked_company_id: None, company_holdings: BTreeMap::new(), holdings_value_total: 0, net_worth_total: 30, purchasing_power_total: 30, }, ], event_runtime_records: vec![RuntimeEventRecord { record_id: 93, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::SetChairmanCash { target: RuntimeChairmanTarget::AllActive, 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() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("all-active chairman cash effect should succeed"); 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] fn deactivate_chairman_clears_selected_and_company_links_for_ids_target() { let mut state = RuntimeState { companies: vec![ RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, 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: Some(1), book_value_per_share: 0, investor_confidence: 0, management_attitude: 0, takeover_cooldown_year: None, merger_cooldown_year: None, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 80, 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: Some(2), book_value_per_share: 0, investor_confidence: 0, management_attitude: 0, takeover_cooldown_year: None, merger_cooldown_year: None, }, ], chairman_profiles: vec![ RuntimeChairmanProfile { profile_id: 1, name: "Chairman One".to_string(), active: true, current_cash: 10, linked_company_id: Some(1), company_holdings: BTreeMap::new(), holdings_value_total: 0, net_worth_total: 0, purchasing_power_total: 0, }, RuntimeChairmanProfile { profile_id: 2, name: "Chairman Two".to_string(), active: true, current_cash: 20, linked_company_id: Some(2), company_holdings: BTreeMap::new(), holdings_value_total: 0, net_worth_total: 0, purchasing_power_total: 0, }, ], selected_chairman_profile_id: Some(2), event_runtime_records: vec![RuntimeEventRecord { record_id: 94, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: Vec::new(), effects: vec![RuntimeEffect::DeactivateChairman { target: RuntimeChairmanTarget::Ids { ids: vec![2] }, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("ids-target chairman deactivation should succeed"); assert!(state.chairman_profiles[0].active); assert!(!state.chairman_profiles[1].active); assert_eq!(state.chairman_profiles[1].linked_company_id, None); assert_eq!(state.selected_chairman_profile_id, None); assert_eq!(state.companies[0].linked_chairman_profile_id, Some(1)); assert_eq!(state.companies[1].linked_chairman_profile_id, None); } #[test] fn company_governance_metric_conditions_gate_execution() { let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, 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: 2620, investor_confidence: 37, management_attitude: 58, takeover_cooldown_year: Some(1844), merger_cooldown_year: Some(1845), }], selected_company_id: Some(1), event_runtime_records: vec![RuntimeEventRecord { record_id: 95, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::CompanyNumericThreshold { target: RuntimeCompanyTarget::SelectedCompany, metric: crate::RuntimeCompanyMetric::BookValuePerShare, comparator: RuntimeConditionComparator::Eq, value: 2620, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world.book_value_gate_passed".to_string(), value: true, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("book-value company condition should gate execution"); assert_eq!( state.world_flags.get("world.book_value_gate_passed"), Some(&true) ); } #[test] fn derived_prime_rate_condition_reads_rehosted_issue_owner_state() { let mut issue_terms = vec![0; 0x3b]; issue_terms[crate::RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100; let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, 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: 2620, investor_confidence: 37, management_attitude: 58, takeover_cooldown_year: Some(1844), merger_cooldown_year: Some(1845), }], selected_company_id: Some(1), world_restore: RuntimeWorldRestoreState { issue_37_value: Some(5.0f32.to_bits()), ..RuntimeWorldRestoreState::default() }, service_state: RuntimeServiceState { world_issue_opinion_base_terms_raw_i32: issue_terms, ..RuntimeServiceState::default() }, event_runtime_records: vec![RuntimeEventRecord { record_id: 195, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::CompanyNumericThreshold { target: RuntimeCompanyTarget::SelectedCompany, metric: crate::RuntimeCompanyMetric::PrimeRate, comparator: RuntimeConditionComparator::Eq, value: 6, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world.derived_prime_rate_gate_passed".to_string(), value: true, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("derived prime-rate company condition should gate execution"); assert_eq!( state .world_flags .get("world.derived_prime_rate_gate_passed"), Some(&true) ); } #[test] fn derived_investor_confidence_condition_reads_rehosted_share_price_cache() { let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, 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: 2620, investor_confidence: 0, management_attitude: 58, takeover_cooldown_year: Some(1844), merger_cooldown_year: Some(1845), }], selected_company_id: Some(1), service_state: RuntimeServiceState { company_market_state: BTreeMap::from([( 1, crate::RuntimeCompanyMarketState { recent_per_share_subscore_raw_u32: 12.0f32.to_bits(), cached_share_price_raw_u32: 37.0f32.to_bits(), ..crate::RuntimeCompanyMarketState::default() }, )]), ..RuntimeServiceState::default() }, event_runtime_records: vec![RuntimeEventRecord { record_id: 196, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::CompanyNumericThreshold { target: RuntimeCompanyTarget::SelectedCompany, metric: crate::RuntimeCompanyMetric::InvestorConfidence, comparator: RuntimeConditionComparator::Eq, value: 37, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world.derived_investor_confidence_gate_passed".to_string(), value: true, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("derived investor-confidence company condition should gate execution"); assert_eq!( state .world_flags .get("world.derived_investor_confidence_gate_passed"), Some(&true) ); } #[test] fn derived_management_attitude_condition_reads_issue3a_owner_state() { let mut issue_terms = vec![0; 0x3b]; issue_terms[crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 40; let mut company_terms = vec![0; 0x3b]; company_terms[crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 12; let mut chairman_terms = vec![0; 0x3b]; chairman_terms[crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 6; let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, 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: Some(3), book_value_per_share: 2620, investor_confidence: 0, management_attitude: 0, takeover_cooldown_year: Some(1844), merger_cooldown_year: Some(1845), }], selected_company_id: Some(1), chairman_profiles: vec![crate::RuntimeChairmanProfile { profile_id: 3, name: "Chairman".to_string(), active: true, current_cash: 0, linked_company_id: Some(1), company_holdings: BTreeMap::new(), holdings_value_total: 0, net_worth_total: 0, purchasing_power_total: 0, }], service_state: RuntimeServiceState { world_issue_opinion_base_terms_raw_i32: issue_terms, company_market_state: BTreeMap::from([( 1, crate::RuntimeCompanyMarketState { issue_opinion_terms_raw_i32: company_terms, ..crate::RuntimeCompanyMarketState::default() }, )]), chairman_issue_opinion_terms_raw_i32: BTreeMap::from([(3, chairman_terms)]), ..RuntimeServiceState::default() }, event_runtime_records: vec![RuntimeEventRecord { record_id: 196, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::CompanyNumericThreshold { target: RuntimeCompanyTarget::SelectedCompany, metric: crate::RuntimeCompanyMetric::ManagementAttitude, comparator: RuntimeConditionComparator::Eq, value: 58, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world.derived_management_attitude_gate_passed".to_string(), value: true, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("derived management-attitude company condition should gate execution"); assert_eq!( state .world_flags .get("world.derived_management_attitude_gate_passed"), Some(&true) ); } #[test] fn book_value_condition_reads_rehosted_direct_company_field_band() { let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, 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: 37, management_attitude: 58, takeover_cooldown_year: Some(1844), merger_cooldown_year: Some(1845), }], selected_company_id: Some(1), service_state: RuntimeServiceState { company_market_state: BTreeMap::from([( 1, crate::RuntimeCompanyMarketState { direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( 0x32f, 2620.0f32.to_bits(), )]), ..crate::RuntimeCompanyMarketState::default() }, )]), ..RuntimeServiceState::default() }, event_runtime_records: vec![RuntimeEventRecord { record_id: 197, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::CompanyNumericThreshold { target: RuntimeCompanyTarget::SelectedCompany, metric: crate::RuntimeCompanyMetric::BookValuePerShare, comparator: RuntimeConditionComparator::Eq, value: 2620, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world.rehosted_book_value_gate_passed".to_string(), value: true, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("rehosted direct-field book-value condition should gate execution"); assert_eq!( state .world_flags .get("world.rehosted_book_value_gate_passed"), Some(&true) ); } #[test] fn derived_credit_rating_condition_reads_rehosted_finance_owner_state() { let mut year_stat_family_qword_bits = vec![ 0u64; (crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize ]; year_stat_family_qword_bits [(0x12 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = 20.0f64.to_bits(); year_stat_family_qword_bits [(0x01 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = 100.0f64.to_bits(); year_stat_family_qword_bits [(0x09 * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + 1) as usize] = 0.0f64.to_bits(); let mut state = RuntimeState { companies: vec![RuntimeCompany { company_id: 1, controller_kind: RuntimeCompanyControllerKind::Human, 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: 37, management_attitude: 58, takeover_cooldown_year: Some(1844), merger_cooldown_year: Some(1845), }], selected_company_id: Some(1), world_restore: RuntimeWorldRestoreState { issue_37_value: Some(5.0f32.to_bits()), issue_38_value: Some(2), packed_year_word_raw_u16: Some(1835), ..RuntimeWorldRestoreState::default() }, service_state: RuntimeServiceState { world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], company_market_state: BTreeMap::from([( 1, crate::RuntimeCompanyMarketState { outstanding_shares: 10_000, founding_year: 1830, last_bankruptcy_year: 1800, year_stat_family_qword_bits, live_bond_slots: vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 100_000, maturity_year: 0, coupon_rate_raw_u32: 0.05f32.to_bits(), }], ..crate::RuntimeCompanyMarketState::default() }, )]), ..RuntimeServiceState::default() }, event_runtime_records: vec![RuntimeEventRecord { record_id: 196, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::CompanyNumericThreshold { target: RuntimeCompanyTarget::SelectedCompany, metric: crate::RuntimeCompanyMetric::CreditRating, comparator: RuntimeConditionComparator::Eq, value: 10, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world.derived_credit_rating_gate_passed".to_string(), value: true, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("derived credit-rating company condition should gate execution"); assert_eq!( state .world_flags .get("world.derived_credit_rating_gate_passed"), Some(&true) ); } #[test] fn chairman_metric_conditions_support_all_active_target() { let mut state = RuntimeState { chairman_profiles: vec![ RuntimeChairmanProfile { profile_id: 1, name: "Chairman One".to_string(), active: true, current_cash: 20, linked_company_id: None, company_holdings: BTreeMap::new(), holdings_value_total: 500, net_worth_total: 700, purchasing_power_total: 900, }, RuntimeChairmanProfile { profile_id: 2, name: "Chairman Two".to_string(), active: true, current_cash: 30, linked_company_id: None, company_holdings: BTreeMap::new(), holdings_value_total: 600, net_worth_total: 800, purchasing_power_total: 1000, }, ], event_runtime_records: vec![RuntimeEventRecord { record_id: 96, trigger_kind: 6, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, conditions: vec![RuntimeCondition::ChairmanNumericThreshold { target: RuntimeChairmanTarget::AllActive, metric: RuntimeChairmanMetric::PurchasingPowerTotal, comparator: RuntimeConditionComparator::Ge, value: 900, }], effects: vec![RuntimeEffect::SetWorldFlag { key: "world.chairman_gate_passed".to_string(), value: true, }], }], ..state() }; execute_step_command( &mut state, &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, ) .expect("all-active chairman condition should gate execution"); assert_eq!( state.world_flags.get("world.chairman_gate_passed"), Some(&true) ); } }