6442 lines
251 KiB
Rust
6442 lines
251 KiB
Rust
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<u8>,
|
|
pub serviced_record_ids: Vec<u32>,
|
|
pub applied_effect_count: u32,
|
|
pub mutated_company_ids: Vec<u32>,
|
|
pub mutated_player_ids: Vec<u32>,
|
|
pub appended_record_ids: Vec<u32>,
|
|
pub activated_record_ids: Vec<u32>,
|
|
pub deactivated_record_ids: Vec<u32>,
|
|
pub removed_record_ids: Vec<u32>,
|
|
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<BoundaryEvent>,
|
|
pub service_events: Vec<ServiceEvent>,
|
|
}
|
|
|
|
#[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<u32>,
|
|
activated_record_ids: Vec<u32>,
|
|
deactivated_record_ids: Vec<u32>,
|
|
removed_record_ids: Vec<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct ResolvedConditionContext {
|
|
matching_company_ids: BTreeSet<u32>,
|
|
matching_player_ids: BTreeSet<u32>,
|
|
matching_chairman_profile_ids: BTreeSet<u32>,
|
|
}
|
|
|
|
pub fn execute_step_command(
|
|
state: &mut RuntimeState,
|
|
command: &StepCommand,
|
|
) -> Result<StepResult, String> {
|
|
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<BoundaryEvent>,
|
|
) -> Result<u64, String> {
|
|
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<BoundaryEvent>,
|
|
) -> u64 {
|
|
for _ in 0..steps {
|
|
step_once(state, boundary_events);
|
|
}
|
|
steps.into()
|
|
}
|
|
|
|
fn step_once(state: &mut RuntimeState, boundary_events: &mut Vec<BoundaryEvent>) {
|
|
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<ServiceEvent>,
|
|
) -> 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<f64> {
|
|
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<usize> {
|
|
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::<u64>()
|
|
});
|
|
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::<u64>()
|
|
})
|
|
.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::<u64>()
|
|
})
|
|
.unwrap_or(0);
|
|
company_mutated = true;
|
|
}
|
|
|
|
company_mutated
|
|
}
|
|
|
|
fn service_company_annual_finance_policy(
|
|
state: &mut RuntimeState,
|
|
service_events: &mut Vec<ServiceEvent>,
|
|
) -> Result<(), String> {
|
|
let active_company_ids = state
|
|
.companies
|
|
.iter()
|
|
.filter(|company| company.active)
|
|
.map(|company| company.company_id)
|
|
.collect::<Vec<_>>();
|
|
let active_company_id_set = active_company_ids.iter().copied().collect::<BTreeSet<_>>();
|
|
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::<u64>()
|
|
})
|
|
.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<ServiceEvent>,
|
|
) -> 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::<Vec<_>>();
|
|
|
|
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<u32>,
|
|
mutated_player_ids: &mut BTreeSet<u32>,
|
|
staged_event_graph_mutations: &mut Vec<EventGraphMutation>,
|
|
) -> Result<AppliedEffectsSummary, String> {
|
|
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<Option<ResolvedConditionContext>, String> {
|
|
if conditions.is_empty() {
|
|
return Ok(Some(ResolvedConditionContext::default()));
|
|
}
|
|
|
|
let mut company_matches: Option<BTreeSet<u32>> = None;
|
|
let mut player_matches: Option<BTreeSet<u32>> = None;
|
|
let mut chairman_matches: Option<BTreeSet<u32>> = 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::<BTreeSet<_>>();
|
|
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::<BTreeSet<_>>();
|
|
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::<i64>();
|
|
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::<BTreeSet<_>>();
|
|
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::<BTreeSet<_>>();
|
|
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::<BTreeSet<_>>();
|
|
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::<i64>();
|
|
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<BTreeSet<u32>>, next: BTreeSet<u32>) {
|
|
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<BTreeSet<u32>>, next: BTreeSet<u32>) {
|
|
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<BTreeSet<u32>>, next: BTreeSet<u32>) {
|
|
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<Vec<u32>, 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::<BTreeSet<_>>();
|
|
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<Vec<u32>, 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::<BTreeSet<_>>();
|
|
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<Vec<u32>, 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::<BTreeSet<_>>();
|
|
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<Vec<u32>, 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::<BTreeSet<_>>();
|
|
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<u64, String> {
|
|
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<u32>>,
|
|
territory_ids: Option<&Vec<u32>>,
|
|
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<crate::RuntimeCompanyTerritoryAccess>,
|
|
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<u64>, 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<u64>, 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<u64>, 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<u64>, 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<u64>, 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<u64>, 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<u64>, 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<u64>, 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::<u32>::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<_>>(),
|
|
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)
|
|
);
|
|
}
|
|
}
|