Rehost annual stock-capital issue branch

This commit is contained in:
Jan Petykiewicz 2026-04-18 00:41:35 -07:00
commit d303ee994b
6 changed files with 789 additions and 39 deletions

View file

@ -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 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 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 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 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 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, headlessly as a pure runtime reader over owned annual-finance state, support-adjusted share price,

View file

@ -53,26 +53,27 @@ pub use runtime::{
RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric,
RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany,
RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState, RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState,
RuntimeCompanyAnnualFinanceState, RuntimeCompanyAnnualStockRepurchaseState, RuntimeCompanyAnnualFinanceState, RuntimeCompanyAnnualStockIssueState,
RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyAnnualStockRepurchaseState, RuntimeCompanyBondSlot,
RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, RuntimeCompanyMetric, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric,
RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, RuntimeCompanyTarget, RuntimeCompanyMarketState, RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate,
RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeCompanyStatSelector, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator,
RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState, RuntimePackedEventTextBandSummary, RuntimePlayer, RuntimePlayerConditionTestScope,
RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric, RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldIssueState, RuntimeWorldRestoreState, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate,
RuntimeWorldIssueState, RuntimeWorldRestoreState,
runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_state, 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_annual_finance_state, runtime_company_annual_stock_issue_state,
runtime_company_assigned_share_pool, runtime_company_average_live_bond_coupon, runtime_company_annual_stock_repurchase_state, runtime_company_assigned_share_pool,
runtime_company_book_value_per_share, runtime_company_credit_rating, runtime_company_average_live_bond_coupon, runtime_company_book_value_per_share,
runtime_company_investor_confidence, runtime_company_management_attitude, runtime_company_credit_rating, runtime_company_investor_confidence,
runtime_company_market_value, runtime_company_prime_rate, runtime_company_management_attitude, runtime_company_market_value, runtime_company_prime_rate,
runtime_company_recent_per_share_subscore, runtime_company_stat_value, runtime_company_recent_per_share_subscore, runtime_company_stat_value,
runtime_company_stat_value_f64, runtime_company_unassigned_share_pool, runtime_company_stat_value_f64, runtime_company_unassigned_share_pool,
runtime_world_annual_finance_mode_active, runtime_world_bankruptcy_allowed, runtime_world_annual_finance_mode_active, runtime_world_bankruptcy_allowed,

View file

@ -258,6 +258,58 @@ pub struct RuntimeCompanyAnnualStockRepurchaseState {
pub eligible_for_single_batch_repurchase: bool, 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<bool>,
#[serde(default)]
pub stock_issue_and_buyback_allowed: Option<bool>,
#[serde(default)]
pub bond_issue_and_repayment_allowed: Option<bool>,
#[serde(default)]
pub years_since_founding: Option<u32>,
#[serde(default)]
pub live_bond_count: Option<u8>,
#[serde(default)]
pub initial_issue_batch_size: Option<u32>,
#[serde(default)]
pub trimmed_issue_batch_size: Option<u32>,
#[serde(default)]
pub share_pressure_basis_points: Option<i64>,
#[serde(default)]
pub pressured_support_adjusted_share_price_scalar: Option<i64>,
#[serde(default)]
pub pressured_proceeds: Option<i64>,
#[serde(default)]
pub book_value_per_share_floor_applied: Option<i64>,
#[serde(default)]
pub price_to_book_ratio_basis_points: Option<i64>,
#[serde(default)]
pub current_cash: Option<i64>,
#[serde(default)]
pub highest_coupon_live_bond_principal: Option<u32>,
#[serde(default)]
pub highest_coupon_live_bond_rate_basis_points: Option<i64>,
#[serde(default)]
pub current_issue_age_absolute_counter_delta: Option<i64>,
#[serde(default)]
pub current_issue_cooldown_floor: Option<i64>,
#[serde(default)]
pub minimum_price_to_book_ratio_basis_points: Option<i64>,
#[serde(default)]
pub passes_share_price_floor: Option<bool>,
#[serde(default)]
pub passes_proceeds_floor: Option<bool>,
#[serde(default)]
pub passes_cash_gate: Option<bool>,
#[serde(default)]
pub passes_issue_cooldown_gate: Option<bool>,
#[serde(default)]
pub passes_coupon_price_to_book_gate: Option<bool>,
pub eligible_for_double_tranche_issue: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RuntimeTrackPieceCounts { pub struct RuntimeTrackPieceCounts {
#[serde(default)] #[serde(default)]
@ -2168,32 +2220,33 @@ fn runtime_decode_saved_f32_value_f64(raw_u32: u32) -> Option<f64> {
Some(value) Some(value)
} }
pub fn runtime_company_recent_per_share_subscore( fn runtime_company_highest_live_bond_coupon_rate_f64(
state: &RuntimeState, state: &RuntimeState,
company_id: u32, company_id: u32,
) -> Option<f64> { ) -> Option<f64> {
let market_state = state.service_state.company_market_state.get(&company_id)?; let market_state = state.service_state.company_market_state.get(&company_id)?;
if runtime_world_absolute_counter(state) market_state
.is_some_and(|counter| counter == market_state.recent_per_share_cache_absolute_counter) .live_bond_slots
{ .iter()
if let Some(cached_value) = .filter_map(|slot| {
runtime_decode_saved_f64_bits(market_state.recent_per_share_cached_value_bits) let value = f32::from_bits(slot.coupon_rate_raw_u32) as f64;
{ value.is_finite().then_some(value)
return Some(cached_value); })
} .max_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal))
}
runtime_decode_saved_f32_value_f64(market_state.recent_per_share_subscore_raw_u32)
} }
fn runtime_company_support_adjusted_share_price_scalar_f64( fn runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(
state: &RuntimeState, state: &RuntimeState,
company_id: u32, company_id: u32,
share_pressure_shares: i64,
) -> Option<f64> { ) -> Option<f64> {
let market_state = state.service_state.company_market_state.get(&company_id)?; let market_state = state.service_state.company_market_state.get(&company_id)?;
if let Some(cached_value) = if let Some(cached_value) =
runtime_decode_saved_f32_value_f64(market_state.cached_share_price_raw_u32) 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 { if market_state.outstanding_shares == 0 {
@ -2224,9 +2277,12 @@ fn runtime_company_support_adjusted_share_price_scalar_f64(
let mutable_support = let mutable_support =
runtime_decode_saved_f32_value_f64(market_state.mutable_support_scalar_raw_u32)?; 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 let share_count_growth_ratio = ((market_state.outstanding_shares as f64
+ 1.4 + 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 / 20_000.0).powf(0.33)))
/ market_state.outstanding_shares as f64) / market_state.outstanding_shares as f64)
.clamp(0.3, 6.0); .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)) 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<f64> {
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<f64> {
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<i64> { pub fn runtime_company_investor_confidence(state: &RuntimeState, company_id: u32) -> Option<i64> {
let company = state let company = state
.companies .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<f64> {
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<f64> {
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<RuntimeCompanyAnnualStockIssueState> {
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( pub fn runtime_company_annual_creditor_pressure_state(
state: &RuntimeState, state: &RuntimeState,
company_id: u32, company_id: u32,
@ -7372,6 +7621,188 @@ mod tests {
assert!(!repurchase_state.eligible_for_single_batch_repurchase); 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<u64>, 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] #[test]
fn reads_company_market_metrics_from_annual_finance_reader() { fn reads_company_market_metrics_from_annual_finance_reader() {
let current_issue_calendar_word = 0x0101_0726; let current_issue_calendar_word = 0x0101_0726;

View file

@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
CalendarPoint, RuntimeState, runtime_company_annual_creditor_pressure_state, CalendarPoint, RuntimeState, runtime_company_annual_creditor_pressure_state,
runtime_company_annual_deep_distress_state, runtime_company_annual_finance_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 { 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<i64>, pub selected_company_stock_repurchase_affordability_cash_floor: Option<i64>,
pub selected_company_stock_repurchase_unassigned_share_pool: Option<u32>, pub selected_company_stock_repurchase_unassigned_share_pool: Option<u32>,
pub selected_company_stock_repurchase_eligible_for_single_batch: Option<bool>, pub selected_company_stock_repurchase_eligible_for_single_batch: Option<bool>,
pub selected_company_stock_issue_live_bond_count: Option<u8>,
pub selected_company_stock_issue_initial_batch_size: Option<u32>,
pub selected_company_stock_issue_trimmed_batch_size: Option<u32>,
pub selected_company_stock_issue_share_pressure_basis_points: Option<i64>,
pub selected_company_stock_issue_pressured_share_price_scalar: Option<i64>,
pub selected_company_stock_issue_pressured_proceeds: Option<i64>,
pub selected_company_stock_issue_book_value_per_share_floor_applied: Option<i64>,
pub selected_company_stock_issue_price_to_book_ratio_basis_points: Option<i64>,
pub selected_company_stock_issue_current_cash: Option<i64>,
pub selected_company_stock_issue_highest_coupon_live_bond_principal: Option<u32>,
pub selected_company_stock_issue_highest_coupon_live_bond_rate_basis_points: Option<i64>,
pub selected_company_stock_issue_current_issue_age_absolute_counter_delta: Option<i64>,
pub selected_company_stock_issue_current_issue_cooldown_floor: Option<i64>,
pub selected_company_stock_issue_minimum_price_to_book_ratio_basis_points: Option<i64>,
pub selected_company_stock_issue_passes_share_price_floor: Option<bool>,
pub selected_company_stock_issue_passes_proceeds_floor: Option<bool>,
pub selected_company_stock_issue_passes_cash_gate: Option<bool>,
pub selected_company_stock_issue_passes_issue_cooldown_gate: Option<bool>,
pub selected_company_stock_issue_passes_coupon_price_to_book_gate: Option<bool>,
pub selected_company_stock_issue_eligible_for_double_tranche: Option<bool>,
pub player_count: usize, pub player_count: usize,
pub chairman_profile_count: usize, pub chairman_profile_count: usize,
pub active_chairman_profile_count: usize, pub active_chairman_profile_count: usize,
@ -224,6 +245,9 @@ impl RuntimeSummary {
state.selected_company_id.and_then(|company_id| { state.selected_company_id.and_then(|company_id| {
runtime_company_annual_stock_repurchase_state(state, 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 { Self {
calendar: state.calendar, calendar: state.calendar,
calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(), calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(),
@ -550,6 +574,81 @@ impl RuntimeSummary {
selected_company_stock_repurchase_state selected_company_stock_repurchase_state
.as_ref() .as_ref()
.map(|repurchase_state| repurchase_state.eligible_for_single_batch_repurchase), .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(), player_count: state.players.len(),
chairman_profile_count: state.chairman_profiles.len(), chairman_profile_count: state.chairman_profiles.len(),
active_chairman_profile_count: state active_chairman_profile_count: state
@ -2811,4 +2910,216 @@ mod tests {
Some(false) 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)
);
}
} }

View file

@ -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 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 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 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 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 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- 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 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 growth setting plus the linked chairman personality byte, which is enough to run the annual
stock-repurchase gate headlessly as another pure reader stock-repurchase gate headlessly as another pure reader

View file

@ -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 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 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 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 annual-finance state, and now also exposes the highest live coupon rate, so the stock-capital
of guessing another finance leaf. The same fixed-world save block now also carries the raw stock, 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 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- 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 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. 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 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 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 ## Why This Boundary