diff --git a/README.md b/README.md index 69f239f..df6e895 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,10 @@ another isolated finance leaf. A checked-in fixed-world finance-policy seam now also carries the raw stock, bond, bankruptcy, and dividend policy bytes from the `0x32c8` save block, and the first annual creditor-pressure branch now runs headlessly as a pure runtime reader over owned annual-finance state, support-adjusted share price, -and current world finance policy rather than as a notes-only atlas fragment. The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we +and current world finance policy rather than as a notes-only atlas fragment. The later deep- +distress bankruptcy fallback is now rehosted on that same owner surface too, using the save-native +cash reader seam plus the first three trailing net-profit years instead of another ad hoc probe. +The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we should prefer rehosting the owning source state or the real reader/setter family rather than guessing one more derived leaf field from nearby offsets. A checked-in `EventEffects` export now exists too in diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 63151f3..74df880 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -52,13 +52,13 @@ pub use runtime::{ RUNTIME_WORLD_ISSUE_PRIME_RATE, RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, - RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualFinanceState, - RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, - RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, RuntimeCompanyMetric, - RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, RuntimeCompanyTarget, - RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, - RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, - RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, + RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState, + RuntimeCompanyAnnualFinanceState, RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, + RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, + RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, + RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, + RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, + RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, @@ -66,11 +66,11 @@ pub use runtime::{ RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldIssueState, RuntimeWorldRestoreState, - runtime_company_annual_creditor_pressure_state, runtime_company_annual_finance_state, - runtime_company_assigned_share_pool, runtime_company_average_live_bond_coupon, - runtime_company_book_value_per_share, runtime_company_credit_rating, - runtime_company_investor_confidence, runtime_company_management_attitude, - runtime_company_market_value, runtime_company_prime_rate, + runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_state, + runtime_company_annual_finance_state, runtime_company_assigned_share_pool, + runtime_company_average_live_bond_coupon, runtime_company_book_value_per_share, + runtime_company_credit_rating, runtime_company_investor_confidence, + runtime_company_management_attitude, runtime_company_market_value, runtime_company_prime_rate, runtime_company_recent_per_share_subscore, runtime_company_stat_value, runtime_company_stat_value_f64, runtime_company_unassigned_share_pool, runtime_world_annual_finance_mode_active, runtime_world_bankruptcy_allowed, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 5b7706f..92ed875 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -208,6 +208,25 @@ pub struct RuntimeCompanyAnnualCreditorPressureState { pub eligible_for_bankruptcy_branch: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualDeepDistressState { + pub company_id: u32, + #[serde(default)] + pub bankruptcy_allowed: Option, + #[serde(default)] + pub years_since_founding: Option, + #[serde(default)] + pub years_since_last_bankruptcy: Option, + #[serde(default)] + pub current_cash: Option, + pub recent_first_three_net_profit_years: Vec, + #[serde(default)] + pub deep_distress_cash_floor: Option, + #[serde(default)] + pub deep_distress_net_profit_floor: Option, + pub eligible_for_bankruptcy_fallback: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeTrackPieceCounts { #[serde(default)] @@ -2894,6 +2913,52 @@ pub fn runtime_company_annual_creditor_pressure_state( }) } +pub fn runtime_company_annual_deep_distress_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let recent_first_three_net_profit_years = annual_finance_state + .trailing_full_year_net_profits + .iter() + .take(3) + .copied() + .collect::>(); + let deep_distress_cash_floor = Some(-300_000); + let deep_distress_net_profit_floor = Some(-20_000); + let eligible_for_bankruptcy_fallback = runtime_world_bankruptcy_allowed(state) == Some(true) + && current_cash + .zip(deep_distress_cash_floor) + .is_some_and(|(value, floor)| value <= floor) + && annual_finance_state + .years_since_founding + .is_some_and(|years| years >= 3) + && recent_first_three_net_profit_years.len() == 3 + && recent_first_three_net_profit_years + .iter() + .all(|value| *value <= deep_distress_net_profit_floor.unwrap()) + && annual_finance_state + .years_since_last_bankruptcy + .is_some_and(|years| years >= 5); + Some(RuntimeCompanyAnnualDeepDistressState { + company_id, + bankruptcy_allowed: runtime_world_bankruptcy_allowed(state), + years_since_founding: annual_finance_state.years_since_founding, + years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy, + current_cash, + recent_first_three_net_profit_years, + deep_distress_cash_floor, + deep_distress_net_profit_floor, + eligible_for_bankruptcy_fallback, + }) +} + pub fn runtime_world_absolute_counter(state: &RuntimeState) -> Option { state.world_restore.absolute_counter_raw_u32 } @@ -6936,6 +7001,118 @@ mod tests { assert!(pressure_state.eligible_for_bankruptcy_branch); } + #[test] + fn derives_annual_deep_distress_bankruptcy_fallback_from_rehosted_finance_owner_state() { + 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, slot_id: u32, value: f64| { + let index = (slot_id * 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 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, -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 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 { + packed_year_word_raw_u16: Some(1845), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 9, + 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([( + 9, + RuntimeCompanyMarketState { + founding_year: 1841, + last_bankruptcy_year: 1840, + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let pressure_state = + runtime_company_annual_deep_distress_state(&state, 9).expect("deep distress state"); + assert_eq!(pressure_state.current_cash, Some(-350_000)); + assert_eq!( + pressure_state.recent_first_three_net_profit_years, + vec![-25_000, -23_000, -21_000] + ); + assert_eq!(pressure_state.deep_distress_cash_floor, Some(-300_000)); + assert_eq!(pressure_state.deep_distress_net_profit_floor, Some(-20_000)); + assert!(pressure_state.eligible_for_bankruptcy_fallback); + } + #[test] fn reads_company_market_metrics_from_annual_finance_reader() { let current_issue_calendar_word = 0x0101_0726; diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index a7dcf50..d0b0b03 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -2,7 +2,8 @@ use serde::{Deserialize, Serialize}; use crate::{ CalendarPoint, RuntimeState, runtime_company_annual_creditor_pressure_state, - runtime_company_annual_finance_state, runtime_company_unassigned_share_pool, + runtime_company_annual_deep_distress_state, runtime_company_annual_finance_state, + runtime_company_unassigned_share_pool, }; fn raw_u32_to_f32_text(raw: u32) -> String { @@ -105,6 +106,11 @@ pub struct RuntimeSummary { pub selected_company_creditor_pressure_current_fuel_cost: Option, pub selected_company_creditor_pressure_current_fuel_cost_floor: Option, pub selected_company_creditor_pressure_eligible_for_bankruptcy_branch: Option, + pub selected_company_deep_distress_current_cash: Option, + pub selected_company_deep_distress_recent_first_three_net_profit_years: Vec, + pub selected_company_deep_distress_cash_floor: Option, + pub selected_company_deep_distress_net_profit_floor: Option, + pub selected_company_deep_distress_eligible_for_bankruptcy_fallback: Option, pub player_count: usize, pub chairman_profile_count: usize, pub active_chairman_profile_count: usize, @@ -198,6 +204,9 @@ impl RuntimeSummary { state.selected_company_id.and_then(|company_id| { runtime_company_annual_creditor_pressure_state(state, company_id) }); + let selected_company_deep_distress_state = state + .selected_company_id + .and_then(|company_id| runtime_company_annual_deep_distress_state(state, company_id)); Self { calendar: state.calendar, calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(), @@ -451,6 +460,26 @@ impl RuntimeSummary { selected_company_creditor_pressure_state .as_ref() .map(|pressure_state| pressure_state.eligible_for_bankruptcy_branch), + selected_company_deep_distress_current_cash: selected_company_deep_distress_state + .as_ref() + .and_then(|pressure_state| pressure_state.current_cash), + selected_company_deep_distress_recent_first_three_net_profit_years: + selected_company_deep_distress_state + .as_ref() + .map(|pressure_state| { + pressure_state.recent_first_three_net_profit_years.clone() + }) + .unwrap_or_default(), + selected_company_deep_distress_cash_floor: selected_company_deep_distress_state + .as_ref() + .and_then(|pressure_state| pressure_state.deep_distress_cash_floor), + selected_company_deep_distress_net_profit_floor: selected_company_deep_distress_state + .as_ref() + .and_then(|pressure_state| pressure_state.deep_distress_net_profit_floor), + selected_company_deep_distress_eligible_for_bankruptcy_fallback: + selected_company_deep_distress_state + .as_ref() + .map(|pressure_state| pressure_state.eligible_for_bankruptcy_fallback), player_count: state.players.len(), chairman_profile_count: state.chairman_profiles.len(), active_chairman_profile_count: state @@ -2436,4 +2465,129 @@ mod tests { Some(true) ); } + + #[test] + fn summarizes_selected_company_deep_distress_fallback_state() { + 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, 0x0d, -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 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 { + packed_year_word_raw_u16: Some(1845), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 9, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::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: Some(9), + 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([( + 9, + crate::RuntimeCompanyMarketState { + founding_year: 1841, + last_bankruptcy_year: 1840, + year_stat_family_qword_bits, + ..crate::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.selected_company_deep_distress_current_cash, + Some(-350_000) + ); + assert_eq!( + summary.selected_company_deep_distress_recent_first_three_net_profit_years, + vec![-25_000, -23_000, -21_000] + ); + assert_eq!( + summary.selected_company_deep_distress_cash_floor, + Some(-300_000) + ); + assert_eq!( + summary.selected_company_deep_distress_net_profit_floor, + Some(-20_000) + ); + assert_eq!( + summary.selected_company_deep_distress_eligible_for_bankruptcy_fallback, + Some(true) + ); + } } diff --git a/docs/README.md b/docs/README.md index f4fcb90..174b025 100644 --- a/docs/README.md +++ b/docs/README.md @@ -136,7 +136,9 @@ The highest-value next passes are now: the largest live bond principal and the chosen highest-coupon live bond principal into that same owner-state surface; the same fixed-world save block now also carries the raw stock, bond, 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 + 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- + net-profit surface too - 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 7bfef64..30cc96a 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -228,7 +228,9 @@ annual-finance state, so later stock-capital gates can extend a rehosted owner-s of guessing another finance leaf. The same fixed-world save block now also carries the raw stock, bond, bankruptcy, and dividend finance-policy bytes, and the earliest annual creditor-pressure bankruptcy branch now runs as a pure runtime reader over owned annual-finance state, support- -adjusted share price, and those policy bytes rather than staying in atlas prose only. +adjusted share price, and those policy bytes rather than staying in atlas prose only. The later +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. ## Why This Boundary