Rehost annual stock repurchase service branch
This commit is contained in:
parent
6f36f6269d
commit
bb3c968cec
5 changed files with 359 additions and 30 deletions
|
|
@ -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,
|
||||
company_id: u32,
|
||||
share_pressure_shares: i64,
|
||||
|
|
@ -4066,7 +4066,7 @@ fn runtime_decode_saved_f64_bits(bits: u64) -> Option<f64> {
|
|||
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() {
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,21 @@ use crate::{
|
|||
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
|
||||
calendar::BoundaryEventKind, runtime_company_annual_dividend_policy_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_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 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)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
|
|
@ -197,6 +205,106 @@ fn service_periodic_boundary(
|
|||
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(
|
||||
state: &mut RuntimeState,
|
||||
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 {
|
||||
continue;
|
||||
};
|
||||
let Some(current_cash) = issue_state.current_cash else {
|
||||
continue;
|
||||
};
|
||||
let Some(current_tuple_word_0) =
|
||||
state.world_restore.current_calendar_tuple_word_raw_u32
|
||||
else {
|
||||
|
|
@ -287,19 +392,32 @@ fn service_company_annual_finance_policy(
|
|||
let Some(total_share_delta) = batch_size.checked_mul(2) else {
|
||||
continue;
|
||||
};
|
||||
let Some(total_cash_delta) = proceeds_per_tranche.checked_mul(2) else {
|
||||
continue;
|
||||
};
|
||||
let Some(next_cash) = current_cash.checked_add(total_cash_delta) else {
|
||||
continue;
|
||||
};
|
||||
let Some(company) = state
|
||||
.companies
|
||||
.iter_mut()
|
||||
.find(|company| company.company_id == company_id)
|
||||
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_add(total_share_delta))
|
||||
else {
|
||||
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
|
||||
.service_state
|
||||
.company_market_state
|
||||
|
|
@ -307,21 +425,89 @@ fn service_company_annual_finance_policy(
|
|||
else {
|
||||
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.prior_issue_calendar_word = market_state.current_issue_calendar_word;
|
||||
market_state.prior_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_2 = current_tuple_word_1;
|
||||
applied_effect_count += 1;
|
||||
mutated_company_ids.insert(company_id);
|
||||
if mutated {
|
||||
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]
|
||||
fn applies_company_effects_for_specific_targets() {
|
||||
let mut state = RuntimeState {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue