diff --git a/README.md b/README.md index 4b53c17..4b02195 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,9 @@ matches the stock-capital branch gate that requires at least two live bonds. The bond table now also contributes both the largest live bond principal and the chosen highest-coupon live bond principal into owned company market and annual-finance state, so the stock-capital approval ladder can extend one rehosted owner-state surface instead of hunting -another isolated finance leaf. A checked-in +another isolated finance leaf. The same bond-slot owner state now also exposes the highest live +coupon rate, which is enough to run the stock-capital price-to-book approval ladder as another +save-native runtime reader instead of a notes-only threshold table. A checked-in fixed-world finance-policy seam now also carries the raw stock, bond, bankruptcy, and dividend policy bytes from the `0x32c8` save block, and the first annual creditor-pressure branch now runs headlessly as a pure runtime reader over owned annual-finance state, support-adjusted share price, diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 7c2ee03..525ad3b 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -53,26 +53,27 @@ pub use runtime::{ RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState, - RuntimeCompanyAnnualFinanceState, RuntimeCompanyAnnualStockRepurchaseState, - RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, - RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, RuntimeCompanyMetric, - RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, RuntimeCompanyTarget, - RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, - RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, - RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, - RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, - RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, - RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, - RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState, - RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric, - RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain, - RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldIssueState, RuntimeWorldRestoreState, + RuntimeCompanyAnnualFinanceState, RuntimeCompanyAnnualStockIssueState, + RuntimeCompanyAnnualStockRepurchaseState, RuntimeCompanyBondSlot, + RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric, + RuntimeCompanyMarketState, RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, + RuntimeCompanyStatSelector, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, + RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator, + RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, + RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, + RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, + RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, + RuntimePackedEventTextBandSummary, RuntimePlayer, RuntimePlayerConditionTestScope, + RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, + RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, + RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate, + RuntimeWorldIssueState, RuntimeWorldRestoreState, runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_state, - runtime_company_annual_finance_state, runtime_company_annual_stock_repurchase_state, - runtime_company_assigned_share_pool, runtime_company_average_live_bond_coupon, - runtime_company_book_value_per_share, runtime_company_credit_rating, - runtime_company_investor_confidence, runtime_company_management_attitude, - runtime_company_market_value, runtime_company_prime_rate, + runtime_company_annual_finance_state, runtime_company_annual_stock_issue_state, + runtime_company_annual_stock_repurchase_state, runtime_company_assigned_share_pool, + runtime_company_average_live_bond_coupon, runtime_company_book_value_per_share, + runtime_company_credit_rating, runtime_company_investor_confidence, + runtime_company_management_attitude, runtime_company_market_value, runtime_company_prime_rate, runtime_company_recent_per_share_subscore, runtime_company_stat_value, runtime_company_stat_value_f64, runtime_company_unassigned_share_pool, runtime_world_annual_finance_mode_active, runtime_world_bankruptcy_allowed, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 760f977..775718b 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -258,6 +258,58 @@ pub struct RuntimeCompanyAnnualStockRepurchaseState { pub eligible_for_single_batch_repurchase: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualStockIssueState { + pub company_id: u32, + #[serde(default)] + pub annual_mode_active: Option, + #[serde(default)] + pub stock_issue_and_buyback_allowed: Option, + #[serde(default)] + pub bond_issue_and_repayment_allowed: Option, + #[serde(default)] + pub years_since_founding: Option, + #[serde(default)] + pub live_bond_count: Option, + #[serde(default)] + pub initial_issue_batch_size: Option, + #[serde(default)] + pub trimmed_issue_batch_size: Option, + #[serde(default)] + pub share_pressure_basis_points: Option, + #[serde(default)] + pub pressured_support_adjusted_share_price_scalar: Option, + #[serde(default)] + pub pressured_proceeds: Option, + #[serde(default)] + pub book_value_per_share_floor_applied: Option, + #[serde(default)] + pub price_to_book_ratio_basis_points: Option, + #[serde(default)] + pub current_cash: Option, + #[serde(default)] + pub highest_coupon_live_bond_principal: Option, + #[serde(default)] + pub highest_coupon_live_bond_rate_basis_points: Option, + #[serde(default)] + pub current_issue_age_absolute_counter_delta: Option, + #[serde(default)] + pub current_issue_cooldown_floor: Option, + #[serde(default)] + pub minimum_price_to_book_ratio_basis_points: Option, + #[serde(default)] + pub passes_share_price_floor: Option, + #[serde(default)] + pub passes_proceeds_floor: Option, + #[serde(default)] + pub passes_cash_gate: Option, + #[serde(default)] + pub passes_issue_cooldown_gate: Option, + #[serde(default)] + pub passes_coupon_price_to_book_gate: Option, + pub eligible_for_double_tranche_issue: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeTrackPieceCounts { #[serde(default)] @@ -2168,32 +2220,33 @@ fn runtime_decode_saved_f32_value_f64(raw_u32: u32) -> Option { Some(value) } -pub fn runtime_company_recent_per_share_subscore( +fn runtime_company_highest_live_bond_coupon_rate_f64( state: &RuntimeState, company_id: u32, ) -> Option { let market_state = state.service_state.company_market_state.get(&company_id)?; - if runtime_world_absolute_counter(state) - .is_some_and(|counter| counter == market_state.recent_per_share_cache_absolute_counter) - { - if let Some(cached_value) = - runtime_decode_saved_f64_bits(market_state.recent_per_share_cached_value_bits) - { - return Some(cached_value); - } - } - runtime_decode_saved_f32_value_f64(market_state.recent_per_share_subscore_raw_u32) + market_state + .live_bond_slots + .iter() + .filter_map(|slot| { + let value = f32::from_bits(slot.coupon_rate_raw_u32) as f64; + value.is_finite().then_some(value) + }) + .max_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal)) } -fn runtime_company_support_adjusted_share_price_scalar_f64( +fn runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( state: &RuntimeState, company_id: u32, + share_pressure_shares: i64, ) -> Option { let market_state = state.service_state.company_market_state.get(&company_id)?; if let Some(cached_value) = runtime_decode_saved_f32_value_f64(market_state.cached_share_price_raw_u32) { - return Some(cached_value.max(0.0001)); + if share_pressure_shares == 0 { + return Some(cached_value.max(0.0001)); + } } if market_state.outstanding_shares == 0 { @@ -2224,9 +2277,12 @@ fn runtime_company_support_adjusted_share_price_scalar_f64( let mutable_support = runtime_decode_saved_f32_value_f64(market_state.mutable_support_scalar_raw_u32)?; + let share_pressure = + (share_pressure_shares as f64 / market_state.outstanding_shares as f64).clamp(-0.2, 0.2); + let effective_mutable_support = mutable_support + share_pressure; let share_count_growth_ratio = ((market_state.outstanding_shares as f64 + 1.4 - * mutable_support + * effective_mutable_support * ((market_state.outstanding_shares as f64 / 20_000.0).powf(0.33))) / market_state.outstanding_shares as f64) .clamp(0.3, 6.0); @@ -2242,6 +2298,30 @@ fn runtime_company_support_adjusted_share_price_scalar_f64( Some(((recent_per_share * share_count_growth_ratio * investor_multiplier) + 1.0).max(0.0001)) } +pub fn runtime_company_recent_per_share_subscore( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + if runtime_world_absolute_counter(state) + .is_some_and(|counter| counter == market_state.recent_per_share_cache_absolute_counter) + { + if let Some(cached_value) = + runtime_decode_saved_f64_bits(market_state.recent_per_share_cached_value_bits) + { + return Some(cached_value); + } + } + runtime_decode_saved_f32_value_f64(market_state.recent_per_share_subscore_raw_u32) +} + +fn runtime_company_support_adjusted_share_price_scalar_f64( + state: &RuntimeState, + company_id: u32, +) -> Option { + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(state, company_id, 0) +} + pub fn runtime_company_investor_confidence(state: &RuntimeState, company_id: u32) -> Option { let company = state .companies @@ -2936,6 +3016,175 @@ pub fn runtime_company_annual_stock_repurchase_state( }) } +fn runtime_company_stock_issue_price_to_book_ratio_f64( + pressured_support_adjusted_share_price_scalar: f64, + book_value_per_share: f64, +) -> Option { + let denominator = book_value_per_share.max(1.0); + if !pressured_support_adjusted_share_price_scalar.is_finite() || !denominator.is_finite() { + return None; + } + Some(pressured_support_adjusted_share_price_scalar / denominator) +} + +fn runtime_company_stock_issue_minimum_price_to_book_ratio_f64( + highest_coupon_rate: f64, +) -> Option { + if !highest_coupon_rate.is_finite() || highest_coupon_rate <= 0.0 { + return None; + } + Some(if highest_coupon_rate <= 0.07 { + 1.30 + } else if highest_coupon_rate <= 0.08 { + 1.20 + } else if highest_coupon_rate <= 0.09 { + 1.10 + } else if highest_coupon_rate <= 0.10 { + 0.95 + } else if highest_coupon_rate <= 0.11 { + 0.80 + } else if highest_coupon_rate <= 0.12 { + 0.62 + } else if highest_coupon_rate <= 0.13 { + 0.50 + } else if highest_coupon_rate <= 0.14 { + 0.35 + } else { + return None; + }) +} + +pub fn runtime_company_annual_stock_issue_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + const ISSUE_PROCEEDS_CAP: i64 = 55_000; + const SHARE_PRICE_FLOOR: i64 = 22; + const ONE_YEAR_ABSOLUTE_COUNTER_SPAN: i64 = 12 * 28 * 24 * 60; + + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let highest_coupon_live_bond_principal = + annual_finance_state.highest_coupon_live_bond_principal; + let highest_coupon_live_bond_rate = + runtime_company_highest_live_bond_coupon_rate_f64(state, company_id); + let highest_coupon_live_bond_rate_basis_points = + highest_coupon_live_bond_rate.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); + let mut initial_issue_batch_size = + (annual_finance_state.outstanding_shares / 10 / 1_000) * 1_000; + if initial_issue_batch_size < 2_000 { + initial_issue_batch_size = 2_000; + } + let initial_issue_batch_size = Some(initial_issue_batch_size); + let mut trimmed_issue_batch_size = initial_issue_batch_size?; + let mut pressured_support_adjusted_share_price_scalar = + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( + state, + company_id, + -(trimmed_issue_batch_size as i64), + ); + let mut pressured_proceeds = pressured_support_adjusted_share_price_scalar + .and_then(|value| runtime_round_f64_to_i64(value * trimmed_issue_batch_size as f64)); + while trimmed_issue_batch_size > 2_000 + && pressured_proceeds.is_some_and(|value| value > ISSUE_PROCEEDS_CAP) + { + trimmed_issue_batch_size = trimmed_issue_batch_size.saturating_sub(1_000); + pressured_support_adjusted_share_price_scalar = + runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( + state, + company_id, + -(trimmed_issue_batch_size as i64), + ); + pressured_proceeds = pressured_support_adjusted_share_price_scalar + .and_then(|value| runtime_round_f64_to_i64(value * trimmed_issue_batch_size as f64)); + } + let pressured_support_adjusted_share_price_scalar_i64 = + pressured_support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64); + let book_value_per_share_floor_applied = + runtime_company_book_value_per_share(state, company_id).map(|value| value.max(1)); + let price_to_book_ratio = pressured_support_adjusted_share_price_scalar + .zip(book_value_per_share_floor_applied) + .and_then(|(share_price, book_value)| { + runtime_company_stock_issue_price_to_book_ratio_f64(share_price, book_value as f64) + }); + let price_to_book_ratio_basis_points = + price_to_book_ratio.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); + let minimum_price_to_book_ratio = highest_coupon_live_bond_rate + .and_then(runtime_company_stock_issue_minimum_price_to_book_ratio_f64); + let minimum_price_to_book_ratio_basis_points = + minimum_price_to_book_ratio.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0)); + let passes_share_price_floor = + pressured_support_adjusted_share_price_scalar_i64.map(|value| value >= SHARE_PRICE_FLOOR); + let passes_proceeds_floor = pressured_proceeds.map(|value| value >= ISSUE_PROCEEDS_CAP); + let passes_cash_gate = current_cash + .zip(highest_coupon_live_bond_principal) + .map(|(cash, principal)| cash <= i64::from(principal) + 5_000); + let passes_issue_cooldown_gate = Some( + annual_finance_state + .current_issue_age_absolute_counter_delta + .is_none_or(|delta| delta >= ONE_YEAR_ABSOLUTE_COUNTER_SPAN), + ); + let passes_coupon_price_to_book_gate = price_to_book_ratio_basis_points + .zip(minimum_price_to_book_ratio_basis_points) + .map(|(actual, minimum)| actual >= minimum); + let eligible_for_double_tranche_issue = runtime_world_annual_finance_mode_active(state) + == Some(true) + && runtime_world_stock_issue_and_buyback_allowed(state) == Some(true) + && runtime_world_bond_issue_and_repayment_allowed(state) == Some(true) + && annual_finance_state.bond_count >= 2 + && annual_finance_state + .years_since_founding + .is_some_and(|years| years >= 1) + && !runtime_company_annual_creditor_pressure_state(state, company_id)? + .eligible_for_bankruptcy_branch + && !runtime_company_annual_deep_distress_state(state, company_id)? + .eligible_for_bankruptcy_fallback + && !runtime_company_annual_stock_repurchase_state(state, company_id)? + .eligible_for_single_batch_repurchase + && passes_share_price_floor == Some(true) + && passes_proceeds_floor == Some(true) + && passes_cash_gate == Some(true) + && passes_issue_cooldown_gate == Some(true) + && passes_coupon_price_to_book_gate == Some(true); + Some(RuntimeCompanyAnnualStockIssueState { + company_id, + annual_mode_active: runtime_world_annual_finance_mode_active(state), + stock_issue_and_buyback_allowed: runtime_world_stock_issue_and_buyback_allowed(state), + bond_issue_and_repayment_allowed: runtime_world_bond_issue_and_repayment_allowed(state), + years_since_founding: annual_finance_state.years_since_founding, + live_bond_count: Some(annual_finance_state.bond_count), + initial_issue_batch_size, + trimmed_issue_batch_size: Some(trimmed_issue_batch_size), + share_pressure_basis_points: runtime_round_f64_to_i64( + -(trimmed_issue_batch_size as f64) / annual_finance_state.outstanding_shares as f64 + * 10_000.0, + ), + pressured_support_adjusted_share_price_scalar: + pressured_support_adjusted_share_price_scalar_i64, + pressured_proceeds, + book_value_per_share_floor_applied, + price_to_book_ratio_basis_points, + current_cash, + highest_coupon_live_bond_principal, + highest_coupon_live_bond_rate_basis_points, + current_issue_age_absolute_counter_delta: annual_finance_state + .current_issue_age_absolute_counter_delta, + current_issue_cooldown_floor: Some(ONE_YEAR_ABSOLUTE_COUNTER_SPAN), + minimum_price_to_book_ratio_basis_points, + passes_share_price_floor, + passes_proceeds_floor, + passes_cash_gate, + passes_issue_cooldown_gate, + passes_coupon_price_to_book_gate, + eligible_for_double_tranche_issue, + }) +} + pub fn runtime_company_annual_creditor_pressure_state( state: &RuntimeState, company_id: u32, @@ -7372,6 +7621,188 @@ mod tests { assert!(!repurchase_state.eligible_for_single_batch_repurchase); } + #[test] + fn derives_annual_stock_issue_state_from_rehosted_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 250_000.0); + + 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 { + partial_year_progress_raw_u8: Some(0x0c), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + issue_37_value: Some(2), + issue_37_multiplier_raw_u32: Some(1.0f32.to_bits()), + issue_37_multiplier_value_f32_text: Some("1.000000".to_string()), + absolute_counter_raw_u32: Some(885_911_040), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 14, + 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: Some(8), + 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: 8, + name: "Taylor".to_string(), + active: true, + current_cash: 200, + linked_company_id: Some(14), + company_holdings: BTreeMap::from([(14, 14_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 { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + chairman_personality_raw_u8: BTreeMap::from([(8, 20)]), + company_market_state: BTreeMap::from([( + 14, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 2, + highest_coupon_live_bond_principal: Some(300_000), + current_issue_calendar_word: 0x0101_0725, + current_issue_calendar_word_2: 0x0001_0001, + founding_year: 1840, + cached_share_price_raw_u32: 35.0f32.to_bits(), + recent_per_share_cache_absolute_counter: 885_911_040, + recent_per_share_cached_value_bits: 34.0f64.to_bits(), + city_connection_latch: false, + live_bond_slots: vec![ + RuntimeCompanyBondSlot { + slot_index: 0, + principal: 300_000, + coupon_rate_raw_u32: 0.11f32.to_bits(), + }, + RuntimeCompanyBondSlot { + slot_index: 1, + principal: 200_000, + coupon_rate_raw_u32: 0.07f32.to_bits(), + }, + ], + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x32f, + 30.0f32.to_bits(), + )]), + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let stock_issue_state = + runtime_company_annual_stock_issue_state(&state, 14).expect("stock issue state"); + assert_eq!(stock_issue_state.live_bond_count, Some(2)); + assert_eq!(stock_issue_state.initial_issue_batch_size, Some(2_000)); + assert_eq!(stock_issue_state.trimmed_issue_batch_size, Some(2_000)); + assert_eq!(stock_issue_state.share_pressure_basis_points, Some(-1_000)); + assert_eq!( + stock_issue_state.pressured_support_adjusted_share_price_scalar, + Some(35) + ); + assert_eq!(stock_issue_state.pressured_proceeds, Some(70_000)); + assert_eq!( + stock_issue_state.book_value_per_share_floor_applied, + Some(30) + ); + assert_eq!( + stock_issue_state.price_to_book_ratio_basis_points, + Some(11_667) + ); + assert_eq!( + stock_issue_state.highest_coupon_live_bond_rate_basis_points, + Some(1_100) + ); + assert_eq!( + stock_issue_state.minimum_price_to_book_ratio_basis_points, + Some(8_000) + ); + assert_eq!(stock_issue_state.current_cash, Some(250_000)); + assert_eq!( + stock_issue_state.highest_coupon_live_bond_principal, + Some(300_000) + ); + assert_eq!( + stock_issue_state.current_issue_age_absolute_counter_delta, + Some(967_680) + ); + assert_eq!( + stock_issue_state.current_issue_cooldown_floor, + Some(483_840) + ); + assert_eq!(stock_issue_state.passes_share_price_floor, Some(true)); + assert_eq!(stock_issue_state.passes_proceeds_floor, Some(true)); + assert_eq!(stock_issue_state.passes_cash_gate, Some(true)); + assert_eq!(stock_issue_state.passes_issue_cooldown_gate, Some(true)); + assert_eq!( + stock_issue_state.passes_coupon_price_to_book_gate, + Some(true) + ); + assert!(stock_issue_state.eligible_for_double_tranche_issue); + } + #[test] fn reads_company_market_metrics_from_annual_finance_reader() { let current_issue_calendar_word = 0x0101_0726; diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index fde1a26..0d4b63f 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::{ CalendarPoint, RuntimeState, runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_state, runtime_company_annual_finance_state, - runtime_company_annual_stock_repurchase_state, runtime_company_unassigned_share_pool, + runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state, + runtime_company_unassigned_share_pool, }; fn raw_u32_to_f32_text(raw: u32) -> String { @@ -124,6 +125,26 @@ pub struct RuntimeSummary { pub selected_company_stock_repurchase_affordability_cash_floor: Option, pub selected_company_stock_repurchase_unassigned_share_pool: Option, pub selected_company_stock_repurchase_eligible_for_single_batch: Option, + pub selected_company_stock_issue_live_bond_count: Option, + pub selected_company_stock_issue_initial_batch_size: Option, + pub selected_company_stock_issue_trimmed_batch_size: Option, + pub selected_company_stock_issue_share_pressure_basis_points: Option, + pub selected_company_stock_issue_pressured_share_price_scalar: Option, + pub selected_company_stock_issue_pressured_proceeds: Option, + pub selected_company_stock_issue_book_value_per_share_floor_applied: Option, + pub selected_company_stock_issue_price_to_book_ratio_basis_points: Option, + pub selected_company_stock_issue_current_cash: Option, + pub selected_company_stock_issue_highest_coupon_live_bond_principal: Option, + pub selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points: Option, + pub selected_company_stock_issue_current_issue_age_absolute_counter_delta: Option, + pub selected_company_stock_issue_current_issue_cooldown_floor: Option, + pub selected_company_stock_issue_minimum_price_to_book_ratio_basis_points: Option, + pub selected_company_stock_issue_passes_share_price_floor: Option, + pub selected_company_stock_issue_passes_proceeds_floor: Option, + pub selected_company_stock_issue_passes_cash_gate: Option, + pub selected_company_stock_issue_passes_issue_cooldown_gate: Option, + pub selected_company_stock_issue_passes_coupon_price_to_book_gate: Option, + pub selected_company_stock_issue_eligible_for_double_tranche: Option, pub player_count: usize, pub chairman_profile_count: usize, pub active_chairman_profile_count: usize, @@ -224,6 +245,9 @@ impl RuntimeSummary { state.selected_company_id.and_then(|company_id| { runtime_company_annual_stock_repurchase_state(state, company_id) }); + let selected_company_stock_issue_state = state + .selected_company_id + .and_then(|company_id| runtime_company_annual_stock_issue_state(state, company_id)); Self { calendar: state.calendar, calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(), @@ -550,6 +574,81 @@ impl RuntimeSummary { selected_company_stock_repurchase_state .as_ref() .map(|repurchase_state| repurchase_state.eligible_for_single_batch_repurchase), + selected_company_stock_issue_live_bond_count: selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.live_bond_count), + selected_company_stock_issue_initial_batch_size: selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.initial_issue_batch_size), + selected_company_stock_issue_trimmed_batch_size: selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.trimmed_issue_batch_size), + selected_company_stock_issue_share_pressure_basis_points: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.share_pressure_basis_points), + selected_company_stock_issue_pressured_share_price_scalar: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| { + issue_state.pressured_support_adjusted_share_price_scalar + }), + selected_company_stock_issue_pressured_proceeds: selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.pressured_proceeds), + selected_company_stock_issue_book_value_per_share_floor_applied: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.book_value_per_share_floor_applied), + selected_company_stock_issue_price_to_book_ratio_basis_points: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.price_to_book_ratio_basis_points), + selected_company_stock_issue_current_cash: selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.current_cash), + selected_company_stock_issue_highest_coupon_live_bond_principal: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.highest_coupon_live_bond_principal), + selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.highest_coupon_live_bond_rate_basis_points), + selected_company_stock_issue_current_issue_age_absolute_counter_delta: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.current_issue_age_absolute_counter_delta), + selected_company_stock_issue_current_issue_cooldown_floor: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.current_issue_cooldown_floor), + selected_company_stock_issue_minimum_price_to_book_ratio_basis_points: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.minimum_price_to_book_ratio_basis_points), + selected_company_stock_issue_passes_share_price_floor: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_share_price_floor), + selected_company_stock_issue_passes_proceeds_floor: selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_proceeds_floor), + selected_company_stock_issue_passes_cash_gate: selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_cash_gate), + selected_company_stock_issue_passes_issue_cooldown_gate: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_issue_cooldown_gate), + selected_company_stock_issue_passes_coupon_price_to_book_gate: + selected_company_stock_issue_state + .as_ref() + .and_then(|issue_state| issue_state.passes_coupon_price_to_book_gate), + selected_company_stock_issue_eligible_for_double_tranche: + selected_company_stock_issue_state + .as_ref() + .map(|issue_state| issue_state.eligible_for_double_tranche_issue), player_count: state.players.len(), chairman_profile_count: state.chairman_profiles.len(), active_chairman_profile_count: state @@ -2811,4 +2910,216 @@ mod tests { Some(false) ); } + + #[test] + fn summarizes_selected_company_stock_issue_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) + * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + year_stat_family_qword_bits[(crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH + * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize] = 250_000.0f64.to_bits(); + + 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 { + partial_year_progress_raw_u8: Some(0x0c), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + bond_issue_and_repayment_policy_raw_u8: Some(0), + bond_issue_and_repayment_allowed: Some(true), + issue_37_value: Some(2), + issue_37_multiplier_raw_u32: Some(1.0f32.to_bits()), + issue_37_multiplier_value_f32_text: Some("1.000000".to_string()), + absolute_counter_raw_u32: Some(885_911_040), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 14, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: crate::RuntimeCompanyControllerKind::Unknown, + linked_chairman_profile_id: Some(8), + 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: Some(14), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::RuntimeChairmanProfile { + profile_id: 8, + name: "Taylor".to_string(), + active: true, + current_cash: 200, + linked_company_id: Some(14), + company_holdings: BTreeMap::from([(14, 14_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: Vec::new().into_iter().collect(), + 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 { + world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b], + chairman_personality_raw_u8: BTreeMap::from([(8, 20)]), + company_market_state: BTreeMap::from([( + 14, + crate::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + bond_count: 2, + highest_coupon_live_bond_principal: Some(300_000), + current_issue_calendar_word: 0x0101_0725, + current_issue_calendar_word_2: 0x0001_0001, + founding_year: 1840, + cached_share_price_raw_u32: 35.0f32.to_bits(), + recent_per_share_cache_absolute_counter: 885_911_040, + recent_per_share_cached_value_bits: 34.0f64.to_bits(), + live_bond_slots: vec![ + crate::RuntimeCompanyBondSlot { + slot_index: 0, + principal: 300_000, + coupon_rate_raw_u32: 0.11f32.to_bits(), + }, + crate::RuntimeCompanyBondSlot { + slot_index: 1, + principal: 200_000, + coupon_rate_raw_u32: 0.07f32.to_bits(), + }, + ], + direct_control_transfer_float_fields_raw_u32: BTreeMap::from([( + 0x32f, + 30.0f32.to_bits(), + )]), + year_stat_family_qword_bits, + ..crate::RuntimeCompanyMarketState::default() + }, + )]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.selected_company_stock_issue_live_bond_count, + Some(2) + ); + assert_eq!( + summary.selected_company_stock_issue_initial_batch_size, + Some(2_000) + ); + assert_eq!( + summary.selected_company_stock_issue_trimmed_batch_size, + Some(2_000) + ); + assert_eq!( + summary.selected_company_stock_issue_share_pressure_basis_points, + Some(-1_000) + ); + assert_eq!( + summary.selected_company_stock_issue_pressured_share_price_scalar, + Some(35) + ); + assert_eq!( + summary.selected_company_stock_issue_pressured_proceeds, + Some(70_000) + ); + assert_eq!( + summary.selected_company_stock_issue_book_value_per_share_floor_applied, + Some(30) + ); + assert_eq!( + summary.selected_company_stock_issue_price_to_book_ratio_basis_points, + Some(11_667) + ); + assert_eq!( + summary.selected_company_stock_issue_current_cash, + Some(250_000) + ); + assert_eq!( + summary.selected_company_stock_issue_highest_coupon_live_bond_principal, + Some(300_000) + ); + assert_eq!( + summary.selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points, + Some(1_100) + ); + assert_eq!( + summary.selected_company_stock_issue_current_issue_age_absolute_counter_delta, + Some(967_680) + ); + assert_eq!( + summary.selected_company_stock_issue_current_issue_cooldown_floor, + Some(483_840) + ); + assert_eq!( + summary.selected_company_stock_issue_minimum_price_to_book_ratio_basis_points, + Some(8_000) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_share_price_floor, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_proceeds_floor, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_cash_gate, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_issue_cooldown_gate, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_passes_coupon_price_to_book_gate, + Some(true) + ); + assert_eq!( + summary.selected_company_stock_issue_eligible_for_double_tranche, + Some(true) + ); + } } diff --git a/docs/README.md b/docs/README.md index 4d80ffd..31abcbc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -134,10 +134,12 @@ The highest-value next passes are now: and last bankruptcy for later annual finance-policy rehosting; live bond-slot count now travels through that same owned annual-finance state for the stock-capital branch gate, and the grounded bond table now also contributes both the largest live bond principal and the chosen highest-coupon live bond principal into that same - owner-state surface; the same fixed-world save block now also carries the raw stock, bond, + owner-state surface, plus the highest live coupon rate for the stock-capital approval ladder; + the same fixed-world save block now also carries the raw stock, bond, bankruptcy, and dividend finance-policy bytes, and the first annual creditor-pressure branch now executes as a pure runtime reader over that owner state instead of remaining atlas-only; the later deep-distress bankruptcy fallback now runs on that same save-native cash and trailing- + profit seam; the annual stock-repurchase and stock-capital issue branches now do too net-profit surface too; the same owner seam now also carries the fixed-world building-density growth setting plus the linked chairman personality byte, which is enough to run the annual stock-repurchase gate headlessly as another pure reader diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index c8fd521..27700dd 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -224,8 +224,9 @@ annual finance-policy gates in the atlas. Live bond-slot count now also flows th owned company market and annual-finance state, matching the stock-capital branch gate that needs at least two live bonds. The same grounded bond table now also contributes both the largest live bond principal and the chosen highest-coupon live bond principal into owned company market and -annual-finance state, so later stock-capital gates can extend a rehosted owner-state seam instead -of guessing another finance leaf. The same fixed-world save block now also carries the raw stock, +annual-finance state, and now also exposes the highest live coupon rate, so the stock-capital +price-to-book ladder can extend a rehosted owner-state seam instead of guessing another finance +leaf. The same fixed-world save block now also carries the raw stock, bond, bankruptcy, and dividend finance-policy bytes, and the earliest annual creditor-pressure bankruptcy branch now runs as a pure runtime reader over owned annual-finance state, support- adjusted share price, and those policy bytes rather than staying in atlas prose only. The later @@ -233,7 +234,9 @@ deep-distress bankruptcy fallback now rides the same owner-state seam too, using cash reader plus the first three trailing net-profit years instead of a parallel raw-offset guess. That same seam now also carries the fixed-world building-density growth setting plus the linked chairman personality byte, which is enough to rehost the annual stock-repurchase gate on owned -save/runtime state instead of another threshold-only note. +save/runtime state instead of another threshold-only note. The stock-capital issue branch now +rides that same seam too, with share-pressure, cooldown, and price-to-book gate state exposed as +normal runtime readers. ## Why This Boundary