Rehost annual bond repayment and compaction
This commit is contained in:
parent
ad048f1528
commit
fc1ba28109
5 changed files with 686 additions and 105 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue