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

@ -99,7 +99,8 @@ distress bankruptcy fallback is now rehosted on that same owner surface too, usi
cash reader seam plus the first three trailing net-profit years instead of another ad hoc probe. cash reader seam plus the first three trailing net-profit years instead of another ad hoc probe.
The annual bond lane now runs on that same owner surface too, using the simulated post-repayment The annual bond lane now runs on that same owner surface too, using the simulated post-repayment
cash window plus the linked-transit threshold split to stage `500000` principal issue counts as a cash window plus the linked-transit threshold split to stage `500000` principal issue counts as a
pure runtime reader. The annual dividend lane now runs there too: the runtime now rehosts the pure runtime reader, and periodic boundary service now commits the same shellless matured-bond repayment-and-
compaction path before issuing the exact staged count. The annual dividend lane now runs there too: the runtime now rehosts the
shared year-or-control-transfer metric seam, the board-approved dividend ceiling helper, and the shared year-or-control-transfer metric seam, the board-approved dividend ceiling helper, and the
full annual dividend adjustment branch over owned current cash, public float, current dividend, full annual dividend adjustment branch over owned current cash, public float, current dividend,
building-growth policy, and recent profit history instead of leaving that policy on shell-side building-growth policy, and recent profit history instead of leaving that policy on shell-side

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) let eligible_for_bond_issue_branch = runtime_world_annual_finance_mode_active(state)
== Some(true) == Some(true)
&& runtime_world_bond_issue_and_repayment_allowed(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 { Some(RuntimeCompanyAnnualBondPolicyState {
company_id, company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state), annual_mode_active: runtime_world_annual_finance_mode_active(state),
@ -8350,6 +8351,116 @@ mod tests {
assert!(bond_state.eligible_for_bond_issue_branch); 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] #[test]
fn derives_annual_stock_issue_state_from_rehosted_owner_state() { fn derives_annual_stock_issue_state_from_rehosted_owner_state() {
let mut year_stat_family_qword_bits = vec![ let mut year_stat_family_qword_bits = vec![

View file

@ -2,25 +2,25 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize}; 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::{ use crate::runtime::{
runtime_company_bond_interest_rate_quote_f64, runtime_company_bond_interest_rate_quote_f64,
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64, runtime_company_support_adjusted_share_price_scalar_with_pressure_f64,
runtime_round_f64_to_i64, 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 PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
const COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT: u32 = 0x33f; 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)? .checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)?
.try_into() .try_into()
.ok()?; .ok()?;
let required_year_len = ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) let required_year_len =
* RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; ((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 { if market_state.year_stat_family_qword_bits.len() < required_year_len {
market_state market_state
.year_stat_family_qword_bits .year_stat_family_qword_bits
@ -247,7 +247,11 @@ fn service_post_company_stat_delta(
} }
let Some(refreshed_current_cash) = ({ 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; return false;
}; };
let Some(index) = service_ensure_company_stat_post_capacity(market_state, slot_id) else { 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 market_state
.year_stat_family_qword_bits .year_stat_family_qword_bits
.get((RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) .get(
as usize) (RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize,
)
.copied() .copied()
.and_then(service_decode_saved_f64_bits) .and_then(service_decode_saved_f64_bits)
.and_then(runtime_round_f64_to_i64) .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; 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; market_state.last_bankruptcy_year = bankruptcy_year;
for slot in &mut market_state.live_bond_slots { for slot in &mut market_state.live_bond_slots {
slot.principal /= 2; 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.bond_count = market_state.live_bond_slots.len().min(u8::MAX as usize) as u8;
market_state.largest_live_bond_principal = market_state.largest_live_bond_principal = market_state
market_state.live_bond_slots.iter().map(|slot| slot.principal).max(); .live_bond_slots
.iter()
.map(|slot| slot.principal)
.max();
market_state.highest_coupon_live_bond_principal = market_state market_state.highest_coupon_live_bond_principal = market_state
.live_bond_slots .live_bond_slots
.iter() .iter()
@ -367,6 +382,108 @@ fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -
company_mutated 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( fn service_company_annual_finance_policy(
state: &mut RuntimeState, state: &mut RuntimeState,
service_events: &mut Vec<ServiceEvent>, service_events: &mut Vec<ServiceEvent>,
@ -515,10 +632,21 @@ fn service_company_annual_finance_policy(
if !bond_state.eligible_for_bond_issue_branch { if !bond_state.eligible_for_bond_issue_branch {
continue; 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 { let Some(principal) = bond_state.issue_principal_step else {
if mutated {
applied_effect_count += 1;
mutated_company_ids.insert(company_id);
}
continue; continue;
}; };
let Some(years_to_maturity) = bond_state.proposed_issue_years_to_maturity else { 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; continue;
}; };
let Some(maturity_year) = state let Some(maturity_year) = state
@ -527,77 +655,104 @@ fn service_company_annual_finance_policy(
.map(u32::from) .map(u32::from)
.and_then(|year| year.checked_add(years_to_maturity)) .and_then(|year| year.checked_add(years_to_maturity))
else { else {
continue; if mutated {
}; applied_effect_count += 1;
let Some(quote_rate) = runtime_company_bond_interest_rate_quote_f64( mutated_company_ids.insert(company_id);
state, }
company_id,
principal,
years_to_maturity,
) else {
continue; continue;
}; };
mutated |= service_post_company_stat_delta( for _ in 0..issue_bond_count {
state, let Some(quote_rate) = runtime_company_bond_interest_rate_quote_f64(
company_id, state,
0x0c, company_id,
(principal as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE, principal,
true, years_to_maturity,
); ) else {
mutated |= service_post_company_stat_delta( break;
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 mutated |= service_post_company_stat_delta(
.service_state state,
.company_market_state company_id,
.get_mut(&company_id) 0x0c,
else { (principal as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE,
continue; true,
}; );
let slot_index = market_state.bond_count as u32; mutated |= service_post_company_stat_delta(
if market_state.bond_count == u8::MAX { state,
continue; company_id,
} 0x12,
market_state.live_bond_slots.push(crate::RuntimeCompanyBondSlot { -(principal as f64),
slot_index, false,
principal, );
maturity_year, mutated |= service_post_company_stat_delta(
coupon_rate_raw_u32: (quote_rate as f32).to_bits(), state,
}); company_id,
market_state.bond_count = market_state.bond_count.saturating_add(1); RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
market_state.largest_live_bond_principal = Some( 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 market_state
.largest_live_bond_principal .live_bond_slots
.unwrap_or(0) .push(crate::RuntimeCompanyBondSlot {
.max(principal), slot_index,
); principal,
let highest_coupon_live_principal = market_state maturity_year,
.live_bond_slots coupon_rate_raw_u32: (quote_rate as f32).to_bits(),
.iter() });
.filter_map(|slot| { market_state.bond_count = market_state.bond_count.saturating_add(1);
let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64; market_state.largest_live_bond_principal = Some(
coupon.is_finite().then_some((coupon, slot.principal)) market_state
}) .largest_live_bond_principal
.max_by(|left, right| { .unwrap_or(0)
left.0 .max(principal),
.partial_cmp(&right.0) );
.unwrap_or(std::cmp::Ordering::Equal) let highest_coupon_live_principal = market_state
}) .live_bond_slots
.map(|(_, principal)| principal); .iter()
market_state.highest_coupon_live_bond_principal = highest_coupon_live_principal; .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 { if mutated {
applied_effect_count += 1; applied_effect_count += 1;
mutated_company_ids.insert(company_id); 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 { let Some(batch_size) = repurchase_state.repurchase_batch_size else {
break; break;
}; };
let Some(pressure_shares) = let Some(pressure_shares) = runtime_round_f64_to_i64(
runtime_round_f64_to_i64(batch_size as f64 * COMPANY_REPURCHASE_PRESSURE_SCALE) batch_size as f64 * COMPANY_REPURCHASE_PRESSURE_SCALE,
else { ) else {
break; break;
}; };
let Some(share_price_scalar) = let Some(share_price_scalar) =
@ -2872,9 +3027,11 @@ mod tests {
}, },
}; };
let pressured_share_price = crate::runtime:: let pressured_share_price =
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( crate::runtime::runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(
&base_state, 23, 700, &base_state,
23,
700,
) )
.expect("repurchase share price"); .expect("repurchase share price");
let expected_repurchase_total = let expected_repurchase_total =
@ -2889,7 +3046,10 @@ mod tests {
state.service_state.annual_finance_last_actions.get(&23), state.service_state.annual_finance_last_actions.get(&23),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase) 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!( assert_eq!(
state.service_state.company_market_state[&23].outstanding_shares, state.service_state.company_market_state[&23].outstanding_shares,
9_000 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] #[test]
fn periodic_boundary_applies_creditor_pressure_bankruptcy_from_annual_finance_policy() { fn periodic_boundary_applies_creditor_pressure_bankruptcy_from_annual_finance_policy() {
let mut year_stat_family_qword_bits = vec![ let mut year_stat_family_qword_bits = vec![
0u64; 0u64;
((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) ((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 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; 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![ let mut year_stat_family_qword_bits = vec![
0u64; 0u64;
((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) ((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 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; let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize;

View file

@ -150,12 +150,12 @@ The highest-value next passes are now:
stock-repurchase gate headlessly as another pure reader; periodic boundary service now also stock-repurchase gate headlessly as another pure reader; periodic boundary service now also
chooses one annual-finance action per active company and already commits the shellless chooses one annual-finance action per active company and already commits the shellless
creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase, creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase,
stock-issue, and bond-issue branches against owned runtime state, with bankruptcy now following stock-issue, and annual bond-restructure branches against owned runtime state, with bankruptcy now following
the grounded “halve live bond debt and stamp the year” path rather than a liquidation shortcut; the grounded “halve live bond debt and stamp the year” path rather than a liquidation shortcut;
the same live bond-slot owner the same live bond-slot owner
surface now also carries save-native maturity years into annual bond policy summaries as the surface now also carries save-native maturity years into annual bond policy summaries, derives the current live coupon burden
next seam for shellless repayment work, and now also derives the current live coupon burden directly from owned bond slots, and now also commits the shellless “repay matured live bonds,
directly from owned bond slots compact the table, then issue the exact staged count” path during periodic service
- the project rule on the remaining closure work is now explicit too: when one runtime-facing field - the project rule on the remaining closure work is now explicit too: when one runtime-facing field
is still ambiguous, prefer rehosting the owning source state or real reader/setter family first is still ambiguous, prefer rehosting the owning source state or real reader/setter family first
instead of guessing another derived leaf field from neighboring raw offsets instead of guessing another derived leaf field from neighboring raw offsets

View file

@ -233,7 +233,9 @@ adjusted share price, and those policy bytes rather than staying in atlas prose
deep-distress bankruptcy fallback now rides the same owner-state seam too, using the save-native deep-distress bankruptcy fallback now rides the same owner-state seam too, using the save-native
cash reader plus the first three trailing net-profit years instead of a parallel raw-offset guess. cash reader plus the first three trailing net-profit years instead of a parallel raw-offset guess.
The annual bond lane now rides it as well, using the simulated post-repayment cash window plus the The annual bond lane now rides it as well, using the simulated post-repayment cash window plus the
linked-transit threshold split to stage `500000` principal issue counts without shell ownership. linked-transit threshold split to stage `500000` principal issue counts without shell ownership,
and periodic boundary service now commits the same shellless matured-bond repay/compact/issue path instead of
stopping at the staging reader.
The annual dividend-adjustment lane now rides that same seam too: the runtime now rehosts the The annual dividend-adjustment lane now rides that same seam too: the runtime now rehosts the
shared year-or-control-transfer metric reader, the board-approved dividend ceiling helper, and the shared year-or-control-transfer metric reader, the board-approved dividend ceiling helper, and the
full annual dividend branch over owned cash, public float, current dividend, and building-growth full annual dividend branch over owned cash, public float, current dividend, and building-growth