Rehost annual bond issue service branch

This commit is contained in:
Jan Petykiewicz 2026-04-18 01:32:57 -07:00
commit c5c14d71b0
5 changed files with 240 additions and 8 deletions

View file

@ -105,9 +105,9 @@ full annual dividend adjustment branch over owned current cash, public float, cu
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
dialog notes. `simulation_service_periodic_boundary_work` is now beginning to use that same owner dialog notes. `simulation_service_periodic_boundary_work` is now beginning to use that same owner
surface too: the runtime chooses one annual-finance action per active company and already commits surface too: the runtime chooses one annual-finance action per active company and already commits
the shellless dividend-adjustment, stock-repurchase, and stock-issue branches by mutating owned the shellless dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches by
dividend, company stat-post, outstanding-share, and issue-calendar state instead of stopping at mutating owned dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot
reader-only diagnostics. state instead of stopping at reader-only diagnostics.
The same seam now also carries the fixed-world building-density growth setting plus the linked The same seam now also carries the fixed-world building-density growth setting plus the linked
chairman personality byte, which is enough to run the annual stock-repurchase gate as another chairman personality byte, which is enough to run the annual stock-repurchase gate as another
pure reader over owned save-native state instead of a guessed finance-side approximation. pure reader over owned save-native state instead of a guessed finance-side approximation.

View file

@ -3053,6 +3053,18 @@ pub fn runtime_company_average_live_bond_coupon(
Some(weighted_coupon_sum / total_principal as f64) Some(weighted_coupon_sum / total_principal as f64)
} }
pub fn runtime_company_bond_interest_rate_quote_f64(
state: &RuntimeState,
company_id: u32,
_principal: u32,
_years_to_maturity: u32,
) -> Option<f64> {
let credit_rating = runtime_company_credit_rating(state, company_id)? as f64;
let prime_rate_percent = runtime_company_prime_rate(state, company_id)? as f64;
let quote = (prime_rate_percent + (10.0 - credit_rating)) / 100.0;
quote.is_finite().then_some(quote)
}
pub fn runtime_world_annual_finance_mode_active(state: &RuntimeState) -> Option<bool> { pub fn runtime_world_annual_finance_mode_active(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.partial_year_progress_raw_u8? == 0x0c) Some(state.world_restore.partial_year_progress_raw_u8? == 0x0c)
} }

View file

