Rehost annual dividend policy branch

This commit is contained in:
Jan Petykiewicz 2026-04-18 01:00:21 -07:00
commit 90d213c9ed
6 changed files with 700 additions and 21 deletions

View file

@ -99,7 +99,11 @@ distress bankruptcy fallback is now rehosted on that same owner surface too, usi
cash reader seam plus the first three trailing net-profit years instead of another ad hoc probe. cash reader seam plus the first three trailing net-profit years instead of another ad hoc probe.
The annual bond lane now runs on that same owner surface too, using the simulated post-repayment The annual bond lane now runs on that same owner surface too, using the simulated post-repayment
cash window plus the linked-transit threshold split to stage `500000` principal issue counts as a cash window plus the linked-transit threshold split to stage `500000` principal issue counts as a
pure runtime reader. pure runtime reader. The annual dividend lane now runs there too: the runtime now rehosts the
shared year-or-control-transfer metric seam, the board-approved dividend ceiling helper, and the
full annual dividend adjustment branch over owned current cash, public float, current dividend,
building-growth policy, and recent profit history instead of leaving that policy on shell-side
dialog notes.
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

@ -53,23 +53,24 @@ pub use runtime::{
RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric,
RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany,
RuntimeCompanyAnnualBondPolicyState, RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualBondPolicyState, RuntimeCompanyAnnualCreditorPressureState,
RuntimeCompanyAnnualDeepDistressState, RuntimeCompanyAnnualFinanceState, RuntimeCompanyAnnualDeepDistressState, RuntimeCompanyAnnualDividendPolicyState,
RuntimeCompanyAnnualStockIssueState, 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,
runtime_company_annual_bond_policy_state, runtime_company_annual_creditor_pressure_state, RuntimeWorldIssueState, RuntimeWorldRestoreState, runtime_company_annual_bond_policy_state,
runtime_company_annual_deep_distress_state, runtime_company_annual_finance_state, runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_state,
runtime_company_annual_dividend_policy_state, runtime_company_annual_finance_state,
runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_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_assigned_share_pool, runtime_company_average_live_bond_coupon,
runtime_company_book_value_per_share, runtime_company_credit_rating, runtime_company_book_value_per_share, runtime_company_credit_rating,

View file

@ -339,6 +339,43 @@ pub struct RuntimeCompanyAnnualBondPolicyState {
pub eligible_for_bond_issue_branch: bool, pub eligible_for_bond_issue_branch: bool,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCompanyAnnualDividendPolicyState {
pub company_id: u32,
#[serde(default)]
pub annual_mode_active: Option<bool>,
#[serde(default)]
pub dividend_adjustment_allowed: Option<bool>,
#[serde(default)]
pub years_since_last_dividend: Option<u32>,
#[serde(default)]
pub years_since_founding: Option<u32>,
#[serde(default)]
pub outstanding_shares: Option<u32>,
#[serde(default)]
pub unassigned_share_pool: Option<u32>,
#[serde(default)]
pub weighted_recent_net_profit_total: Option<i64>,
#[serde(default)]
pub weighted_recent_net_profit_average: Option<i64>,
#[serde(default)]
pub current_cash: Option<i64>,
pub tiny_unassigned_share_cash_supplement_branch: bool,
#[serde(default)]
pub tentative_target_dividend_per_share_tenths: Option<i64>,
#[serde(default)]
pub current_dividend_per_share_tenths: Option<i64>,
#[serde(default)]
pub building_density_growth_setting: Option<u32>,
#[serde(default)]
pub growth_adjusted_current_dividend_per_share_tenths: Option<i64>,
#[serde(default)]
pub board_approved_dividend_rate_ceiling_tenths: Option<i64>,
#[serde(default)]
pub proposed_dividend_per_share_tenths: Option<i64>,
pub eligible_for_dividend_adjustment_branch: 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)]
@ -2615,6 +2652,34 @@ fn runtime_company_trailing_full_year_stat_series(
Some((year_words, values)) Some((year_words, values))
} }
fn runtime_company_year_or_control_transfer_metric_value_f64(
state: &RuntimeState,
company_id: u32,
year_word: u32,
slot_id: u32,
) -> Option<f64> {
let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?);
if year_word == current_year_word {
runtime_company_stat_value_f64(
state,
company_id,
RuntimeCompanyStatSelector {
family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
slot_id,
},
)
} else {
runtime_company_stat_value_f64(
state,
company_id,
RuntimeCompanyStatSelector {
family_id: year_word,
slot_id,
},
)
}
}
pub fn runtime_world_issue_opinion_term_sum_raw( pub fn runtime_world_issue_opinion_term_sum_raw(
state: &RuntimeState, state: &RuntimeState,
issue_id: u32, issue_id: u32,
@ -3115,6 +3180,224 @@ pub fn runtime_company_annual_bond_policy_state(
}) })
} }
fn runtime_company_board_approved_dividend_rate_ceiling_f64(
state: &RuntimeState,
company_id: u32,
) -> Option<f64> {
const REVENUE_GUARD_DIVISOR: f64 = 2.0;
const EARLY_SUPPORT_MULTIPLIER: f64 = 0.05;
const HISTORICAL_GUARD_SCALE: f64 = 1.25;
const ANCHOR_SCALE: f64 = 0.35;
let market_state = state.service_state.company_market_state.get(&company_id)?;
let current_cash = runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)?;
let shares_plus_one = market_state.outstanding_shares.checked_add(1)?;
let shares_plus_one_f64 = shares_plus_one as f64;
let current_cash_per_share_ceiling = current_cash / shares_plus_one_f64;
let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?);
let years_since_founding = current_year_word
.checked_sub(market_state.founding_year)
.unwrap_or(0)
.min(3);
let start_year_offset = if state.world_restore.partial_year_progress_raw_u8 == Some(0x0c) {
0
} else {
1
};
let mut strongest_net_profit_guard = 0.0f64;
let mut strongest_revenue_guard = 0.0f64;
if start_year_offset <= years_since_founding {
for year_offset in start_year_offset..=years_since_founding {
let year_word = current_year_word.checked_sub(year_offset)?;
let net_profit = runtime_company_year_or_control_transfer_metric_value_f64(
state, company_id, year_word, 0x2b,
)?;
strongest_net_profit_guard = strongest_net_profit_guard.max(net_profit);
let revenue = runtime_company_year_or_control_transfer_metric_value_f64(
state, company_id, year_word, 0x2c,
)?;
strongest_revenue_guard = strongest_revenue_guard.max(revenue);
}
}
let mut historical_guard_total =
strongest_net_profit_guard.min(strongest_revenue_guard / REVENUE_GUARD_DIVISOR);
if years_since_founding <= 1 {
let early_support_guard = market_state.outstanding_shares as f64
* runtime_decode_saved_f32_value_f64(
market_state.young_company_support_scalar_raw_u32,
)?
* EARLY_SUPPORT_MULTIPLIER;
historical_guard_total = historical_guard_total.max(early_support_guard);
}
let historical_guard_per_share_ceiling =
historical_guard_total / shares_plus_one_f64 * HISTORICAL_GUARD_SCALE;
let mut ceiling = current_cash_per_share_ceiling.min(historical_guard_per_share_ceiling);
let anchor_value = if years_since_founding == 0 {
runtime_decode_saved_f32_value_f64(market_state.young_company_support_scalar_raw_u32)?
} else {
runtime_company_year_or_control_transfer_metric_value_f64(
state,
company_id,
current_year_word.checked_sub(1)?,
0x1c,
)?
};
ceiling = ceiling.min(anchor_value * ANCHOR_SCALE);
Some(ceiling.max(0.0))
}
pub fn runtime_company_annual_dividend_policy_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualDividendPolicyState> {
const WEIGHTED_NET_PROFIT_DIVISOR: f64 = 6.0;
const CASH_SUPPLEMENT_DIVISOR: f64 = 3.0;
const STANDARD_TARGET_DIVISOR: f64 = 6.0;
const DIVIDEND_DELTA_COLLAPSE_THRESHOLD: f64 = 0.1;
const GROWTH_SETTING_ONE_DIVIDEND_SCALE: f64 = 0.66;
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 current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?);
let current_dividend_per_share =
runtime_company_control_transfer_stat_value_f64(state, company_id, 0x20)?;
let building_density_growth_setting = runtime_world_building_density_growth_setting(state);
let weighted_recent_net_profit_total = Some(
runtime_company_year_or_control_transfer_metric_value_f64(
state,
company_id,
current_year_word,
0x2b,
)
.and_then(runtime_round_f64_to_i64)?
.checked_mul(3)?
.checked_add(
runtime_company_year_or_control_transfer_metric_value_f64(
state,
company_id,
current_year_word.checked_sub(1)?,
0x2b,
)
.and_then(runtime_round_f64_to_i64)?
.checked_mul(2)?,
)?
.checked_add(
runtime_company_year_or_control_transfer_metric_value_f64(
state,
company_id,
current_year_word.checked_sub(2)?,
0x2b,
)
.and_then(runtime_round_f64_to_i64)?,
)?,
);
let weighted_recent_net_profit_average = weighted_recent_net_profit_total
.and_then(|value| runtime_round_f64_to_i64(value as f64 / WEIGHTED_NET_PROFIT_DIVISOR));
let tiny_unassigned_share_cash_supplement_branch =
annual_finance_state.unassigned_share_pool <= 1_000;
let tentative_target_dividend_per_share =
weighted_recent_net_profit_average.and_then(|value| {
if annual_finance_state.outstanding_shares == 0 {
return None;
}
let shares = annual_finance_state.outstanding_shares as f64;
if tiny_unassigned_share_cash_supplement_branch {
let cash_component = current_cash.unwrap_or(0).max(0) as f64;
Some(
((value as f64 / CASH_SUPPLEMENT_DIVISOR)
+ cash_component / CASH_SUPPLEMENT_DIVISOR)
/ shares,
)
} else {
Some((value as f64 / STANDARD_TARGET_DIVISOR) / shares)
}
});
let growth_adjusted_current_dividend_per_share = Some(match building_density_growth_setting {
Some(1) => current_dividend_per_share * GROWTH_SETTING_ONE_DIVIDEND_SCALE,
Some(2) => 0.0,
_ => current_dividend_per_share,
});
let proposed_dividend_per_share = if tentative_target_dividend_per_share
.is_some_and(|value| value <= DIVIDEND_DELTA_COLLAPSE_THRESHOLD)
{
Some(0.0)
} else {
growth_adjusted_current_dividend_per_share
.zip(tentative_target_dividend_per_share)
.map(|(current_dividend, target)| {
((current_dividend + target + DIVIDEND_DELTA_COLLAPSE_THRESHOLD) / 2.0 * 10.0)
.round()
/ 10.0
})
};
let board_approved_dividend_rate_ceiling =
runtime_company_board_approved_dividend_rate_ceiling_f64(state, company_id);
let proposed_dividend_per_share = proposed_dividend_per_share
.zip(board_approved_dividend_rate_ceiling)
.map(|(proposed, ceiling)| proposed.min(ceiling));
let current_dividend_per_share_tenths =
runtime_round_f64_to_i64(current_dividend_per_share * 10.0);
let eligible_for_dividend_adjustment_branch = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_dividend_adjustment_allowed(state) == Some(true)
&& annual_finance_state
.years_since_last_dividend
.is_some_and(|years| years >= 1)
&& annual_finance_state
.years_since_founding
.is_some_and(|years| years >= 2)
&& !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_bond_policy_state(state, company_id)?
.eligible_for_bond_issue_branch
&& !runtime_company_annual_stock_repurchase_state(state, company_id)?
.eligible_for_single_batch_repurchase
&& !runtime_company_annual_stock_issue_state(state, company_id)?
.eligible_for_double_tranche_issue
&& proposed_dividend_per_share.and_then(|value| runtime_round_f64_to_i64(value * 10.0))
!= current_dividend_per_share_tenths;
Some(RuntimeCompanyAnnualDividendPolicyState {
company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state),
dividend_adjustment_allowed: runtime_world_dividend_adjustment_allowed(state),
years_since_last_dividend: annual_finance_state.years_since_last_dividend,
years_since_founding: annual_finance_state.years_since_founding,
outstanding_shares: Some(annual_finance_state.outstanding_shares),
unassigned_share_pool: Some(annual_finance_state.unassigned_share_pool),
weighted_recent_net_profit_total,
weighted_recent_net_profit_average,
current_cash,
tiny_unassigned_share_cash_supplement_branch,
tentative_target_dividend_per_share_tenths: tentative_target_dividend_per_share
.and_then(|value| runtime_round_f64_to_i64(value * 10.0)),
current_dividend_per_share_tenths,
building_density_growth_setting,
growth_adjusted_current_dividend_per_share_tenths:
growth_adjusted_current_dividend_per_share
.and_then(|value| runtime_round_f64_to_i64(value * 10.0)),
board_approved_dividend_rate_ceiling_tenths: board_approved_dividend_rate_ceiling
.and_then(|value| runtime_round_f64_to_i64(value * 10.0)),
proposed_dividend_per_share_tenths: proposed_dividend_per_share
.and_then(|value| runtime_round_f64_to_i64(value * 10.0)),
eligible_for_dividend_adjustment_branch,
})
}
fn runtime_company_stock_issue_price_to_book_ratio_f64( fn runtime_company_stock_issue_price_to_book_ratio_f64(
pressured_support_adjusted_share_price_scalar: f64, pressured_support_adjusted_share_price_scalar: f64,
book_value_per_share: f64, book_value_per_share: f64,
@ -8019,6 +8302,163 @@ mod tests {
assert!(stock_issue_state.eligible_for_double_tranche_issue); assert!(stock_issue_state.eligible_for_double_tranche_issue);
} }
#[test]
fn derives_annual_dividend_policy_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();
};
let write_prior_year_value =
|bits: &mut Vec<u64>, slot_id: u32, year_delta: u32, value: f64| {
let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize;
bits[index] = value.to_bits();
};
write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.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 {
packed_year_word_raw_u16: Some(1845),
partial_year_progress_raw_u8: Some(0x0c),
dividend_policy_raw_u8: Some(0),
dividend_adjustment_allowed: Some(true),
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(1),
..RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 15,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
}],
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: vec![RuntimeChairmanProfile {
profile_id: 3,
name: "Chairman Three".to_string(),
active: true,
current_cash: 0,
linked_company_id: Some(15),
company_holdings: BTreeMap::from([(15, 9_500)]),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
}],
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
all_cargo_price_override: None,
named_cargo_price_overrides: BTreeMap::new(),
all_cargo_production_override: None,
factory_cargo_production_override: None,
farm_mine_cargo_production_override: None,
named_cargo_production_overrides: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
world_runtime_variables: BTreeMap::new(),
company_runtime_variables: BTreeMap::new(),
player_runtime_variables: BTreeMap::new(),
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState {
company_market_state: BTreeMap::from([(
15,
RuntimeCompanyMarketState {
outstanding_shares: 10_000,
founding_year: 1840,
last_dividend_year: 1844,
year_stat_family_qword_bits,
direct_control_transfer_float_fields_raw_u32: BTreeMap::from([(
0x33f,
0.4f32.to_bits(),
)]),
..RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
let dividend_state = runtime_company_annual_dividend_policy_state(&state, 15)
.expect("annual dividend policy state");
assert_eq!(dividend_state.years_since_last_dividend, Some(1));
assert_eq!(dividend_state.years_since_founding, Some(5));
assert_eq!(dividend_state.outstanding_shares, Some(10_000));
assert_eq!(dividend_state.unassigned_share_pool, Some(500));
assert_eq!(
dividend_state.weighted_recent_net_profit_total,
Some(600_000)
);
assert_eq!(
dividend_state.weighted_recent_net_profit_average,
Some(100_000)
);
assert_eq!(dividend_state.current_cash, Some(300_000));
assert!(dividend_state.tiny_unassigned_share_cash_supplement_branch);
assert_eq!(
dividend_state.tentative_target_dividend_per_share_tenths,
Some(133)
);
assert_eq!(dividend_state.current_dividend_per_share_tenths, Some(4));
assert_eq!(
dividend_state.growth_adjusted_current_dividend_per_share_tenths,
Some(3)
);
assert_eq!(
dividend_state.board_approved_dividend_rate_ceiling_tenths,
Some(18)
);
assert_eq!(dividend_state.proposed_dividend_per_share_tenths, Some(18));
assert!(dividend_state.eligible_for_dividend_adjustment_branch);
}
#[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,8 +3,9 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
CalendarPoint, RuntimeState, runtime_company_annual_bond_policy_state, CalendarPoint, RuntimeState, runtime_company_annual_bond_policy_state,
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_issue_state, runtime_company_annual_dividend_policy_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 {
@ -156,6 +157,16 @@ pub struct RuntimeSummary {
pub selected_company_stock_issue_passes_issue_cooldown_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_passes_coupon_price_to_book_gate: Option<bool>,
pub selected_company_stock_issue_eligible_for_double_tranche: Option<bool>, pub selected_company_stock_issue_eligible_for_double_tranche: Option<bool>,
pub selected_company_dividend_weighted_recent_net_profit_total: Option<i64>,
pub selected_company_dividend_weighted_recent_net_profit_average: Option<i64>,
pub selected_company_dividend_current_cash: Option<i64>,
pub selected_company_dividend_tiny_unassigned_share_cash_supplement_branch: Option<bool>,
pub selected_company_dividend_tentative_target_per_share_tenths: Option<i64>,
pub selected_company_dividend_current_per_share_tenths: Option<i64>,
pub selected_company_dividend_growth_adjusted_current_per_share_tenths: Option<i64>,
pub selected_company_dividend_board_approved_ceiling_tenths: Option<i64>,
pub selected_company_dividend_proposed_per_share_tenths: Option<i64>,
pub selected_company_dividend_eligible_for_adjustment_branch: 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,
@ -262,6 +273,9 @@ impl RuntimeSummary {
let selected_company_stock_issue_state = state let selected_company_stock_issue_state = state
.selected_company_id .selected_company_id
.and_then(|company_id| runtime_company_annual_stock_issue_state(state, company_id)); .and_then(|company_id| runtime_company_annual_stock_issue_state(state, company_id));
let selected_company_dividend_state = state
.selected_company_id
.and_then(|company_id| runtime_company_annual_dividend_policy_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(),
@ -702,6 +716,51 @@ impl RuntimeSummary {
selected_company_stock_issue_state selected_company_stock_issue_state
.as_ref() .as_ref()
.map(|issue_state| issue_state.eligible_for_double_tranche_issue), .map(|issue_state| issue_state.eligible_for_double_tranche_issue),
selected_company_dividend_weighted_recent_net_profit_total:
selected_company_dividend_state
.as_ref()
.and_then(|dividend_state| dividend_state.weighted_recent_net_profit_total),
selected_company_dividend_weighted_recent_net_profit_average:
selected_company_dividend_state
.as_ref()
.and_then(|dividend_state| dividend_state.weighted_recent_net_profit_average),
selected_company_dividend_current_cash: selected_company_dividend_state
.as_ref()
.and_then(|dividend_state| dividend_state.current_cash),
selected_company_dividend_tiny_unassigned_share_cash_supplement_branch:
selected_company_dividend_state
.as_ref()
.map(|dividend_state| {
dividend_state.tiny_unassigned_share_cash_supplement_branch
}),
selected_company_dividend_tentative_target_per_share_tenths:
selected_company_dividend_state
.as_ref()
.and_then(|dividend_state| {
dividend_state.tentative_target_dividend_per_share_tenths
}),
selected_company_dividend_current_per_share_tenths: selected_company_dividend_state
.as_ref()
.and_then(|dividend_state| dividend_state.current_dividend_per_share_tenths),
selected_company_dividend_growth_adjusted_current_per_share_tenths:
selected_company_dividend_state
.as_ref()
.and_then(|dividend_state| {
dividend_state.growth_adjusted_current_dividend_per_share_tenths
}),
selected_company_dividend_board_approved_ceiling_tenths:
selected_company_dividend_state
.as_ref()
.and_then(|dividend_state| {
dividend_state.board_approved_dividend_rate_ceiling_tenths
}),
selected_company_dividend_proposed_per_share_tenths: selected_company_dividend_state
.as_ref()
.and_then(|dividend_state| dividend_state.proposed_dividend_per_share_tenths),
selected_company_dividend_eligible_for_adjustment_branch:
selected_company_dividend_state
.as_ref()
.map(|dividend_state| dividend_state.eligible_for_dividend_adjustment_branch),
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
@ -3322,4 +3381,173 @@ mod tests {
Some(true) Some(true)
); );
} }
#[test]
fn summarizes_selected_company_annual_dividend_policy_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
];
let write_current_value = |bits: &mut Vec<u64>, slot_id: u32, value: f64| {
let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize;
bits[index] = value.to_bits();
};
let write_prior_year_value =
|bits: &mut Vec<u64>, slot_id: u32, year_delta: u32, value: f64| {
let index =
(slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize;
bits[index] = value.to_bits();
};
write_current_value(&mut year_stat_family_qword_bits, 0x0d, 300_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x01, 300_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x09, -180_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 280_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -190_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 260_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -200_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x1c, 1, 5.0);
let state = RuntimeState {
calendar: 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),
dividend_policy_raw_u8: Some(0),
dividend_adjustment_allowed: Some(true),
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(1),
..crate::RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany {
company_id: 15,
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: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
track_piece_counts: crate::RuntimeTrackPieceCounts::default(),
}],
selected_company_id: Some(15),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: vec![crate::RuntimeChairmanProfile {
profile_id: 3,
name: "Chairman Three".to_string(),
active: true,
current_cash: 0,
linked_company_id: Some(15),
company_holdings: BTreeMap::from([(15, 9_500)]),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
}],
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
all_cargo_price_override: None,
named_cargo_price_overrides: BTreeMap::new(),
all_cargo_production_override: None,
factory_cargo_production_override: None,
farm_mine_cargo_production_override: None,
named_cargo_production_overrides: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
world_runtime_variables: BTreeMap::new(),
company_runtime_variables: BTreeMap::new(),
player_runtime_variables: BTreeMap::new(),
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: crate::RuntimeServiceState {
company_market_state: BTreeMap::from([(
15,
crate::RuntimeCompanyMarketState {
outstanding_shares: 10_000,
founding_year: 1840,
last_dividend_year: 1844,
year_stat_family_qword_bits,
direct_control_transfer_float_fields_raw_u32: BTreeMap::from([(
0x33f,
0.4f32.to_bits(),
)]),
..crate::RuntimeCompanyMarketState::default()
},
)]),
..crate::RuntimeServiceState::default()
},
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(
summary.selected_company_dividend_weighted_recent_net_profit_total,
Some(600_000)
);
assert_eq!(
summary.selected_company_dividend_weighted_recent_net_profit_average,
Some(100_000)
);
assert_eq!(
summary.selected_company_dividend_current_cash,
Some(300_000)
);
assert_eq!(
summary.selected_company_dividend_tiny_unassigned_share_cash_supplement_branch,
Some(true)
);
assert_eq!(
summary.selected_company_dividend_tentative_target_per_share_tenths,
Some(133)
);
assert_eq!(
summary.selected_company_dividend_current_per_share_tenths,
Some(4)
);
assert_eq!(
summary.selected_company_dividend_growth_adjusted_current_per_share_tenths,
Some(3)
);
assert_eq!(
summary.selected_company_dividend_board_approved_ceiling_tenths,
Some(18)
);
assert_eq!(
summary.selected_company_dividend_proposed_per_share_tenths,
Some(18)
);
assert_eq!(
summary.selected_company_dividend_eligible_for_adjustment_branch,
Some(true)
);
}
} }

View file

@ -140,7 +140,9 @@ The highest-value next passes are 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 bond, stock-repurchase, and stock-capital issue branches now do too profit seam; the annual bond, 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 annual dividend-adjustment branch now does as well through the
shared year-or-control-transfer reader and board-approved dividend ceiling helper; 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
- 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

View file

@ -234,6 +234,10 @@ 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.
The annual bond lane now rides it as well, using the simulated post-repayment cash window plus the The annual bond lane now rides it as well, using the simulated post-repayment cash window plus the
linked-transit threshold split to stage `500000` principal issue counts without shell ownership. linked-transit threshold split to stage `500000` principal issue counts without shell ownership.
The annual dividend-adjustment lane now rides that same seam too: the runtime now rehosts the
shared year-or-control-transfer metric reader, the board-approved dividend ceiling helper, and the
full annual dividend branch over owned cash, public float, current dividend, and building-growth
policy instead of treating dividend changes as shell-dialog-only logic.
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. The stock-capital issue branch now save/runtime state instead of another threshold-only note. The stock-capital issue branch now