diff --git a/README.md b/README.md index 7111bf4..25a62fd 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,10 @@ state too, deriving assigned shares, public float, and rounded cached share pric company market reader instead of scattering more finance helpers across the runtime. A checked-in The fixed-world finance neighborhood itself is now widened to 16 dwords rooted at `[world+0x11]`, so future issue-`0x38/0x39` closure can build on a broader owned restore-state window rather than -another narrow one-off probe. A checked-in +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 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-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index a081a73..5815a6e 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -47,11 +47,12 @@ pub use runtime::{ RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyAnnualFinanceState, - RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketState, - RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, - RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, - RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, - RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, + RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric, + RuntimeCompanyMarketState, RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, + RuntimeCompanyStatSelector, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, + RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator, + RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, + RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, @@ -62,7 +63,8 @@ pub use runtime::{ 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_annual_finance_state, runtime_company_assigned_share_pool, - runtime_company_stat_value, runtime_company_unassigned_share_pool, runtime_world_issue_state, + runtime_company_market_value, 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 ef5fe72..68f4687 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -342,6 +342,20 @@ pub enum RuntimeCompanyMetric { TrackPiecesNonElectric, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeCompanyMarketMetric { + OutstandingShares, + AssignedSharePool, + UnassignedSharePool, + CachedSharePrice, + ChairmanSalaryBaseline, + ChairmanSalaryCurrent, + ChairmanBonusAmount, + CurrentIssueCalendarWord, + PriorIssueCalendarWord, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RuntimeChairmanMetric { @@ -1910,6 +1924,41 @@ pub fn runtime_company_annual_finance_state( }) } +pub fn runtime_company_market_value( + state: &RuntimeState, + company_id: u32, + metric: RuntimeCompanyMarketMetric, +) -> Option { + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + match metric { + RuntimeCompanyMarketMetric::OutstandingShares => { + Some(annual_finance_state.outstanding_shares as i64) + } + RuntimeCompanyMarketMetric::AssignedSharePool => { + Some(annual_finance_state.assigned_share_pool as i64) + } + RuntimeCompanyMarketMetric::UnassignedSharePool => { + Some(annual_finance_state.unassigned_share_pool as i64) + } + RuntimeCompanyMarketMetric::CachedSharePrice => annual_finance_state.cached_share_price, + RuntimeCompanyMarketMetric::ChairmanSalaryBaseline => { + Some(annual_finance_state.chairman_salary_baseline as i64) + } + RuntimeCompanyMarketMetric::ChairmanSalaryCurrent => { + Some(annual_finance_state.chairman_salary_current as i64) + } + RuntimeCompanyMarketMetric::ChairmanBonusAmount => { + Some(annual_finance_state.chairman_bonus_amount as i64) + } + RuntimeCompanyMarketMetric::CurrentIssueCalendarWord => { + Some(annual_finance_state.current_issue_calendar_word as i64) + } + RuntimeCompanyMarketMetric::PriorIssueCalendarWord => { + Some(annual_finance_state.prior_issue_calendar_word as i64) + } + } +} + fn rounded_cached_share_price_i64(raw_u32: u32) -> Option { let value = f32::from_bits(raw_u32); if !value.is_finite() { @@ -4191,4 +4240,149 @@ mod tests { assert_eq!(runtime_company_assigned_share_pool(&state, 99), None); assert_eq!(runtime_company_annual_finance_state(&state, 99), None); } + + #[test] + fn reads_company_market_metrics_from_annual_finance_reader() { + 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: 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![RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(7), + company_holdings: BTreeMap::from([(7, 12_000)]), + 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([( + 7, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + cached_share_price_raw_u32: 0x42200000, + chairman_salary_baseline: 18, + chairman_salary_current: 27, + chairman_bonus_year: 1843, + chairman_bonus_amount: 625, + founding_year: 1832, + last_bankruptcy_year: 0, + last_dividend_year: 1842, + current_issue_calendar_word: 9, + prior_issue_calendar_word: 8, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!( + runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::OutstandingShares), + Some(20_000) + ); + assert_eq!( + runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::AssignedSharePool), + Some(12_000) + ); + assert_eq!( + runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::UnassignedSharePool), + Some(8_000) + ); + assert_eq!( + runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::CachedSharePrice), + Some(40) + ); + assert_eq!( + runtime_company_market_value( + &state, + 7, + RuntimeCompanyMarketMetric::CurrentIssueCalendarWord + ), + Some(9) + ); + assert_eq!( + runtime_company_market_value( + &state, + 7, + RuntimeCompanyMarketMetric::PriorIssueCalendarWord + ), + Some(8) + ); + assert_eq!( + runtime_company_market_value( + &state, + 7, + RuntimeCompanyMarketMetric::ChairmanSalaryCurrent + ), + Some(27) + ); + assert_eq!( + runtime_company_market_value( + &state, + 7, + RuntimeCompanyMarketMetric::ChairmanBonusAmount + ), + Some(625) + ); + assert_eq!( + runtime_company_market_value(&state, 99, RuntimeCompanyMarketMetric::OutstandingShares), + None + ); + } } diff --git a/docs/README.md b/docs/README.md index c2f00c6..cd319d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -123,7 +123,8 @@ The highest-value next passes are now: 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 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 + 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 - 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 860455a..f386ff5 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -211,7 +211,10 @@ supports a bundled annual-finance reader seam for assigned shares, public float, cached share price, which is a better base for later dividend / issue-calendar simulation than scattered single-field helpers. The fixed-world finance neighborhood is now widened to 16 dwords rooted at `[world+0x11]`, so later issue-`0x38/0x39` closure can build on a broader owned -restore-state window instead of another narrow probe. +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. ## Why This Boundary