@ -17,6 +17,7 @@ use crate::{
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN, 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_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,
}; };
@ -436,6 +437,95 @@ fn service_company_annual_finance_policy(
mutated_company_ids.insert(company_id); mutated_company_ids.insert(company_id);
} }
} }
crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue => {
let mut mutated = false;
let Some(bond_state) =
crate::runtime::runtime_company_annual_bond_policy_state(state, company_id)
else {
continue;
};
if !bond_state.eligible_for_bond_issue_branch {
continue;
}
let Some(principal) = bond_state.issue_principal_step else {
continue;
};
let Some(years_to_maturity) = bond_state.proposed_issue_years_to_maturity else {
continue;
};
let Some(quote_rate) = runtime_company_bond_interest_rate_quote_f64(
state,
company_id,
principal,
years_to_maturity,
) else {
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,
);
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,
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 mutated {
applied_effect_count += 1;
mutated_company_ids.insert(company_id);
}
}
crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => { crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => {
let mut mutated = false; let mut mutated = false;
for _ in 0..128 { for _ in 0..128 {
@ -2735,6 +2825,133 @@ mod tests {
); );
} }
#[test]
fn periodic_boundary_applies_bond_issue_from_annual_finance_policy() {
let mut year_stat_family_qword_bits = vec![
0u64;
(crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize
];
year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH
* crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize] = (-400_000.0f64).to_bits();
let mut state = RuntimeState {
calendar: crate::CalendarPoint {
year: 1845,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: crate::RuntimeSaveProfileState::default(),
world_restore: crate::RuntimeWorldRestoreState {
packed_year_word_raw_u16: Some(1845),
partial_year_progress_raw_u8: Some(0x0c),
bond_issue_and_repayment_policy_raw_u8: Some(0),
bond_issue_and_repayment_allowed: Some(true),
stock_issue_and_buyback_policy_raw_u8: Some(0),
stock_issue_and_buyback_allowed: Some(true),
bankruptcy_policy_raw_u8: Some(0),
bankruptcy_allowed: Some(true),
..crate::RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany {
company_id: 24,
controller_kind: crate::RuntimeCompanyControllerKind::Unknown,
current_cash: 0,
debt: 0,
credit_rating_score: Some(6),
prime_rate: Some(5),
track_piece_counts: crate::RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
}],
selected_company_id: Some(24),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
all_cargo_price_override: None,
named_cargo_price_overrides: BTreeMap::new(),
all_cargo_production_override: None,
factory_cargo_production_override: None,
farm_mine_cargo_production_override: None,
named_cargo_production_overrides: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
world_runtime_variables: BTreeMap::new(),
company_runtime_variables: BTreeMap::new(),
player_runtime_variables: BTreeMap::new(),
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: crate::RuntimeServiceState {
company_market_state: BTreeMap::from([(
24,
crate::RuntimeCompanyMarketState {
outstanding_shares: 10_000,
founding_year: 1840,
year_stat_family_qword_bits,
..crate::RuntimeCompanyMarketState::default()
},
)]),
..crate::RuntimeServiceState::default()
},
};
let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary)
.expect("periodic boundary should apply annual bond issue policy");
assert_eq!(
state.service_state.annual_finance_last_actions.get(&24),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::BondIssue)
);
assert_eq!(state.companies[0].current_cash, 100_000);
assert_eq!(state.service_state.company_market_state[&24].bond_count, 1);
assert_eq!(
state.service_state.company_market_state[&24].largest_live_bond_principal,
Some(500_000)
);
assert_eq!(
state.service_state.company_market_state[&24].highest_coupon_live_bond_principal,
Some(500_000)
);
assert_eq!(
state.service_state.company_market_state[&24].live_bond_slots,
vec![crate::RuntimeCompanyBondSlot {
slot_index: 0,
principal: 500_000,
coupon_rate_raw_u32: 0.09f32.to_bits(),
}]
);
assert!(
result
.service_events
.iter()
.any(|event| event.kind == "annual_finance_policy"
&& event.applied_effect_count == 1
&& event.mutated_company_ids == vec![24])
);
}
#[test] #[test]
fn applies_company_effects_for_specific_targets() { fn applies_company_effects_for_specific_targets() {
let mut state = RuntimeState { let mut state = RuntimeState {

View file

@ -139,14 +139,17 @@ The highest-value next passes are now:
bankruptcy, and dividend finance-policy bytes, and the first annual creditor-pressure branch now bankruptcy, and dividend finance-policy bytes, and the first annual creditor-pressure branch now
executes as a pure runtime reader over that owner state instead of remaining atlas-only; the executes as a pure runtime reader over that owner state instead of remaining atlas-only; the
later deep-distress bankruptcy fallback now runs on that same save-native cash and trailing- later deep-distress bankruptcy fallback now runs on that same save-native cash and trailing-
profit seam; the annual bond, stock-repurchase, and stock-capital issue branches now do too profit seam; the annual bond, stock-repurchase, and stock-capital issue branches now do too,
net-profit surface too; the annual dividend-adjustment branch now does as well through the and periodic boundary service now also commits the bond-issue branch through that owned
stat-post and live-bond seam instead of leaving it as a reader-only finance lane; the rehosted
company market reader now carries the first bundled annual net-profit surface too; the annual
dividend-adjustment branch now does as well through the
shared year-or-control-transfer reader and board-approved dividend ceiling helper; the same shared year-or-control-transfer reader and board-approved dividend ceiling helper; the same
owner seam now also carries the fixed-world building-density owner seam now also carries the fixed-world building-density
growth setting plus the linked chairman personality byte, which is enough to run the annual growth setting plus the linked chairman personality byte, which is enough to run the annual
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
dividend-adjustment, stock-repurchase, and stock-issue branches against owned runtime state dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches against owned runtime state
- 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

@ -244,8 +244,8 @@ save/runtime state instead of another threshold-only note. The stock-capital iss
rides that same seam too, with share-pressure, cooldown, and price-to-book gate state exposed as rides that same seam too, with share-pressure, cooldown, and price-to-book gate state exposed as
normal runtime readers. Periodic boundary service now also uses that owner seam as a real chooser: normal runtime readers. Periodic boundary service now also uses that owner seam as a real chooser:
the runtime selects one annual-finance action per active company and already commits the shellless the runtime selects one annual-finance action per active company and already commits the shellless
dividend-adjustment, stock-repurchase, and stock-issue branches directly into owned dividend, dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches directly into owned
company stat-post, outstanding-share, and issue-calendar state. dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state.
## Why This Boundary ## Why This Boundary