diff --git a/README.md b/README.md index 78f37a6..7f17102 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,9 @@ reader seam is now also rehosted for the grounded `0x37` investor-confidence lan save-native world-restore state. The selected-company summary path now also exposes the unassigned share pool derived from outstanding shares minus chairman-held shares, so later dividend / stock-capital logic can extend one owned market reader instead of another ad hoc -counter. A checked-in +counter. The next bundled annual-finance reader seam is now rehosted on top of that same market +state too, deriving assigned shares, public float, and rounded cached share price from one shared +company market reader instead of scattering more finance helpers across the runtime. 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 2e63ffd..2088cb8 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -92,8 +92,12 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub selected_company_outstanding_shares: Option, #[serde(default)] + pub selected_company_assigned_share_pool: Option, + #[serde(default)] pub selected_company_unassigned_share_pool: Option, #[serde(default)] + pub selected_company_cached_share_price: Option, + #[serde(default)] pub selected_company_cached_share_price_value_f32_text: Option, #[serde(default)] pub selected_company_mutable_support_scalar_value_f32_text: Option, @@ -593,6 +597,14 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(value) = self.selected_company_assigned_share_pool { + if actual.selected_company_assigned_share_pool != Some(value) { + mismatches.push(format!( + "selected_company_assigned_share_pool mismatch: expected {value}, got {:?}", + actual.selected_company_assigned_share_pool + )); + } + } if let Some(value) = self.selected_company_unassigned_share_pool { if actual.selected_company_unassigned_share_pool != Some(value) { mismatches.push(format!( @@ -601,6 +613,14 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(value) = self.selected_company_cached_share_price { + if actual.selected_company_cached_share_price != Some(value) { + mismatches.push(format!( + "selected_company_cached_share_price mismatch: expected {value}, got {:?}", + actual.selected_company_cached_share_price + )); + } + } if let Some(value) = &self.selected_company_cached_share_price_value_f32_text { if actual .selected_company_cached_share_price_value_f32_text diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 8cce91e..a081a73 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -46,12 +46,12 @@ pub use pk4::{ pub use runtime::{ RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, - RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope, - RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric, - RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, RuntimeCompanyTarget, - RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, - RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, - RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, + RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyAnnualFinanceState, + RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketState, + RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, + RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, + RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, + RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, @@ -61,8 +61,8 @@ pub use runtime::{ RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldIssueState, RuntimeWorldRestoreState, RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE, - runtime_company_stat_value, runtime_company_unassigned_share_pool, - runtime_world_issue_state, + runtime_company_annual_finance_state, runtime_company_assigned_share_pool, + runtime_company_stat_value, runtime_company_unassigned_share_pool, runtime_world_issue_state, }; 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 21caf32..ef5fe72 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -92,6 +92,27 @@ pub struct RuntimeCompanyMarketState { pub stat_band_root_1c47_candidates: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualFinanceState { + pub company_id: u32, + pub outstanding_shares: u32, + pub assigned_share_pool: u32, + pub unassigned_share_pool: u32, + #[serde(default)] + pub cached_share_price: Option, + pub chairman_salary_baseline: u32, + pub chairman_salary_current: u32, + pub chairman_bonus_year: u32, + pub chairman_bonus_amount: i32, + pub founding_year: u32, + pub last_bankruptcy_year: u32, + pub last_dividend_year: u32, + pub current_issue_calendar_word: u32, + pub prior_issue_calendar_word: u32, + pub city_connection_latch: bool, + pub linked_transit_latch: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeTrackPieceCounts { #[serde(default)] @@ -1844,14 +1865,51 @@ pub fn runtime_company_unassigned_share_pool(state: &RuntimeState, company_id: u .company_market_state .get(&company_id)? .outstanding_shares; - let assigned_shares = state - .chairman_profiles - .iter() - .filter_map(|profile| profile.company_holdings.get(&company_id).copied()) - .sum::(); + let assigned_shares = runtime_company_assigned_share_pool(state, company_id)?; Some(outstanding_shares.saturating_sub(assigned_shares)) } +pub fn runtime_company_assigned_share_pool(state: &RuntimeState, company_id: u32) -> Option { + state + .service_state + .company_market_state + .get(&company_id)?; + Some( + state + .chairman_profiles + .iter() + .filter_map(|profile| profile.company_holdings.get(&company_id).copied()) + .sum::(), + ) +} + +pub fn runtime_company_annual_finance_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + 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)?; + Some(RuntimeCompanyAnnualFinanceState { + company_id, + outstanding_shares: market_state.outstanding_shares, + assigned_share_pool, + unassigned_share_pool, + cached_share_price: rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32), + chairman_salary_baseline: market_state.chairman_salary_baseline, + chairman_salary_current: market_state.chairman_salary_current, + chairman_bonus_year: market_state.chairman_bonus_year, + chairman_bonus_amount: market_state.chairman_bonus_amount, + founding_year: market_state.founding_year, + last_bankruptcy_year: market_state.last_bankruptcy_year, + last_dividend_year: market_state.last_dividend_year, + 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, + linked_transit_latch: market_state.linked_transit_latch, + }) +} + fn rounded_cached_share_price_i64(raw_u32: u32) -> Option { let value = f32::from_bits(raw_u32); if !value.is_finite() { @@ -4001,4 +4059,136 @@ mod tests { assert_eq!(runtime_company_unassigned_share_pool(&state, 4), Some(4_500)); assert_eq!(runtime_company_unassigned_share_pool(&state, 99), None); } + + #[test] + fn derives_company_annual_finance_state_from_owned_runtime_market_state() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + 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: 4, + 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![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(4), + company_holdings: BTreeMap::from([(4, 8_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 0, + linked_company_id: None, + company_holdings: BTreeMap::from([(4, 7_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + ], + 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([( + 4, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + cached_share_price_raw_u32: 0x42200000, + chairman_salary_baseline: 24, + chairman_salary_current: 30, + chairman_bonus_year: 1842, + chairman_bonus_amount: 750, + founding_year: 1831, + last_bankruptcy_year: 0, + last_dividend_year: 1841, + current_issue_calendar_word: 5, + prior_issue_calendar_word: 4, + city_connection_latch: true, + linked_transit_latch: false, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!(runtime_company_assigned_share_pool(&state, 4), Some(15_500)); + assert_eq!( + runtime_company_annual_finance_state(&state, 4), + Some(RuntimeCompanyAnnualFinanceState { + company_id: 4, + outstanding_shares: 20_000, + assigned_share_pool: 15_500, + unassigned_share_pool: 4_500, + cached_share_price: Some(40), + chairman_salary_baseline: 24, + chairman_salary_current: 30, + chairman_bonus_year: 1842, + chairman_bonus_amount: 750, + founding_year: 1831, + last_bankruptcy_year: 0, + last_dividend_year: 1841, + current_issue_calendar_word: 5, + prior_issue_calendar_word: 4, + city_connection_latch: true, + linked_transit_latch: false, + }) + ); + assert_eq!(runtime_company_assigned_share_pool(&state, 99), None); + assert_eq!(runtime_company_annual_finance_state(&state, 99), None); + } } diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 16f589c..4b52bb8 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::{runtime_company_unassigned_share_pool, CalendarPoint, RuntimeState}; +use crate::{ + runtime_company_annual_finance_state, runtime_company_unassigned_share_pool, CalendarPoint, + RuntimeState, +}; fn raw_u32_to_f32_text(raw: u32) -> String { format!("{:.6}", f32::from_bits(raw)) @@ -47,7 +50,9 @@ pub struct RuntimeSummary { pub active_company_count: usize, pub company_market_state_owner_count: usize, pub selected_company_outstanding_shares: Option, + pub selected_company_assigned_share_pool: Option, pub selected_company_unassigned_share_pool: Option, + pub selected_company_cached_share_price: Option, pub selected_company_cached_share_price_value_f32_text: Option, pub selected_company_mutable_support_scalar_value_f32_text: Option, pub selected_company_stat_band_root_0cfb_count: usize, @@ -142,6 +147,9 @@ impl RuntimeSummary { let selected_company_market_state = state .selected_company_id .and_then(|company_id| state.service_state.company_market_state.get(&company_id)); + let selected_company_annual_finance_state = state + .selected_company_id + .and_then(|company_id| runtime_company_annual_finance_state(state, company_id)); Self { calendar: state.calendar, calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(), @@ -247,9 +255,15 @@ impl RuntimeSummary { company_market_state_owner_count: state.service_state.company_market_state.len(), selected_company_outstanding_shares: selected_company_market_state .map(|market_state| market_state.outstanding_shares), + selected_company_assigned_share_pool: selected_company_annual_finance_state + .as_ref() + .map(|finance_state| finance_state.assigned_share_pool), selected_company_unassigned_share_pool: state .selected_company_id .and_then(|company_id| runtime_company_unassigned_share_pool(state, company_id)), + selected_company_cached_share_price: selected_company_annual_finance_state + .as_ref() + .and_then(|finance_state| finance_state.cached_share_price), selected_company_cached_share_price_value_f32_text: selected_company_market_state .map(|market_state| raw_u32_to_f32_text(market_state.cached_share_price_raw_u32)), selected_company_mutable_support_scalar_value_f32_text: selected_company_market_state @@ -1997,7 +2011,9 @@ mod tests { let summary = RuntimeSummary::from_state(&state); assert_eq!(summary.company_market_state_owner_count, 1); assert_eq!(summary.selected_company_outstanding_shares, Some(20_000)); + assert_eq!(summary.selected_company_assigned_share_pool, Some(5_000)); assert_eq!(summary.selected_company_unassigned_share_pool, Some(15_000)); + assert_eq!(summary.selected_company_cached_share_price, Some(40)); assert_eq!( summary.selected_company_cached_share_price_value_f32_text, Some("40.000000".to_string()) diff --git a/docs/README.md b/docs/README.md index 47225fa..d419752 100644 --- a/docs/README.md +++ b/docs/README.md @@ -120,7 +120,8 @@ The highest-value next passes are now: stat-band windows themselves now carry 16 dwords per root; the matching world-side issue reader seam is now rehosted for the grounded `0x37` lane, and selected-company summaries now expose the unassigned share pool derived from outstanding shares minus chairman-held shares for later annual - finance logic + finance logic; that same owned company market state now also backs a bundled annual-finance + reader seam for assigned shares, public float, and rounded cached share price - 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 0a510c5..3bb9ae1 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -206,7 +206,10 @@ runtime-owned company market state, and the matching world-side issue reader sea for the grounded `0x37` lane over save-native world restore state. The selected-company summary surface now also carries the unassigned share pool derived from outstanding shares minus chairman-held shares, so later dividend / stock-capital work can extend a shared owned-state -reader instead of guessing another finance leaf. +reader instead of guessing another finance leaf. The same owned company market state now also +supports a bundled annual-finance reader seam for assigned shares, public float, and rounded +cached share price, which is a better base for later dividend / issue-calendar simulation than +scattered single-field helpers. ## Why This Boundary