Rehost annual stock repurchase service branch

This commit is contained in:
Jan Petykiewicz 2026-04-18 01:27:13 -07:00
commit bb3c968cec
5 changed files with 359 additions and 30 deletions

View file

@ -105,8 +105,9 @@ full annual dividend adjustment branch over owned current cash, public float, cu
building-growth policy, and recent profit history instead of leaving that policy on shell-side building-growth policy, and recent profit history instead of leaving that policy on shell-side
dialog notes. `simulation_service_periodic_boundary_work` is now beginning to use that same owner dialog notes. `simulation_service_periodic_boundary_work` is now beginning to use that same owner
surface too: the runtime chooses one annual-finance action per active company and already commits surface too: the runtime chooses one annual-finance action per active company and already commits
the shellless dividend-adjustment and stock-issue branches by mutating owned dividend, cash, the shellless dividend-adjustment, stock-repurchase, and stock-issue branches by mutating owned
outstanding-share, and issue-calendar state instead of stopping at reader-only diagnostics. dividend, company stat-post, outstanding-share, and issue-calendar state instead of stopping at
reader-only diagnostics.
The same seam now also carries the fixed-world building-density growth setting plus the linked The same 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 as another chairman personality byte, which is enough to run the annual stock-repurchase gate as another
pure reader over owned save-native state instead of a guessed finance-side approximation. pure reader over owned save-native state instead of a guessed finance-side approximation.

View file

@ -2353,7 +2353,7 @@ fn runtime_company_total_live_bond_principal(state: &RuntimeState, company_id: u
) )
} }
fn runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( pub(crate) 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, share_pressure_shares: i64,
@ -4066,7 +4066,7 @@ fn runtime_decode_saved_f64_bits(bits: u64) -> Option<f64> {
Some(value) Some(value)
} }
fn runtime_round_f64_to_i64(value: f64) -> Option<i64> { pub(crate) fn runtime_round_f64_to_i64(value: f64) -> Option<i64> {
if !value.is_finite() { if !value.is_finite() {
return None; return None;
} }

View file

@ -10,13 +10,21 @@ use crate::{
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind, runtime_company_annual_dividend_policy_state, calendar::BoundaryEventKind, runtime_company_annual_dividend_policy_state,
runtime_company_annual_finance_policy_state, runtime_company_annual_stock_issue_state, runtime_company_annual_finance_policy_state, runtime_company_annual_stock_issue_state,
runtime_company_annual_stock_repurchase_state,
runtime_company_book_value_per_share, runtime_company_credit_rating, runtime_company_book_value_per_share, runtime_company_credit_rating,
runtime_company_investor_confidence, runtime_company_management_attitude, runtime_company_investor_confidence, runtime_company_management_attitude,
runtime_company_prime_rate, runtime_company_prime_rate, RUNTIME_COMPANY_STAT_SLOT_COUNT,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN,
};
use crate::runtime::{
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64,
runtime_round_f64_to_i64,
}; };
const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4]; const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
const COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT: u32 = 0x33f; const COMPANY_DIRECT_DIVIDEND_RATE_FIELD_SLOT: u32 = 0x33f;
const COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE: f64 = -0.02;
const COMPANY_REPURCHASE_PRESSURE_SCALE: f64 = 0.7;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
@ -197,6 +205,106 @@ fn service_periodic_boundary(
Ok(()) Ok(())
} }
fn service_decode_saved_f64_bits(raw_bits: u64) -> Option<f64> {
let value = f64::from_bits(raw_bits);
value.is_finite().then_some(value)
}
fn service_ensure_company_stat_post_capacity(
market_state: &mut crate::RuntimeCompanyMarketState,
slot_id: u32,
) -> Option<usize> {
let index = slot_id
.checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)?
.try_into()
.ok()?;
let required_year_len = ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2)
* RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize;
if market_state.year_stat_family_qword_bits.len() < required_year_len {
market_state
.year_stat_family_qword_bits
.resize(required_year_len, 0.0f64.to_bits());
}
let required_special_len = RUNTIME_COMPANY_STAT_SLOT_COUNT as usize;
if market_state.special_stat_family_232a_qword_bits.len() < required_special_len {
market_state
.special_stat_family_232a_qword_bits
.resize(required_special_len, 0.0f64.to_bits());
}
Some(index)
}
fn service_post_company_stat_delta(
state: &mut RuntimeState,
company_id: u32,
slot_id: u32,
delta: f64,
mirror_cash_totals: bool,
) -> bool {
if !delta.is_finite() {
return false;
}
let Some(refreshed_current_cash) = ({
let Some(market_state) = state.service_state.company_market_state.get_mut(&company_id) else {
return false;
};
let Some(index) = service_ensure_company_stat_post_capacity(market_state, slot_id) else {
return false;
};
let prior_year_value = market_state
.year_stat_family_qword_bits
.get(index)
.copied()
.and_then(service_decode_saved_f64_bits)
.unwrap_or(0.0);
market_state.year_stat_family_qword_bits[index] = (prior_year_value + delta).to_bits();
let special_index = slot_id as usize;
let prior_special_value = market_state
.special_stat_family_232a_qword_bits
.get(special_index)
.copied()
.and_then(service_decode_saved_f64_bits)
.unwrap_or(0.0);
market_state.special_stat_family_232a_qword_bits[special_index] =
(prior_special_value + delta).to_bits();
if mirror_cash_totals {
let cash_index = RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize;
let prior_cash_shadow_value = market_state
.special_stat_family_232a_qword_bits
.get(cash_index)
.copied()
.and_then(service_decode_saved_f64_bits)
.unwrap_or(0.0);
market_state.special_stat_family_232a_qword_bits[cash_index] =
(prior_cash_shadow_value + delta).to_bits();
}
market_state
.year_stat_family_qword_bits
.get((RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize)
.copied()
.and_then(service_decode_saved_f64_bits)
.and_then(runtime_round_f64_to_i64)
}) else {
return false;
};
if let Some(company) = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
{
company.current_cash = refreshed_current_cash;
true
} else {
false
}
}
fn service_company_annual_finance_policy( fn service_company_annual_finance_policy(
state: &mut RuntimeState, state: &mut RuntimeState,
service_events: &mut Vec<ServiceEvent>, service_events: &mut Vec<ServiceEvent>,
@ -271,9 +379,6 @@ fn service_company_annual_finance_policy(
let Some(proceeds_per_tranche) = issue_state.pressured_proceeds else { let Some(proceeds_per_tranche) = issue_state.pressured_proceeds else {
continue; continue;
}; };
let Some(current_cash) = issue_state.current_cash else {
continue;
};
let Some(current_tuple_word_0) = let Some(current_tuple_word_0) =
state.world_restore.current_calendar_tuple_word_raw_u32 state.world_restore.current_calendar_tuple_word_raw_u32
else { else {
@ -287,19 +392,32 @@ fn service_company_annual_finance_policy(
let Some(total_share_delta) = batch_size.checked_mul(2) else { let Some(total_share_delta) = batch_size.checked_mul(2) else {
continue; continue;
}; };
let Some(total_cash_delta) = proceeds_per_tranche.checked_mul(2) else { let Some(next_outstanding_shares) = state
continue; .service_state
}; .company_market_state
let Some(next_cash) = current_cash.checked_add(total_cash_delta) else { .get(&company_id)
continue; .map(|market_state| market_state.outstanding_shares)
}; .and_then(|value| value.checked_add(total_share_delta))
let Some(company) = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
else { else {
continue; continue;
}; };
let mut mutated = false;
for _ in 0..2 {
mutated |= service_post_company_stat_delta(
state,
company_id,
0x0c,
(batch_size as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE,
true,
);
mutated |= service_post_company_stat_delta(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
proceeds_per_tranche as f64,
false,
);
}
let Some(market_state) = state let Some(market_state) = state
.service_state .service_state
.company_market_state .company_market_state
@ -307,21 +425,89 @@ fn service_company_annual_finance_policy(
else { else {
continue; continue;
}; };
let Some(next_outstanding_shares) = market_state
.outstanding_shares
.checked_add(total_share_delta)
else {
continue;
};
company.current_cash = next_cash;
market_state.outstanding_shares = next_outstanding_shares; market_state.outstanding_shares = next_outstanding_shares;
market_state.prior_issue_calendar_word = market_state.current_issue_calendar_word; market_state.prior_issue_calendar_word = market_state.current_issue_calendar_word;
market_state.prior_issue_calendar_word_2 = market_state.prior_issue_calendar_word_2 =
market_state.current_issue_calendar_word_2; market_state.current_issue_calendar_word_2;
market_state.current_issue_calendar_word = current_tuple_word_0; market_state.current_issue_calendar_word = current_tuple_word_0;
market_state.current_issue_calendar_word_2 = current_tuple_word_1; market_state.current_issue_calendar_word_2 = current_tuple_word_1;
applied_effect_count += 1; if mutated {
mutated_company_ids.insert(company_id); applied_effect_count += 1;
mutated_company_ids.insert(company_id);
}
}
crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => {
let mut mutated = false;
for _ in 0..128 {
let Some(repurchase_state) =
runtime_company_annual_stock_repurchase_state(state, company_id)
else {
break;
};
if !repurchase_state.eligible_for_single_batch_repurchase {
break;
}
let Some(batch_size) = repurchase_state.repurchase_batch_size else {
break;
};
let Some(pressure_shares) =
runtime_round_f64_to_i64(batch_size as f64 * COMPANY_REPURCHASE_PRESSURE_SCALE)
else {
break;
};
let Some(share_price_scalar) =
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(
state,
company_id,
pressure_shares,
)
else {
break;
};
let Some(repurchase_total) =
runtime_round_f64_to_i64(share_price_scalar * batch_size as f64)
else {
break;
};
if repurchase_total <= 0 {
break;
}
let Some(next_outstanding_shares) = state
.service_state
.company_market_state
.get(&company_id)
.map(|market_state| market_state.outstanding_shares)
.and_then(|value| value.checked_sub(batch_size))
else {
break;
};
mutated |= service_post_company_stat_delta(
state,
company_id,
0x0c,
(repurchase_total as f64) * COMPANY_STOCK_AND_BOND_CAPITAL_POST_SCALE,
true,
);
mutated |= service_post_company_stat_delta(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
-(repurchase_total as f64),
false,
);
let Some(market_state) = state
.service_state
.company_market_state
.get_mut(&company_id)
else {
break;
};
market_state.outstanding_shares = next_outstanding_shares;
}
if mutated {
applied_effect_count += 1;
mutated_company_ids.insert(company_id);
}
} }
_ => {} _ => {}
} }
@ -2407,6 +2593,148 @@ mod tests {
); );
} }
#[test]
fn periodic_boundary_applies_stock_repurchase_from_annual_finance_policy() {
let mut year_stat_family_qword_bits = vec![
0u64;
(crate::RUNTIME_COMPANY_STAT_SLOT_COUNT * 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] = 1_600_000.0f64.to_bits();
let base_state = RuntimeState {
calendar: crate::CalendarPoint {
year: 1845,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: crate::RuntimeSaveProfileState::default(),
world_restore: crate::RuntimeWorldRestoreState {
packed_year_word_raw_u16: Some(1845),
partial_year_progress_raw_u8: Some(0x0c),
absolute_counter_raw_u32: Some(1_000),
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),
bankruptcy_policy_raw_u8: Some(0),
bankruptcy_allowed: Some(true),
building_density_growth_setting_raw_u32: Some(0),
..crate::RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany {
company_id: 23,
controller_kind: crate::RuntimeCompanyControllerKind::Unknown,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: crate::RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
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,
}],
selected_company_id: Some(23),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: vec![crate::RuntimeChairmanProfile {
profile_id: 8,
name: "Chairman".to_string(),
active: true,
current_cash: 0,
linked_company_id: Some(23),
company_holdings: BTreeMap::from([(23, 9_000)]),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
}],
selected_chairman_profile_id: Some(8),
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: crate::RuntimeServiceState {
world_issue_opinion_base_terms_raw_i32: vec![0; 0x3b],
company_market_state: BTreeMap::from([(
23,
crate::RuntimeCompanyMarketState {
outstanding_shares: 10_000,
founding_year: 1840,
city_connection_latch: true,
recent_per_share_cache_absolute_counter: 1_000,
recent_per_share_cached_value_bits: 50.0f64.to_bits(),
mutable_support_scalar_raw_u32: 0.0f32.to_bits(),
young_company_support_scalar_raw_u32: 0.0f32.to_bits(),
year_stat_family_qword_bits,
..crate::RuntimeCompanyMarketState::default()
},
)]),
..crate::RuntimeServiceState::default()
},
};
let pressured_share_price = crate::runtime::
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(
&base_state, 23, 700,
)
.expect("repurchase share price");
let expected_repurchase_total =
crate::runtime::runtime_round_f64_to_i64(pressured_share_price * 1_000.0)
.expect("repurchase total should round");
let mut state = base_state;
let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary)
.expect("periodic boundary should apply annual stock repurchase policy");
assert_eq!(
state.service_state.annual_finance_last_actions.get(&23),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase)
);
assert_eq!(state.companies[0].current_cash, 1_600_000 - expected_repurchase_total);
assert_eq!(
state.service_state.company_market_state[&23].outstanding_shares,
9_000
);
assert!(
result
.service_events
.iter()
.any(|event| event.kind == "annual_finance_policy"
&& event.applied_effect_count == 1
&& event.mutated_company_ids == vec![23])
);
}
#[test] #[test]
fn applies_company_effects_for_specific_targets() { fn applies_company_effects_for_specific_targets() {
let mut state = RuntimeState { let mut state = RuntimeState {

View file

@ -146,7 +146,7 @@ The highest-value next passes are now:
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; periodic boundary service now also stock-repurchase gate headlessly as another pure reader; periodic boundary service now also
chooses one annual-finance action per active company and already commits the shellless chooses one annual-finance action per active company and already commits the shellless
dividend-adjustment and stock-issue branches against owned runtime state dividend-adjustment, stock-repurchase, and stock-issue branches against owned runtime state
- the project rule on the remaining closure work is now explicit too: when one runtime-facing field - 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 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 instead of guessing another derived leaf field from neighboring raw offsets

View file

@ -244,8 +244,8 @@ save/runtime state instead of another threshold-only note. The stock-capital iss
rides that same seam too, with share-pressure, cooldown, and price-to-book gate state exposed as rides that same seam too, with share-pressure, cooldown, and price-to-book gate state exposed as
normal runtime readers. Periodic boundary service now also uses that owner seam as a real chooser: normal runtime readers. Periodic boundary service now also uses that owner seam as a real chooser:
the runtime selects one annual-finance action per active company and already commits the shellless the runtime selects one annual-finance action per active company and already commits the shellless
dividend-adjustment and stock-issue branches directly into owned dividend, cash, outstanding-share, dividend-adjustment, stock-repurchase, and stock-issue branches directly into owned dividend,
and issue-calendar state. company stat-post, outstanding-share, and issue-calendar state.
## Why This Boundary ## Why This Boundary