From b87216a55603cda60328e253de9584cd2942b3eb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 01:38:42 -0700 Subject: [PATCH] Rehost annual bankruptcy service branches --- README.md | 7 +- crates/rrt-runtime/src/step.rs | 357 +++++++++++++++++++++++++++++++++ docs/README.md | 3 +- docs/runtime-rehost-plan.md | 5 +- 4 files changed, 366 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9553ace..4c605fc 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,10 @@ 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, 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 shellless creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, +stock-repurchase, stock-issue, and bond-issue branches by mutating owned company activity, +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/step.rs b/crates/rrt-runtime/src/step.rs index 275ecb1..3a33b56 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -306,6 +306,45 @@ fn service_post_company_stat_delta( } } +fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool { + let Some(bankruptcy_year) = state + .world_restore + .packed_year_word_raw_u16 + .map(u32::from) + .or_else(|| Some(state.calendar.year)) + else { + return false; + }; + + let mut company_mutated = false; + if let Some(company) = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + { + company.current_cash = 0; + company.debt = 0; + company.active = false; + if state.selected_company_id == Some(company_id) { + state.selected_company_id = None; + } + company_mutated = true; + } + + if let Some(market_state) = state.service_state.company_market_state.get_mut(&company_id) { + market_state.last_bankruptcy_year = bankruptcy_year; + market_state.bond_count = 0; + market_state.largest_live_bond_principal = None; + market_state.highest_coupon_live_bond_principal = None; + market_state.live_bond_slots.clear(); + company_mutated = true; + } + + let retired_company_ids = vec![company_id]; + retire_matching_trains(&mut state.trains, Some(&retired_company_ids), None, None); + company_mutated +} + fn service_company_annual_finance_policy( state: &mut RuntimeState, service_events: &mut Vec, @@ -369,6 +408,13 @@ fn service_company_annual_finance_policy( mutated_company_ids.insert(company_id); } } + crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy + | crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => { + if service_apply_company_bankruptcy(state, company_id) { + applied_effect_count += 1; + mutated_company_ids.insert(company_id); + } + } crate::RuntimeCompanyAnnualFinancePolicyAction::StockIssue => { let Some(issue_state) = runtime_company_annual_stock_issue_state(state, company_id) else { @@ -2952,6 +2998,317 @@ mod tests { ); } + #[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 + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = + (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value( + &mut year_stat_family_qword_bits, + crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -200_000.0, + ); + write_current_value(&mut year_stat_family_qword_bits, 0x12, -500_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 95_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -125_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -115_000.0); + + 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), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..crate::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::RuntimeCompany { + company_id: 31, + controller_kind: crate::RuntimeCompanyControllerKind::Unknown, + current_cash: 0, + debt: 200_000, + credit_rating_score: None, + prime_rate: None, + 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(31), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: vec![crate::RuntimeTrain { + train_id: 88, + owner_company_id: 31, + territory_id: None, + locomotive_name: Some("Mikado".to_string()), + active: true, + retired: false, + }], + 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([( + 31, + crate::RuntimeCompanyMarketState { + outstanding_shares: 10_000, + bond_count: 1, + largest_live_bond_principal: Some(500_000), + highest_coupon_live_bond_principal: Some(500_000), + cached_share_price_raw_u32: 25.0f32.to_bits(), + founding_year: 1841, + last_bankruptcy_year: 1832, + year_stat_family_qword_bits, + live_bond_slots: vec![crate::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 500_000, + coupon_rate_raw_u32: 0.08f32.to_bits(), + }], + ..crate::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply creditor-pressure bankruptcy"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&31), + Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy) + ); + assert!(!state.companies[0].active); + assert_eq!(state.companies[0].current_cash, 0); + assert_eq!(state.companies[0].debt, 0); + assert_eq!(state.selected_company_id, None); + assert!(state.trains[0].retired); + assert_eq!( + state.service_state.company_market_state[&31].last_bankruptcy_year, + 1845 + ); + assert_eq!(state.service_state.company_market_state[&31].bond_count, 0); + assert!( + state.service_state.company_market_state[&31] + .live_bond_slots + .is_empty() + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![31]) + ); + } + + #[test] + fn periodic_boundary_applies_deep_distress_bankruptcy_fallback_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 + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + let write_prior_year_value = + |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { + let index = + (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value( + &mut year_stat_family_qword_bits, + crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + -350_000.0, + ); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0); + + 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), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..crate::RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![crate::RuntimeCompany { + company_id: 32, + controller_kind: crate::RuntimeCompanyControllerKind::Unknown, + current_cash: 0, + debt: 50_000, + credit_rating_score: None, + prime_rate: None, + 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(32), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: vec![crate::RuntimeTrain { + train_id: 89, + owner_company_id: 32, + territory_id: None, + locomotive_name: Some("Orca".to_string()), + active: true, + retired: false, + }], + 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([( + 32, + crate::RuntimeCompanyMarketState { + outstanding_shares: 8_000, + bond_count: 1, + largest_live_bond_principal: Some(250_000), + highest_coupon_live_bond_principal: Some(250_000), + founding_year: 1841, + last_bankruptcy_year: 1840, + year_stat_family_qword_bits, + live_bond_slots: vec![crate::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 250_000, + coupon_rate_raw_u32: 0.07f32.to_bits(), + }], + ..crate::RuntimeCompanyMarketState::default() + }, + )]), + ..crate::RuntimeServiceState::default() + }, + }; + + let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary) + .expect("periodic boundary should apply deep-distress bankruptcy fallback"); + + assert_eq!( + state.service_state.annual_finance_last_actions.get(&32), + Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback) + ); + assert!(!state.companies[0].active); + assert_eq!(state.companies[0].current_cash, 0); + assert_eq!(state.companies[0].debt, 0); + assert_eq!(state.selected_company_id, None); + assert!(state.trains[0].retired); + assert_eq!( + state.service_state.company_market_state[&32].last_bankruptcy_year, + 1845 + ); + assert_eq!(state.service_state.company_market_state[&32].bond_count, 0); + assert!( + state.service_state.company_market_state[&32] + .live_bond_slots + .is_empty() + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "annual_finance_policy" + && event.applied_effect_count == 1 + && event.mutated_company_ids == vec![32]) + ); + } + #[test] fn applies_company_effects_for_specific_targets() { let mut state = RuntimeState { diff --git a/docs/README.md b/docs/README.md index d55e96d..a061e96 100644 --- a/docs/README.md +++ b/docs/README.md @@ -149,7 +149,8 @@ The highest-value next passes are now: 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, stock-issue, and bond-issue branches against owned runtime state + creditor-pressure-bankruptcy, deep-distress-bankruptcy, 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 813adf8..d5d6cc9 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -244,8 +244,9 @@ 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, stock-issue, and bond-issue branches directly into owned -dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state. +creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase, +stock-issue, and bond-issue branches directly into owned dividend, company stat-post, +outstanding-share, issue-calendar, live bond-slot, and company activity state. ## Why This Boundary