From dbca9a2e78b55243b419e87c9607b776940d14af Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 22:40:06 -0700 Subject: [PATCH] Carry trailing finance lanes into annual state --- crates/rrt-runtime/src/runtime.rs | 164 ++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 7f1d257..392980b 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -135,6 +135,14 @@ pub struct RuntimeCompanyAnnualFinanceState { #[serde(default)] pub current_partial_year_weight_numerator: Option, #[serde(default)] + pub trailing_full_year_year_words: Vec, + #[serde(default)] + pub trailing_full_year_net_profits: Vec, + #[serde(default)] + pub trailing_full_year_revenues: Vec, + #[serde(default)] + pub trailing_full_year_fuel_costs: Vec, + #[serde(default)] pub current_issue_absolute_counter: Option, #[serde(default)] pub prior_issue_absolute_counter: Option, @@ -2054,6 +2062,29 @@ fn runtime_divide_by_rounded_stat_i64(numerator: f64, denominator: i64) -> Optio Some(numerator / denominator as f64) } +fn runtime_company_trailing_full_year_stat_series( + state: &RuntimeState, + company_id: u32, + slot_id: u32, + full_year_count: u32, +) -> Option<(Vec, Vec)> { + let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?); + let mut year_words = Vec::with_capacity(full_year_count as usize); + let mut values = Vec::with_capacity(full_year_count as usize); + for year_offset in 1..=full_year_count { + let family_id = current_year_word.checked_sub(year_offset)?; + let value = runtime_company_stat_value_f64( + state, + company_id, + RuntimeCompanyStatSelector { family_id, slot_id }, + ) + .and_then(runtime_round_f64_to_i64)?; + year_words.push(family_id); + values.push(value); + } + Some((year_words, values)) +} + pub fn runtime_world_issue_state( state: &RuntimeState, issue_id: u32, @@ -2196,6 +2227,15 @@ pub fn runtime_company_annual_finance_state( } _ => None, }; + let (trailing_full_year_year_words, trailing_full_year_net_profits) = + runtime_company_trailing_full_year_stat_series(state, company_id, 0x2b, 4) + .unwrap_or_default(); + let (_, trailing_full_year_revenues) = + runtime_company_trailing_full_year_stat_series(state, company_id, 0x2c, 4) + .unwrap_or_default(); + let (_, trailing_full_year_fuel_costs) = + runtime_company_trailing_full_year_stat_series(state, company_id, 0x09, 4) + .unwrap_or_default(); Some(RuntimeCompanyAnnualFinanceState { company_id, outstanding_shares: market_state.outstanding_shares, @@ -2216,6 +2256,10 @@ pub fn runtime_company_annual_finance_state( years_since_last_bankruptcy, years_since_last_dividend, current_partial_year_weight_numerator: runtime_world_partial_year_weight_numerator(state), + trailing_full_year_year_words, + trailing_full_year_net_profits, + trailing_full_year_revenues, + trailing_full_year_fuel_costs, current_issue_absolute_counter, prior_issue_absolute_counter, current_issue_age_absolute_counter_delta, @@ -4485,6 +4529,122 @@ mod tests { ); } + #[test] + fn carries_trailing_full_year_finance_lanes_into_annual_finance_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_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(); + }; + for (year_delta, revenue_parts, extra_profit_parts, fuel_cost) in [ + (1, [60.0, 50.0, 40.0, 30.0], [10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0], 18.0), + (2, [50.0, 45.0, 40.0, 35.0], [9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0], 17.0), + (3, [50.0, 40.0, 35.0, 35.0], [8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0], 16.0), + (4, [45.0, 40.0, 35.0, 30.0], [7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0], 15.0), + ] { + for (slot_id, value) in [0x01, 0x02, 0x03, 0x04] + .into_iter() + .zip(revenue_parts) + { + write_year_value(&mut year_stat_family_qword_bits, slot_id, year_delta, value); + } + for (slot_id, value) in [0x05, 0x06, 0x07, 0x08, 0x0a, 0x0b, 0x0c] + .into_iter() + .zip(extra_profit_parts) + { + write_year_value(&mut year_stat_family_qword_bits, slot_id, year_delta, value); + } + write_year_value(&mut year_stat_family_qword_bits, 0x09, year_delta, fuel_cost); + } + + 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), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 7, + current_cash: 125_000, + 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: 2_620, + 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 { + outstanding_shares: 20_000, + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let finance_state = + runtime_company_annual_finance_state(&state, 7).expect("annual finance state"); + assert_eq!( + finance_state.trailing_full_year_year_words, + vec![1844, 1843, 1842, 1841] + ); + assert_eq!( + finance_state.trailing_full_year_net_profits, + vec![247, 229, 211, 193] + ); + assert_eq!(finance_state.trailing_full_year_revenues, vec![180, 170, 160, 150]); + assert_eq!(finance_state.trailing_full_year_fuel_costs, vec![18, 17, 16, 15]); + } + #[test] fn reads_grounded_world_issue_state_from_runtime_restore_state() { let state = RuntimeState { @@ -4894,6 +5054,10 @@ mod tests { years_since_last_bankruptcy: None, years_since_last_dividend: None, current_partial_year_weight_numerator: None, + trailing_full_year_year_words: Vec::new(), + trailing_full_year_net_profits: Vec::new(), + trailing_full_year_revenues: Vec::new(), + trailing_full_year_fuel_costs: Vec::new(), current_issue_absolute_counter: None, prior_issue_absolute_counter: None, current_issue_age_absolute_counter_delta: None,