From 08f44debc7a44edb0b63a54564e5302776855cef Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 21:07:41 -0700 Subject: [PATCH] Derive annual finance timing inputs --- README.md | 4 +- crates/rrt-fixtures/src/schema.rs | 60 ++++++++++++++++ crates/rrt-runtime/src/runtime.rs | 110 ++++++++++++++++++++++++++++++ crates/rrt-runtime/src/summary.rs | 15 ++++ docs/README.md | 4 +- docs/runtime-rehost-plan.md | 4 +- 6 files changed, 194 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 25a62fd..cc992d2 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,9 @@ so future issue-`0x38/0x39` closure can build on a broader owned restore-state w another narrow one-off probe. The next company-side seam is now bundled too: a shared company market reader now exposes outstanding shares, assigned shares, public float, rounded cached share price, salary lanes, bonus amount, and issue-calendar words from the owned annual-finance state -instead of leaving that logic spread across summary helpers. A checked-in +instead of leaving that logic spread across summary helpers. The same annual-finance state now also +derives elapsed years since founding, last dividend, and last bankruptcy from the runtime calendar, +which lines up directly with the grounded annual finance-policy gates in the atlas. A checked-in 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 diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index 2088cb8..32997f8 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -108,6 +108,18 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub selected_company_stat_band_root_1c47_count: Option, #[serde(default)] + pub selected_company_last_dividend_year: Option, + #[serde(default)] + pub selected_company_years_since_founding: Option, + #[serde(default)] + pub selected_company_years_since_last_bankruptcy: Option, + #[serde(default)] + pub selected_company_years_since_last_dividend: Option, + #[serde(default)] + pub selected_company_chairman_bonus_year: Option, + #[serde(default)] + pub selected_company_chairman_bonus_amount: Option, + #[serde(default)] pub player_count: Option, #[serde(default)] pub chairman_profile_count: Option, @@ -669,6 +681,54 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(value) = self.selected_company_last_dividend_year { + if actual.selected_company_last_dividend_year != Some(value) { + mismatches.push(format!( + "selected_company_last_dividend_year mismatch: expected {value}, got {:?}", + actual.selected_company_last_dividend_year + )); + } + } + if let Some(value) = self.selected_company_years_since_founding { + if actual.selected_company_years_since_founding != Some(value) { + mismatches.push(format!( + "selected_company_years_since_founding mismatch: expected {value}, got {:?}", + actual.selected_company_years_since_founding + )); + } + } + if let Some(value) = self.selected_company_years_since_last_bankruptcy { + if actual.selected_company_years_since_last_bankruptcy != Some(value) { + mismatches.push(format!( + "selected_company_years_since_last_bankruptcy mismatch: expected {value}, got {:?}", + actual.selected_company_years_since_last_bankruptcy + )); + } + } + if let Some(value) = self.selected_company_years_since_last_dividend { + if actual.selected_company_years_since_last_dividend != Some(value) { + mismatches.push(format!( + "selected_company_years_since_last_dividend mismatch: expected {value}, got {:?}", + actual.selected_company_years_since_last_dividend + )); + } + } + if let Some(value) = self.selected_company_chairman_bonus_year { + if actual.selected_company_chairman_bonus_year != Some(value) { + mismatches.push(format!( + "selected_company_chairman_bonus_year mismatch: expected {value}, got {:?}", + actual.selected_company_chairman_bonus_year + )); + } + } + if let Some(value) = self.selected_company_chairman_bonus_amount { + if actual.selected_company_chairman_bonus_amount != Some(value) { + mismatches.push(format!( + "selected_company_chairman_bonus_amount mismatch: expected {value}, got {:?}", + actual.selected_company_chairman_bonus_amount + )); + } + } if let Some(count) = self.player_count { if actual.player_count != count { mismatches.push(format!( diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 68f4687..93e155e 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -107,6 +107,12 @@ pub struct RuntimeCompanyAnnualFinanceState { pub founding_year: u32, pub last_bankruptcy_year: u32, pub last_dividend_year: u32, + #[serde(default)] + pub years_since_founding: Option, + #[serde(default)] + pub years_since_last_bankruptcy: Option, + #[serde(default)] + pub years_since_last_dividend: Option, pub current_issue_calendar_word: u32, pub prior_issue_calendar_word: u32, pub city_connection_latch: bool, @@ -1904,6 +1910,16 @@ pub fn runtime_company_annual_finance_state( let market_state = state.service_state.company_market_state.get(&company_id)?; let assigned_share_pool = runtime_company_assigned_share_pool(state, company_id)?; let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id)?; + let years_since_founding = + derive_runtime_company_elapsed_years(state.calendar.year, market_state.founding_year); + let years_since_last_bankruptcy = derive_runtime_company_elapsed_years( + state.calendar.year, + market_state.last_bankruptcy_year, + ); + let years_since_last_dividend = derive_runtime_company_elapsed_years( + state.calendar.year, + market_state.last_dividend_year, + ); Some(RuntimeCompanyAnnualFinanceState { company_id, outstanding_shares: market_state.outstanding_shares, @@ -1917,6 +1933,9 @@ pub fn runtime_company_annual_finance_state( founding_year: market_state.founding_year, last_bankruptcy_year: market_state.last_bankruptcy_year, last_dividend_year: market_state.last_dividend_year, + years_since_founding, + years_since_last_bankruptcy, + years_since_last_dividend, current_issue_calendar_word: market_state.current_issue_calendar_word, prior_issue_calendar_word: market_state.prior_issue_calendar_word, city_connection_latch: market_state.city_connection_latch, @@ -1970,6 +1989,13 @@ fn rounded_cached_share_price_i64(raw_u32: u32) -> Option { Some(value.round() as i64) } +fn derive_runtime_company_elapsed_years(current_year: u32, prior_year: u32) -> Option { + if prior_year == 0 || prior_year > current_year { + return None; + } + Some(current_year - prior_year) +} + fn derive_runtime_chairman_holdings_share_price_total( holdings_by_company: &BTreeMap, company_share_prices: &BTreeMap, @@ -4231,6 +4257,9 @@ mod tests { founding_year: 1831, last_bankruptcy_year: 0, last_dividend_year: 1841, + years_since_founding: None, + years_since_last_bankruptcy: None, + years_since_last_dividend: None, current_issue_calendar_word: 5, prior_issue_calendar_word: 4, city_connection_latch: true, @@ -4385,4 +4414,85 @@ mod tests { None ); } + + #[test] + fn derives_elapsed_company_finance_years_from_calendar_and_saved_market_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1844, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 3, + 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([( + 3, + RuntimeCompanyMarketState { + outstanding_shares: 10_000, + founding_year: 1838, + last_bankruptcy_year: 1841, + last_dividend_year: 1843, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let finance_state = + runtime_company_annual_finance_state(&state, 3).expect("finance state should derive"); + assert_eq!(finance_state.years_since_founding, Some(6)); + assert_eq!(finance_state.years_since_last_bankruptcy, Some(3)); + assert_eq!(finance_state.years_since_last_dividend, Some(1)); + } } diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 4b52bb8..8242ec5 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -59,6 +59,9 @@ pub struct RuntimeSummary { pub selected_company_stat_band_root_0d7f_count: usize, pub selected_company_stat_band_root_1c47_count: usize, pub selected_company_last_dividend_year: Option, + pub selected_company_years_since_founding: Option, + pub selected_company_years_since_last_bankruptcy: Option, + pub selected_company_years_since_last_dividend: Option, pub selected_company_chairman_bonus_year: Option, pub selected_company_chairman_bonus_amount: Option, pub player_count: usize, @@ -282,6 +285,15 @@ impl RuntimeSummary { selected_company_last_dividend_year: selected_company_market_state .map(|market_state| market_state.last_dividend_year) .filter(|year| *year != 0), + selected_company_years_since_founding: selected_company_annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.years_since_founding), + selected_company_years_since_last_bankruptcy: selected_company_annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.years_since_last_bankruptcy), + selected_company_years_since_last_dividend: selected_company_annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.years_since_last_dividend), selected_company_chairman_bonus_year: selected_company_market_state .map(|market_state| market_state.chairman_bonus_year) .filter(|year| *year != 0), @@ -2026,6 +2038,9 @@ mod tests { assert_eq!(summary.selected_company_stat_band_root_0d7f_count, 1); assert_eq!(summary.selected_company_stat_band_root_1c47_count, 1); assert_eq!(summary.selected_company_last_dividend_year, Some(1841)); + assert_eq!(summary.selected_company_years_since_founding, None); + assert_eq!(summary.selected_company_years_since_last_bankruptcy, None); + assert_eq!(summary.selected_company_years_since_last_dividend, None); assert_eq!(summary.selected_company_chairman_bonus_year, Some(1842)); assert_eq!(summary.selected_company_chairman_bonus_amount, Some(750)); } diff --git a/docs/README.md b/docs/README.md index cd319d9..2e9e61f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -124,7 +124,9 @@ The highest-value next passes are now: reader seam for assigned shares, public float, and rounded cached share price; the fixed-world finance neighborhood is now widened to 16 dwords rooted at `[world+0x11]` so later issue-family closure can target a broader owned restore-state window; the same annual-finance state now also - feeds a shared company market reader for stock-capital, salary, bonus, and issue-calendar values + feeds a shared company market reader for stock-capital, salary, bonus, and issue-calendar values, + and now derives elapsed years since founding, last dividend, and last bankruptcy for later annual + finance-policy rehosting - 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 f386ff5..b59fb9c 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -214,7 +214,9 @@ rooted at `[world+0x11]`, so later issue-`0x38/0x39` closure can build on a broa restore-state window instead of another narrow probe. The same owned company annual-finance state now also drives a shared company market reader seam for stock-capital, salary, bonus, and issue-calendar values, which is a better base for shellless finance simulation than summary-only -helpers. +helpers. That same owned annual-finance state now also derives elapsed years since founding, last +dividend, and last bankruptcy from the runtime calendar, which lines up directly with the grounded +annual finance-policy gates in the atlas. ## Why This Boundary