diff --git a/README.md b/README.md index 55fc353..69f239f 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,10 @@ bond table now also contributes both the largest live bond principal and the cho highest-coupon live bond principal into owned company market and annual-finance state, so the stock-capital approval ladder can extend one rehosted owner-state surface instead of hunting another isolated finance leaf. A checked-in -The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we +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 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/import.rs b/crates/rrt-runtime/src/import.rs index 214db65..aac036e 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -851,6 +851,38 @@ fn project_save_slice_components( .world_issue_37_state .as_ref() .map(|state| state.multiplier_value_f32_text.clone()), + stock_issue_and_buyback_policy_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.stock_policy_raw_u8), + bond_issue_and_repayment_policy_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.bond_policy_raw_u8), + bankruptcy_policy_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.bankruptcy_policy_raw_u8), + dividend_policy_raw_u8: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.dividend_policy_raw_u8), + stock_issue_and_buyback_allowed: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.stock_policy_raw_u8 == 0), + bond_issue_and_repayment_allowed: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.bond_policy_raw_u8 == 0), + bankruptcy_allowed: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.bankruptcy_policy_raw_u8 == 0), + dividend_adjustment_allowed: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.dividend_policy_raw_u8 == 0), finance_neighborhood_candidates: save_slice .world_finance_neighborhood_state .as_ref() @@ -5979,6 +6011,14 @@ mod tests { absolute_counter_raw_hex: "0x00000003".to_string(), absolute_counter_mirror_raw_u32: 4, absolute_counter_mirror_raw_hex: "0x00000004".to_string(), + stock_policy_raw_u8: 0, + stock_policy_raw_hex: "0x00".to_string(), + bond_policy_raw_u8: 1, + bond_policy_raw_hex: "0x01".to_string(), + bankruptcy_policy_raw_u8: 0, + bankruptcy_policy_raw_hex: "0x00".to_string(), + dividend_policy_raw_u8: 1, + dividend_policy_raw_hex: "0x01".to_string(), labels: vec![ "current_calendar_tuple_word".to_string(), "current_calendar_tuple_word_2".to_string(), @@ -6263,6 +6303,35 @@ mod tests { import.state.world_restore.issue_37_multiplier_raw_u32, Some(0x3d75c28f) ); + assert_eq!( + import + .state + .world_restore + .stock_issue_and_buyback_policy_raw_u8, + Some(0) + ); + assert_eq!( + import + .state + .world_restore + .bond_issue_and_repayment_policy_raw_u8, + Some(1) + ); + assert_eq!(import.state.world_restore.bankruptcy_policy_raw_u8, Some(0)); + assert_eq!(import.state.world_restore.dividend_policy_raw_u8, Some(1)); + assert_eq!( + import.state.world_restore.stock_issue_and_buyback_allowed, + Some(true) + ); + assert_eq!( + import.state.world_restore.bond_issue_and_repayment_allowed, + Some(false) + ); + assert_eq!(import.state.world_restore.bankruptcy_allowed, Some(true)); + assert_eq!( + import.state.world_restore.dividend_adjustment_allowed, + Some(false) + ); assert_eq!( import .state diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 571d77f..63151f3 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -52,12 +52,13 @@ pub use runtime::{ RUNTIME_WORLD_ISSUE_PRIME_RATE, RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, - RuntimeCompanyAnnualFinanceState, RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, - RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, - RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, - RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, - RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, - RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, + RuntimeCompanyAnnualCreditorPressureState, 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, @@ -65,14 +66,18 @@ pub use runtime::{ RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldIssueState, RuntimeWorldRestoreState, - 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_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, + runtime_world_bond_issue_and_repayment_allowed, runtime_world_dividend_adjustment_allowed, runtime_world_issue_opinion_multiplier, runtime_world_issue_opinion_term_sum_raw, runtime_world_issue_state, runtime_world_prime_rate_baseline, + runtime_world_stock_issue_and_buyback_allowed, }; pub use smp::{ SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index ab21b3b..5b7706f 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -177,6 +177,37 @@ pub struct RuntimeCompanyAnnualFinanceState { pub linked_transit_latch: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualCreditorPressureState { + pub company_id: u32, + #[serde(default)] + pub annual_mode_active: Option, + #[serde(default)] + pub bankruptcy_allowed: Option, + #[serde(default)] + pub years_since_last_bankruptcy: Option, + #[serde(default)] + pub years_since_founding: Option, + pub recent_bad_net_profit_year_count: u32, + #[serde(default)] + pub recent_peak_revenue: Option, + #[serde(default)] + pub recent_three_year_net_profit_total: Option, + #[serde(default)] + pub pressure_ladder_cash_floor: Option, + #[serde(default)] + pub current_cash_plus_slot_12_total: Option, + #[serde(default)] + pub support_adjusted_share_price_floor: Option, + #[serde(default)] + pub support_adjusted_share_price_scalar: Option, + #[serde(default)] + pub current_fuel_cost: Option, + #[serde(default)] + pub current_fuel_cost_floor: Option, + pub eligible_for_bankruptcy_branch: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeTrackPieceCounts { #[serde(default)] @@ -1101,6 +1132,22 @@ pub struct RuntimeWorldRestoreState { #[serde(default)] pub issue_37_multiplier_value_f32_text: Option, #[serde(default)] + pub stock_issue_and_buyback_policy_raw_u8: Option, + #[serde(default)] + pub bond_issue_and_repayment_policy_raw_u8: Option, + #[serde(default)] + pub bankruptcy_policy_raw_u8: Option, + #[serde(default)] + pub dividend_policy_raw_u8: Option, + #[serde(default)] + pub stock_issue_and_buyback_allowed: Option, + #[serde(default)] + pub bond_issue_and_repayment_allowed: Option, + #[serde(default)] + pub bankruptcy_allowed: Option, + #[serde(default)] + pub dividend_adjustment_allowed: Option, + #[serde(default)] pub finance_neighborhood_candidates: Vec, #[serde(default)] pub economic_tuning_mirror_raw_u32: Option, @@ -2718,6 +2765,135 @@ pub fn runtime_company_average_live_bond_coupon( Some(weighted_coupon_sum / total_principal as f64) } +pub fn runtime_world_annual_finance_mode_active(state: &RuntimeState) -> Option { + Some(state.world_restore.partial_year_progress_raw_u8? == 0x0c) +} + +pub fn runtime_world_bankruptcy_allowed(state: &RuntimeState) -> Option { + Some(state.world_restore.bankruptcy_policy_raw_u8? == 0) +} + +pub fn runtime_world_bond_issue_and_repayment_allowed(state: &RuntimeState) -> Option { + Some(state.world_restore.bond_issue_and_repayment_policy_raw_u8? == 0) +} + +pub fn runtime_world_stock_issue_and_buyback_allowed(state: &RuntimeState) -> Option { + Some(state.world_restore.stock_issue_and_buyback_policy_raw_u8? == 0) +} + +pub fn runtime_world_dividend_adjustment_allowed(state: &RuntimeState) -> Option { + Some(state.world_restore.dividend_policy_raw_u8? == 0) +} + +pub fn runtime_company_annual_creditor_pressure_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_cash_plus_slot_12_total = + runtime_company_control_transfer_stat_value_f64(state, company_id, 0x12) + .and_then(runtime_round_f64_to_i64) + .and_then(|slot_12| { + runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64) + .map(|current_cash| current_cash + slot_12) + }); + let support_adjusted_share_price_scalar = + runtime_company_support_adjusted_share_price_scalar_f64(state, company_id) + .and_then(runtime_round_f64_to_i64); + let current_fuel_cost = runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { + family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, + slot_id: 0x09, + }, + ) + .and_then(runtime_round_f64_to_i64); + let recent_bad_net_profit_year_count = annual_finance_state + .trailing_full_year_net_profits + .iter() + .take(3) + .filter(|value| **value < -10_000) + .count() as u32; + let recent_peak_revenue = annual_finance_state + .trailing_full_year_revenues + .iter() + .take(3) + .copied() + .max(); + let recent_three_year_net_profit_total = + if annual_finance_state.trailing_full_year_net_profits.len() >= 3 { + Some( + annual_finance_state + .trailing_full_year_net_profits + .iter() + .take(3) + .sum::(), + ) + } else { + None + }; + let pressure_ladder_cash_floor = recent_peak_revenue.map(|revenue| { + if revenue < 120_000 { + -600_000 + } else if revenue < 230_000 { + -1_100_000 + } else if revenue < 340_000 { + -1_600_000 + } else { + -2_000_000 + } + }); + let support_adjusted_share_price_floor = Some(if recent_bad_net_profit_year_count == 3 { + 20 + } else { + 15 + }); + let current_fuel_cost_floor = pressure_ladder_cash_floor.map(|floor| floor * 8 / 100); + let eligible_for_bankruptcy_branch = runtime_world_annual_finance_mode_active(state) + == Some(true) + && runtime_world_bankruptcy_allowed(state) == Some(true) + && annual_finance_state + .years_since_last_bankruptcy + .is_some_and(|years| years >= 13) + && annual_finance_state + .years_since_founding + .is_some_and(|years| years >= 4) + && recent_bad_net_profit_year_count >= 2 + && current_cash_plus_slot_12_total + .zip(pressure_ladder_cash_floor) + .is_some_and(|(value, floor)| value <= floor) + && support_adjusted_share_price_scalar + .zip(support_adjusted_share_price_floor) + .is_some_and(|(value, floor)| value >= floor) + && current_fuel_cost + .zip(current_fuel_cost_floor) + .is_some_and(|(value, floor)| value <= floor) + && recent_three_year_net_profit_total.is_some_and(|value| value <= -60_000); + Some(RuntimeCompanyAnnualCreditorPressureState { + company_id, + annual_mode_active: runtime_world_annual_finance_mode_active(state), + bankruptcy_allowed: runtime_world_bankruptcy_allowed(state), + years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy, + years_since_founding: annual_finance_state.years_since_founding, + recent_bad_net_profit_year_count, + recent_peak_revenue, + recent_three_year_net_profit_total, + pressure_ladder_cash_floor, + current_cash_plus_slot_12_total, + support_adjusted_share_price_floor, + support_adjusted_share_price_scalar, + current_fuel_cost, + current_fuel_cost_floor, + eligible_for_bankruptcy_branch, + }) +} + pub fn runtime_world_absolute_counter(state: &RuntimeState) -> Option { state.world_restore.absolute_counter_raw_u32 } @@ -3522,6 +3698,14 @@ mod tests { issue_3a_value: None, issue_37_multiplier_raw_u32: None, issue_37_multiplier_value_f32_text: None, + stock_issue_and_buyback_policy_raw_u8: None, + bond_issue_and_repayment_policy_raw_u8: None, + bankruptcy_policy_raw_u8: None, + dividend_policy_raw_u8: None, + stock_issue_and_buyback_allowed: None, + bond_issue_and_repayment_allowed: None, + bankruptcy_allowed: None, + dividend_adjustment_allowed: None, finance_neighborhood_candidates: Vec::new(), economic_tuning_mirror_raw_u32: None, economic_tuning_mirror_value_f32_text: None, @@ -4859,7 +5043,7 @@ mod tests { fn reads_grounded_company_stat_family_slots_from_runtime_state() { let mut year_stat_family_qword_bits = vec![ 0u64; - (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize ]; year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] = @@ -5271,7 +5455,7 @@ mod tests { fn reads_year_relative_company_stat_family_from_saved_market_matrix() { let mut year_stat_family_qword_bits = vec![ 0u64; - (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize ]; let write_year_value = |bits: &mut Vec, slot_id: u32, year_delta: u32, value: f64| { @@ -6626,6 +6810,132 @@ mod tests { assert_eq!(runtime_company_annual_finance_state(&state, 99), None); } + #[test] + fn derives_annual_creditor_pressure_from_rehosted_finance_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + (RUNTIME_COMPANY_STAT_SLOT_COUNT * 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, 0x09, -50_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x0d, -700_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x12, 0.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, 80_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -115_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, -110_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), + partial_year_progress_raw_u8: Some(0x0c), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + 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([( + 7, + RuntimeCompanyMarketState { + founding_year: 1841, + last_bankruptcy_year: 1832, + cached_share_price_raw_u32: 25.0f32.to_bits(), + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!(runtime_world_annual_finance_mode_active(&state), Some(true)); + assert_eq!(runtime_world_bankruptcy_allowed(&state), Some(true)); + let pressure_state = runtime_company_annual_creditor_pressure_state(&state, 7) + .expect("creditor pressure state"); + assert_eq!(pressure_state.recent_bad_net_profit_year_count, 3); + assert_eq!(pressure_state.recent_peak_revenue, Some(100_000)); + assert_eq!( + pressure_state.recent_three_year_net_profit_total, + Some(-65_000) + ); + assert_eq!(pressure_state.pressure_ladder_cash_floor, Some(-600_000)); + assert_eq!( + pressure_state.current_cash_plus_slot_12_total, + Some(-700_000) + ); + assert_eq!(pressure_state.support_adjusted_share_price_floor, Some(20)); + assert_eq!(pressure_state.support_adjusted_share_price_scalar, Some(25)); + assert_eq!(pressure_state.current_fuel_cost, Some(-50_000)); + assert_eq!(pressure_state.current_fuel_cost_floor, Some(-48_000)); + assert!(pressure_state.eligible_for_bankruptcy_branch); + } + #[test] fn reads_company_market_metrics_from_annual_finance_reader() { let current_issue_calendar_word = 0x0101_0726; diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 93a5e00..8affe9e 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -133,6 +133,10 @@ const RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT: usize = 0x3b; const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET: usize = 0x83; const RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET: usize = 0xc1; const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET: usize = 0x0bbf; +const RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET: usize = 0x4a83; +const RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET: usize = 0x4a87; +const RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET: usize = 0x4a8b; +const RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET: usize = 0x4a8f; const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET: usize = 0x0bda; const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS: [usize; 6] = [0x0bde, 0x0be2, 0x0be6, 0x0bea, 0x0bee, 0x0bf2]; @@ -1572,6 +1576,14 @@ pub struct SmpSaveWorldFinanceNeighborhoodProbe { pub current_calendar_tuple_word_2_lane: SmpSaveDwordCandidate, pub absolute_counter_lane: SmpSaveDwordCandidate, pub absolute_counter_mirror_lane: SmpSaveDwordCandidate, + pub stock_policy_raw_u8: u8, + pub stock_policy_raw_hex: String, + pub bond_policy_raw_u8: u8, + pub bond_policy_raw_hex: String, + pub bankruptcy_policy_raw_u8: u8, + pub bankruptcy_policy_raw_hex: String, + pub dividend_policy_raw_u8: u8, + pub dividend_policy_raw_hex: String, pub dword_candidates: Vec, pub evidence: Vec, } @@ -2256,6 +2268,14 @@ pub struct SmpLoadedWorldFinanceNeighborhoodState { pub absolute_counter_raw_hex: String, pub absolute_counter_mirror_raw_u32: u32, pub absolute_counter_mirror_raw_hex: String, + pub stock_policy_raw_u8: u8, + pub stock_policy_raw_hex: String, + pub bond_policy_raw_u8: u8, + pub bond_policy_raw_hex: String, + pub bankruptcy_policy_raw_u8: u8, + pub bankruptcy_policy_raw_hex: String, + pub dividend_policy_raw_u8: u8, + pub dividend_policy_raw_hex: String, pub labels: Vec, pub relative_offsets: Vec, pub relative_offset_hex: Vec, @@ -3500,6 +3520,14 @@ fn derive_loaded_world_finance_neighborhood_state_from_probe( absolute_counter_raw_hex: probe.absolute_counter_lane.raw_u32_hex.clone(), absolute_counter_mirror_raw_u32: probe.absolute_counter_mirror_lane.raw_u32, absolute_counter_mirror_raw_hex: probe.absolute_counter_mirror_lane.raw_u32_hex.clone(), + stock_policy_raw_u8: probe.stock_policy_raw_u8, + stock_policy_raw_hex: probe.stock_policy_raw_hex.clone(), + bond_policy_raw_u8: probe.bond_policy_raw_u8, + bond_policy_raw_hex: probe.bond_policy_raw_hex.clone(), + bankruptcy_policy_raw_u8: probe.bankruptcy_policy_raw_u8, + bankruptcy_policy_raw_hex: probe.bankruptcy_policy_raw_hex.clone(), + dividend_policy_raw_u8: probe.dividend_policy_raw_u8, + dividend_policy_raw_hex: probe.dividend_policy_raw_hex.clone(), labels: probe .dword_candidates .iter() @@ -9100,6 +9128,22 @@ fn parse_save_world_finance_neighborhood_probe( "absolute_calendar_counter_mirror", RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET, )?; + let stock_policy_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET, + )?; + let bond_policy_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET, + )?; + let bankruptcy_policy_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, + )?; + let dividend_policy_raw_u8 = read_u8_at( + bytes, + payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET, + )?; let dword_candidates = build_save_world_finance_neighborhood_candidates(bytes, payload_offset)?; @@ -9119,6 +9163,14 @@ fn parse_save_world_finance_neighborhood_probe( current_calendar_tuple_word_2_lane, absolute_counter_lane, absolute_counter_mirror_lane, + stock_policy_raw_u8, + stock_policy_raw_hex: format!("0x{stock_policy_raw_u8:02x}"), + bond_policy_raw_u8, + bond_policy_raw_hex: format!("0x{bond_policy_raw_u8:02x}"), + bankruptcy_policy_raw_u8, + bankruptcy_policy_raw_hex: format!("0x{bankruptcy_policy_raw_u8:02x}"), + dividend_policy_raw_u8, + dividend_policy_raw_hex: format!("0x{dividend_policy_raw_u8:02x}"), dword_candidates, evidence: vec![ format!( @@ -9133,6 +9185,13 @@ fn parse_save_world_finance_neighborhood_probe( RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET, RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET ), + format!( + "payload +0x{:x}/+0x{:x}/+0x{:x}/+0x{:x} carry the stock, bond, bankruptcy, and dividend finance-policy bytes mirrored from scenario offsets 0x4a87/0x4a8b/0x4a8f/0x4a93", + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET + ), "finance-neighborhood candidates cover the fixed dword strip around the grounded world calendar tuple, absolute-counter, selection-context, and issue-0x37 lanes so broader finance reader closure can build on one rehosted owner surface.".to_string(), ], }); @@ -15868,6 +15927,10 @@ mod tests { bytes[payload_offset + relative_offset..payload_offset + relative_offset + 4] .copy_from_slice(&((index as u32) + 1).to_le_bytes()); } + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET] = 1; + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET] = 2; + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET] = 3; + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET] = 4; let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; bytes[next_chunk_offset..next_chunk_offset + 4] .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); @@ -15897,6 +15960,14 @@ mod tests { assert_eq!(probe.packed_year_word_raw_hex, "0x0001"); assert_eq!(probe.partial_year_progress_raw_u8, 0); assert_eq!(probe.partial_year_progress_raw_hex, "0x00"); + assert_eq!(probe.stock_policy_raw_u8, 1); + assert_eq!(probe.stock_policy_raw_hex, "0x01"); + assert_eq!(probe.bond_policy_raw_u8, 2); + assert_eq!(probe.bond_policy_raw_hex, "0x02"); + assert_eq!(probe.bankruptcy_policy_raw_u8, 3); + assert_eq!(probe.bankruptcy_policy_raw_hex, "0x03"); + assert_eq!(probe.dividend_policy_raw_u8, 4); + assert_eq!(probe.dividend_policy_raw_hex, "0x04"); assert_eq!(probe.current_calendar_tuple_word_lane.value_i32, 1); assert_eq!( probe.current_calendar_tuple_word_2_lane.relative_offset_hex, diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 716b316..a7dcf50 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; use crate::{ - CalendarPoint, RuntimeState, runtime_company_annual_finance_state, - runtime_company_unassigned_share_pool, + CalendarPoint, RuntimeState, runtime_company_annual_creditor_pressure_state, + runtime_company_annual_finance_state, runtime_company_unassigned_share_pool, }; fn raw_u32_to_f32_text(raw: u32) -> String { @@ -50,6 +50,14 @@ pub struct RuntimeSummary { pub world_restore_issue_3a_value: Option, pub world_restore_issue_37_multiplier_raw_u32: Option, pub world_restore_issue_37_multiplier_value_f32_text: Option, + pub world_restore_stock_issue_and_buyback_policy_raw_u8: Option, + pub world_restore_bond_issue_and_repayment_policy_raw_u8: Option, + pub world_restore_bankruptcy_policy_raw_u8: Option, + pub world_restore_dividend_policy_raw_u8: Option, + pub world_restore_stock_issue_and_buyback_allowed: Option, + pub world_restore_bond_issue_and_repayment_allowed: Option, + pub world_restore_bankruptcy_allowed: Option, + pub world_restore_dividend_adjustment_allowed: Option, pub world_restore_finance_neighborhood_count: usize, pub world_restore_finance_neighborhood_labels: Vec, pub world_restore_economic_tuning_mirror_raw_u32: Option, @@ -87,6 +95,16 @@ pub struct RuntimeSummary { pub selected_company_current_issue_age_absolute_counter_delta: Option, pub selected_company_chairman_bonus_year: Option, pub selected_company_chairman_bonus_amount: Option, + pub selected_company_creditor_pressure_recent_bad_net_profit_year_count: Option, + pub selected_company_creditor_pressure_recent_peak_revenue: Option, + pub selected_company_creditor_pressure_recent_three_year_net_profit_total: Option, + pub selected_company_creditor_pressure_cash_floor: Option, + pub selected_company_creditor_pressure_cash_plus_slot_12_total: Option, + pub selected_company_creditor_pressure_share_price_floor: Option, + pub selected_company_creditor_pressure_share_price_scalar: Option, + 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 player_count: usize, pub chairman_profile_count: usize, pub active_chairman_profile_count: usize, @@ -176,6 +194,10 @@ impl RuntimeSummary { let selected_company_annual_finance_state = state .selected_company_id .and_then(|company_id| runtime_company_annual_finance_state(state, company_id)); + let selected_company_creditor_pressure_state = + state.selected_company_id.and_then(|company_id| { + runtime_company_annual_creditor_pressure_state(state, company_id) + }); Self { calendar: state.calendar, calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(), @@ -255,6 +277,24 @@ impl RuntimeSummary { .world_restore .issue_37_multiplier_value_f32_text .clone(), + world_restore_stock_issue_and_buyback_policy_raw_u8: state + .world_restore + .stock_issue_and_buyback_policy_raw_u8, + world_restore_bond_issue_and_repayment_policy_raw_u8: state + .world_restore + .bond_issue_and_repayment_policy_raw_u8, + world_restore_bankruptcy_policy_raw_u8: state.world_restore.bankruptcy_policy_raw_u8, + world_restore_dividend_policy_raw_u8: state.world_restore.dividend_policy_raw_u8, + world_restore_stock_issue_and_buyback_allowed: state + .world_restore + .stock_issue_and_buyback_allowed, + world_restore_bond_issue_and_repayment_allowed: state + .world_restore + .bond_issue_and_repayment_allowed, + world_restore_bankruptcy_allowed: state.world_restore.bankruptcy_allowed, + world_restore_dividend_adjustment_allowed: state + .world_restore + .dividend_adjustment_allowed, world_restore_finance_neighborhood_count: state .world_restore .finance_neighborhood_candidates @@ -372,6 +412,45 @@ impl RuntimeSummary { selected_company_chairman_bonus_amount: selected_company_market_state .map(|market_state| market_state.chairman_bonus_amount) .filter(|amount| *amount != 0), + selected_company_creditor_pressure_recent_bad_net_profit_year_count: + selected_company_creditor_pressure_state + .as_ref() + .map(|pressure_state| pressure_state.recent_bad_net_profit_year_count), + selected_company_creditor_pressure_recent_peak_revenue: + selected_company_creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.recent_peak_revenue), + selected_company_creditor_pressure_recent_three_year_net_profit_total: + selected_company_creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.recent_three_year_net_profit_total), + selected_company_creditor_pressure_cash_floor: selected_company_creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.pressure_ladder_cash_floor), + selected_company_creditor_pressure_cash_plus_slot_12_total: + selected_company_creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.current_cash_plus_slot_12_total), + selected_company_creditor_pressure_share_price_floor: + selected_company_creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.support_adjusted_share_price_floor), + selected_company_creditor_pressure_share_price_scalar: + selected_company_creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.support_adjusted_share_price_scalar), + selected_company_creditor_pressure_current_fuel_cost: + selected_company_creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.current_fuel_cost), + selected_company_creditor_pressure_current_fuel_cost_floor: + selected_company_creditor_pressure_state + .as_ref() + .and_then(|pressure_state| pressure_state.current_fuel_cost_floor), + selected_company_creditor_pressure_eligible_for_bankruptcy_branch: + selected_company_creditor_pressure_state + .as_ref() + .map(|pressure_state| pressure_state.eligible_for_bankruptcy_branch), player_count: state.players.len(), chairman_profile_count: state.chairman_profiles.len(), active_chairman_profile_count: state @@ -1217,6 +1296,14 @@ mod tests { issue_3a_value: Some(4), issue_37_multiplier_raw_u32: Some(0x3d75c28f), issue_37_multiplier_value_f32_text: Some("0.06".to_string()), + stock_issue_and_buyback_policy_raw_u8: Some(0), + bond_issue_and_repayment_policy_raw_u8: Some(1), + bankruptcy_policy_raw_u8: Some(0), + dividend_policy_raw_u8: Some(1), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_allowed: Some(false), + bankruptcy_allowed: Some(true), + dividend_adjustment_allowed: Some(false), economic_tuning_mirror_raw_u32: Some(0x3f46dff5), economic_tuning_mirror_value_f32_text: Some("0.7766201".to_string()), economic_tuning_lane_raw_u32: vec![ @@ -1301,6 +1388,29 @@ mod tests { summary.world_restore_issue_37_multiplier_raw_u32, Some(0x3d75c28f) ); + assert_eq!( + summary.world_restore_stock_issue_and_buyback_policy_raw_u8, + Some(0) + ); + assert_eq!( + summary.world_restore_bond_issue_and_repayment_policy_raw_u8, + Some(1) + ); + assert_eq!(summary.world_restore_bankruptcy_policy_raw_u8, Some(0)); + assert_eq!(summary.world_restore_dividend_policy_raw_u8, Some(1)); + assert_eq!( + summary.world_restore_stock_issue_and_buyback_allowed, + Some(true) + ); + assert_eq!( + summary.world_restore_bond_issue_and_repayment_allowed, + Some(false) + ); + assert_eq!(summary.world_restore_bankruptcy_allowed, Some(true)); + assert_eq!( + summary.world_restore_dividend_adjustment_allowed, + Some(false) + ); assert_eq!( summary .world_restore_issue_37_multiplier_value_f32_text @@ -2177,4 +2287,153 @@ mod tests { assert_eq!(summary.selected_company_chairman_bonus_year, Some(1842)); assert_eq!(summary.selected_company_chairman_bonus_amount, Some(750)); } + + #[test] + fn summarizes_selected_company_creditor_pressure_branch_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, 0x09, -50_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x0d, -700_000.0); + write_current_value(&mut year_stat_family_qword_bits, 0x12, 0.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, 80_000.0); + write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -115_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, -110_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), + partial_year_progress_raw_u8: Some(0x0c), + bankruptcy_policy_raw_u8: Some(0), + bankruptcy_allowed: Some(true), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + 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(7), + 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([( + 7, + crate::RuntimeCompanyMarketState { + founding_year: 1841, + last_bankruptcy_year: 1832, + cached_share_price_raw_u32: 25.0f32.to_bits(), + year_stat_family_qword_bits, + ..crate::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.selected_company_creditor_pressure_recent_bad_net_profit_year_count, + Some(3) + ); + assert_eq!( + summary.selected_company_creditor_pressure_recent_peak_revenue, + Some(100_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_recent_three_year_net_profit_total, + Some(-65_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_cash_floor, + Some(-600_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_cash_plus_slot_12_total, + Some(-700_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_share_price_floor, + Some(20) + ); + assert_eq!( + summary.selected_company_creditor_pressure_share_price_scalar, + Some(25) + ); + assert_eq!( + summary.selected_company_creditor_pressure_current_fuel_cost, + Some(-50_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_current_fuel_cost_floor, + Some(-48_000) + ); + assert_eq!( + summary.selected_company_creditor_pressure_eligible_for_bankruptcy_branch, + Some(true) + ); + } } diff --git a/docs/README.md b/docs/README.md index 29d9153..f4fcb90 100644 --- a/docs/README.md +++ b/docs/README.md @@ -134,7 +134,9 @@ The highest-value next passes are now: and last bankruptcy for later annual finance-policy rehosting; live bond-slot count now travels through that same owned annual-finance state for the stock-capital branch gate, and the grounded bond table now also contributes both the largest live bond principal and the chosen highest-coupon live bond principal into that same - owner-state surface + 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 - 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 ccf7b35..7bfef64 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -225,7 +225,10 @@ owned company market and annual-finance state, matching the stock-capital branch at least two live bonds. The same grounded bond table now also contributes both the largest live bond principal and the chosen highest-coupon live bond principal into owned company market and annual-finance state, so later stock-capital gates can extend a rehosted owner-state seam instead -of guessing another finance leaf. +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. ## Why This Boundary