Rehost annual bond repayment and compaction

This commit is contained in:
Jan Petykiewicz 2026-04-18 01:57:06 -07:00
commit fc1ba28109
5 changed files with 686 additions and 105 deletions

View file

@ -3298,7 +3298,8 @@ pub fn runtime_company_annual_bond_policy_state(
let eligible_for_bond_issue_branch = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_bond_issue_and_repayment_allowed(state) == Some(true)
&& proposed_issue_bond_count.is_some_and(|count| count > 0);
&& (matured_live_bond_principal_total.is_some_and(|principal| principal > 0)
|| proposed_issue_bond_count.is_some_and(|count| count > 0));
Some(RuntimeCompanyAnnualBondPolicyState {
company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state),
@ -8350,6 +8351,116 @@ mod tests {
assert!(bond_state.eligible_for_bond_issue_branch);
}
#[test]
fn annual_bond_policy_stays_eligible_for_repayment_without_new_issue() {
let mut year_stat_family_qword_bits = vec![
0u64;
((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * 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 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize;
bits[index] = value.to_bits();
};
write_current_value(&mut year_stat_family_qword_bits, 0x0d, 900_000.0);
let state = RuntimeState {
calendar: CalendarPoint {
year: 1845,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState {
partial_year_progress_raw_u8: Some(0x0c),
bond_issue_and_repayment_policy_raw_u8: Some(0),
bond_issue_and_repayment_allowed: Some(true),
..RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 12,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
}],
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 {
company_market_state: BTreeMap::from([(
12,
RuntimeCompanyMarketState {
bond_count: 2,
live_bond_slots: vec![
RuntimeCompanyBondSlot {
slot_index: 0,
principal: 200_000,
maturity_year: 1845,
coupon_rate_raw_u32: 0.09f32.to_bits(),
},
RuntimeCompanyBondSlot {
slot_index: 1,
principal: 150_000,
maturity_year: 1847,
coupon_rate_raw_u32: 0.08f32.to_bits(),
},
],
year_stat_family_qword_bits,
..RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
let bond_state =
runtime_company_annual_bond_policy_state(&state, 12).expect("bond policy state");
assert_eq!(bond_state.live_bond_principal_total, Some(350_000));
assert_eq!(bond_state.cash_after_full_repayment, Some(550_000));
assert_eq!(bond_state.proposed_issue_bond_count, Some(0));
assert!(bond_state.eligible_for_bond_issue_branch);
}
#[test]
fn derives_annual_stock_issue_state_from_rehosted_owner_state() {
let mut year_stat_family_qword_bits = vec![

View file

@ -2,25 +2,25 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{
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, RUNTIME_COMPANY_STAT_SLOT_COUNT,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN,
};
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;
@ -219,8 +219,8 @@ fn service_ensure_company_stat_post_capacity(
.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;
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
@ -247,7 +247,11 @@ fn service_post_company_stat_delta(
}
let Some(refreshed_current_cash) = ({
let Some(market_state) = state.service_state.company_market_state.get_mut(&company_id) else {
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 {
@ -285,8 +289,10 @@ fn service_post_company_stat_delta(
market_state
.year_stat_family_qword_bits
.get((RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize)
.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)
@ -317,15 +323,24 @@ fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -
};
let mut company_mutated = false;
if let Some(market_state) = state.service_state.company_market_state.get_mut(&company_id) {
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
.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.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()
@ -367,6 +382,108 @@ fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -
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>,
@ -515,10 +632,21 @@ fn service_company_annual_finance_policy(
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
@ -527,77 +655,104 @@ fn service_company_annual_finance_policy(
.map(u32::from)
.and_then(|year| year.checked_add(years_to_maturity))
else {
continue;
};
let Some(quote_rate) = runtime_company_bond_interest_rate_quote_f64(
state,
company_id,
principal,
years_to_maturity,
) else {
if mutated {
applied_effect_count += 1;
mutated_company_ids.insert(company_id);
}
continue;
};
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,
);
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;
};
let Some(market_state) = state
.service_state
.company_market_state
.get_mut(&company_id)
else {
continue;
};
let slot_index = market_state.bond_count as u32;
if market_state.bond_count == u8::MAX {
continue;
}
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(
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
.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;
.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);
@ -617,9 +772,9 @@ fn service_company_annual_finance_policy(
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 {
let Some(pressure_shares) = runtime_round_f64_to_i64(
batch_size as f64 * COMPANY_REPURCHASE_PRESSURE_SCALE,
) else {
break;
};
let Some(share_price_scalar) =
@ -2872,9 +3027,11 @@ mod tests {
},
};
let pressured_share_price = crate::runtime::
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(
&base_state, 23, 700,
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 =
@ -2889,7 +3046,10 @@ mod tests {
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.companies[0].current_cash,
1_600_000 - expected_repurchase_total
);
assert_eq!(
state.service_state.company_market_state[&23].outstanding_shares,
9_000
@ -3032,12 +3192,318 @@ mod tests {
);
}
#[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
* 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;
@ -3207,7 +3673,8 @@ mod tests {
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
* 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;