From c41a6b0e92854f4437d9443a020280fe3cac0918 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 20:55:42 -0700 Subject: [PATCH] Rehost company unassigned share pool --- README.md | 5 +- crates/rrt-fixtures/src/schema.rs | 10 +++ crates/rrt-runtime/src/lib.rs | 3 +- crates/rrt-runtime/src/runtime.rs | 112 ++++++++++++++++++++++++++++++ crates/rrt-runtime/src/summary.rs | 63 ++++++++++++++--- docs/README.md | 4 +- docs/runtime-rehost-plan.md | 5 +- 7 files changed, 189 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 120d6fb..78f37a6 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,10 @@ extend one shared reader family instead of hard-coding more direct field accesse stat-band windows are now widened to 16 dwords per root in save-slice/runtime state so later year-series finance closure has enough owned raw state to attach to. The matching world-side issue reader seam is now also rehosted for the grounded `0x37` investor-confidence lane on top of the -save-native world-restore state. A checked-in +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 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 3d190ce..2e63ffd 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -92,6 +92,8 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub selected_company_outstanding_shares: Option, #[serde(default)] + pub selected_company_unassigned_share_pool: 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, @@ -591,6 +593,14 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(value) = self.selected_company_unassigned_share_pool { + if actual.selected_company_unassigned_share_pool != Some(value) { + mismatches.push(format!( + "selected_company_unassigned_share_pool mismatch: expected {value}, got {:?}", + actual.selected_company_unassigned_share_pool + )); + } + } 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 bf87157..8cce91e 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -61,7 +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_world_issue_state, + 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 e7577a5..21caf32 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -1838,6 +1838,20 @@ pub fn runtime_world_issue_state(state: &RuntimeState, issue_id: u32) -> Option< } } +pub fn runtime_company_unassigned_share_pool(state: &RuntimeState, company_id: u32) -> Option { + let outstanding_shares = state + .service_state + .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::(); + Some(outstanding_shares.saturating_sub(assigned_shares)) +} + fn rounded_cached_share_price_i64(raw_u32: u32) -> Option { let value = f32::from_bits(raw_u32); if !value.is_finite() { @@ -3889,4 +3903,102 @@ mod tests { assert_eq!(issue.multiplier_value_f32_text, "0.060000"); assert_eq!(runtime_world_issue_state(&state, 0x39), None); } + + #[test] + fn derives_company_unassigned_share_pool_from_market_state_and_holdings() { + 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, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + assert_eq!(runtime_company_unassigned_share_pool(&state, 4), Some(4_500)); + assert_eq!(runtime_company_unassigned_share_pool(&state, 99), None); + } } diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index fddc18a..16f589c 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{CalendarPoint, RuntimeState}; +use crate::{runtime_company_unassigned_share_pool, CalendarPoint, RuntimeState}; fn raw_u32_to_f32_text(raw: u32) -> String { format!("{:.6}", f32::from_bits(raw)) @@ -47,6 +47,7 @@ pub struct RuntimeSummary { pub active_company_count: usize, pub company_market_state_owner_count: usize, pub selected_company_outstanding_shares: Option, + pub selected_company_unassigned_share_pool: 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, @@ -246,6 +247,9 @@ 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_unassigned_share_pool: state + .selected_company_id + .and_then(|company_id| runtime_company_unassigned_share_pool(state, company_id)), 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 @@ -865,8 +869,18 @@ mod tests { selected_company_id: None, players: Vec::new(), selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, + chairman_profiles: vec![crate::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(1), trains: Vec::new(), locomotive_catalog: vec![ crate::RuntimeLocomotiveCatalogEntry { @@ -1116,8 +1130,18 @@ mod tests { selected_company_id: None, players: Vec::new(), selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, + chairman_profiles: vec![crate::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(1), trains: Vec::new(), locomotive_catalog: Vec::new(), cargo_catalog: Vec::new(), @@ -1227,8 +1251,18 @@ mod tests { selected_company_id: None, players: Vec::new(), selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, + chairman_profiles: vec![crate::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(1), trains: Vec::new(), locomotive_catalog: vec![ crate::RuntimeLocomotiveCatalogEntry { @@ -1855,8 +1889,18 @@ mod tests { selected_company_id: Some(1), players: Vec::new(), selected_player_id: None, - chairman_profiles: Vec::new(), - selected_chairman_profile_id: None, + chairman_profiles: vec![crate::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: Some(1), + company_holdings: std::collections::BTreeMap::from([(1, 5_000)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: Some(1), trains: Vec::new(), locomotive_catalog: Vec::new(), cargo_catalog: Vec::new(), @@ -1953,6 +1997,7 @@ 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_unassigned_share_pool, Some(15_000)); 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 84e71d1..47225fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -118,7 +118,9 @@ The highest-value next passes are now: owned runtime data instead of one more guessed save offset; the first runtime-side `0x2329` stat-family reader seam is now also rehosted for slots `0x0d` and `0x1d`, and the saved 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 + 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 - 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 882a778..0a510c5 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -203,7 +203,10 @@ the first grounded stat-band root windows at `[company+0x0cfb]`, `[company+0x0d7 for slots `0x0d` and `0x1d`, so later finance readers can target saved owner state and one shared reader family directly. Those stat-band windows now carry 16 dwords per root in the save-slice and runtime-owned company market state, and the matching world-side issue reader seam is now rehosted -for the grounded `0x37` lane over save-native world restore state. +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. ## Why This Boundary