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.
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
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
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

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;

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
chooses one annual-finance action per active company and already commits the shellless
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 same live bond-slot owner
surface now also carries save-native maturity years into annual bond policy summaries as the
next seam for shellless repayment work, and now also derives the current live coupon burden
directly from owned bond slots
surface now also carries save-native maturity years into annual bond policy summaries, derives the current live coupon burden
directly from owned bond slots, and now also commits the shellless “repay matured live bonds,
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
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

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
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
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
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