From c5c14d71b02bbb847f183db683fbb9b36aec88fd Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 01:32:57 -0700 Subject: [PATCH] Rehost annual bond issue service branch --- README.md | 6 +- crates/rrt-runtime/src/runtime.rs | 12 ++ crates/rrt-runtime/src/step.rs | 217 ++++++++++++++++++++++++++++++ docs/README.md | 9 +- docs/runtime-rehost-plan.md | 4 +- 5 files changed, 240 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index af2bb7e..9553ace 100644 --- a/README.md +++ b/README.md @@ -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 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 -the shellless dividend-adjustment, stock-repurchase, and stock-issue branches by mutating owned -dividend, company stat-post, outstanding-share, and issue-calendar state instead of stopping at -reader-only diagnostics. +the shellless dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches by +mutating owned dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot +state instead of stopping at reader-only diagnostics. 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 pure reader over owned save-native state instead of a guessed finance-side approximation. diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 3e09fc8..52c03d0 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -3053,6 +3053,18 @@ pub fn runtime_company_average_live_bond_coupon( 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 { + 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 { Some(state.world_restore.partial_year_progress_raw_u8? == 0x0c) } diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 334bd1b..275ecb1 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -17,6 +17,7 @@ use crate::{ 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, }; @@ -436,6 +437,95 @@ fn service_company_annual_finance_policy( 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 => { let mut mutated = false; 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] fn applies_company_effects_for_specific_targets() { let mut state = RuntimeState { diff --git a/docs/README.md b/docs/README.md index 770b40e..d55e96d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 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- - 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 + profit seam; the annual bond, stock-repurchase, and stock-capital issue branches now do too, + 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 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 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 - 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 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 diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 621fb9d..813adf8 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -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 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 -dividend-adjustment, stock-repurchase, and stock-issue branches directly into owned dividend, -company stat-post, outstanding-share, and issue-calendar state. +dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches directly into owned +dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state. ## Why This Boundary